From 036602588eff1c9ac0170abd0d231a65de4a1678 Mon Sep 17 00:00:00 2001 From: Jage9 Date: Fri, 20 Feb 2026 17:46:43 -0500 Subject: [PATCH] Add escape-confirm disconnect and option-list effect picker --- client/public/version.js | 2 +- client/src/main.ts | 96 ++++++++++++++++++++++++++++++++++- client/src/state/gameState.ts | 7 ++- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/client/public/version.js b/client/public/version.js index 43a69b0..044bf03 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,3 +1,3 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.20 R63"; +window.CHGRID_WEB_VERSION = "2026.02.20 R64"; diff --git a/client/src/main.ts b/client/src/main.ts index 25c7889..00da775 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -98,6 +98,9 @@ const EDITABLE_ITEM_PROPERTY_KEYS = new Set([ 'sides', 'number', ]); +const OPTION_ITEM_PROPERTY_VALUES: Partial> = { + effect: EFFECT_SEQUENCE.map((effect) => effect.id), +}; const APP_BASE_URL = import.meta.env.BASE_URL || '/'; function withBase(path: string): string { const normalizedBase = APP_BASE_URL.endsWith('/') ? APP_BASE_URL : `${APP_BASE_URL}/`; @@ -145,6 +148,7 @@ const sharedRadioSources = new Map(); const itemRadioOutputs = new Map(); let replaceTextOnNextType = false; let itemPropertiesReadOnly = false; +let pendingEscapeDisconnect = false; const signalingProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const signalingUrl = `${signalingProtocol}://${window.location.host}/ws`; @@ -362,6 +366,9 @@ function beginItemProperties(item: WorldItem, readOnly = false): void { state.selectedItemId = item.id; state.mode = 'itemProperties'; itemPropertiesReadOnly = readOnly; + state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; if (readOnly) { state.itemPropertyKeys = getInspectItemPropertyKeys(item); } else { @@ -383,6 +390,21 @@ function useItem(item: WorldItem): void { signaling.send({ type: 'item_use', itemId: item.id }); } +function openItemPropertyOptionSelect(item: WorldItem, key: string): void { + const options = OPTION_ITEM_PROPERTY_VALUES[key]; + if (!options || options.length === 0) { + return; + } + state.mode = 'itemPropertyOptionSelect'; + state.editingPropertyKey = key; + state.itemPropertyOptionValues = options; + const currentValue = getItemPropertyValue(item, key); + const currentIndex = options.indexOf(currentValue); + state.itemPropertyOptionIndex = currentIndex >= 0 ? currentIndex : 0; + updateStatus(`Select ${key}: ${state.itemPropertyOptionValues[state.itemPropertyOptionIndex]}`); + audio.sfxUiBlip(); +} + function releaseSharedRadioSource(streamUrl: string): void { const shared = sharedRadioSources.get(streamUrl); if (!shared) return; @@ -824,6 +846,10 @@ function disconnect(): void { state.itemPropertyKeys = []; state.itemPropertyIndex = 0; state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; + itemPropertiesReadOnly = false; + pendingEscapeDisconnect = false; connecting = false; dom.nicknameContainer.classList.remove('hidden'); @@ -1016,6 +1042,10 @@ function toggleMute(): void { } function handleNormalModeInput(code: string, shiftKey: boolean): void { + if (code !== 'Escape' && pendingEscapeDisconnect) { + pendingEscapeDisconnect = false; + } + if (code === 'KeyN') { state.mode = 'nickname'; state.nicknameInput = state.player.nickname; @@ -1303,7 +1333,15 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { } if (code === 'Escape') { - disconnect(); + if (pendingEscapeDisconnect) { + pendingEscapeDisconnect = false; + disconnect(); + return; + } + pendingEscapeDisconnect = true; + updateStatus('Press Escape again to disconnect.'); + audio.sfxUiCancel(); + return; } } @@ -1519,12 +1557,18 @@ function handleItemPropertiesModeInput(code: string): void { if (!itemId) { state.mode = 'normal'; itemPropertiesReadOnly = false; + state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; return; } const item = state.items.get(itemId); if (!item) { state.mode = 'normal'; itemPropertiesReadOnly = false; + state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; updateStatus('Item no longer exists.'); audio.sfxUiCancel(); return; @@ -1554,6 +1598,10 @@ function handleItemPropertiesModeInput(code: string): void { audio.sfxUiBlip(); return; } + if (OPTION_ITEM_PROPERTY_VALUES[key]) { + openItemPropertyOptionSelect(item, key); + return; + } state.mode = 'itemPropertyEdit'; state.editingPropertyKey = key; state.nicknameInput = @@ -1576,6 +1624,9 @@ function handleItemPropertiesModeInput(code: string): void { state.selectedItemId = null; state.itemPropertyKeys = []; state.itemPropertyIndex = 0; + state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; updateStatus('Closed item properties.'); audio.sfxUiCancel(); } @@ -1684,6 +1735,47 @@ function handleItemPropertyEditModeInput(code: string, key: string): void { } } +function handleItemPropertyOptionSelectModeInput(code: string): void { + const itemId = state.selectedItemId; + const propertyKey = state.editingPropertyKey; + if (!itemId || !propertyKey || state.itemPropertyOptionValues.length === 0) { + state.mode = 'itemProperties'; + state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; + return; + } + + if (code === 'ArrowDown' || code === 'ArrowUp') { + state.itemPropertyOptionIndex = + code === 'ArrowDown' + ? (state.itemPropertyOptionIndex + 1) % state.itemPropertyOptionValues.length + : (state.itemPropertyOptionIndex - 1 + state.itemPropertyOptionValues.length) % state.itemPropertyOptionValues.length; + updateStatus(`${propertyKey}: ${state.itemPropertyOptionValues[state.itemPropertyOptionIndex]}`); + audio.sfxUiBlip(); + return; + } + + if (code === 'Enter') { + const selectedValue = state.itemPropertyOptionValues[state.itemPropertyOptionIndex]; + signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: selectedValue } }); + state.mode = 'itemProperties'; + state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; + return; + } + + if (code === 'Escape') { + state.mode = 'itemProperties'; + state.editingPropertyKey = null; + state.itemPropertyOptionValues = []; + state.itemPropertyOptionIndex = 0; + updateStatus('Cancelled.'); + audio.sfxUiCancel(); + } +} + function handleNicknameModeInput(code: string, key: string): void { if (code === 'Enter') { const clean = sanitizeName(state.nicknameInput); @@ -1778,6 +1870,8 @@ function setupInputHandlers(): void { handleItemPropertiesModeInput(code); } else if (state.mode === 'itemPropertyEdit') { handleItemPropertyEditModeInput(code, event.key); + } else if (state.mode === 'itemPropertyOptionSelect') { + handleItemPropertyOptionSelectModeInput(code); } else { handleNormalModeInput(code, event.shiftKey); } diff --git a/client/src/state/gameState.ts b/client/src/state/gameState.ts index bde474e..5a4cf6e 100644 --- a/client/src/state/gameState.ts +++ b/client/src/state/gameState.ts @@ -31,7 +31,8 @@ export type GameMode = | 'addItem' | 'selectItem' | 'itemProperties' - | 'itemPropertyEdit'; + | 'itemPropertyEdit' + | 'itemPropertyOptionSelect'; export type Player = { id: string | null; @@ -66,6 +67,8 @@ export type GameState = { itemPropertyKeys: string[]; itemPropertyIndex: number; editingPropertyKey: string | null; + itemPropertyOptionValues: string[]; + itemPropertyOptionIndex: number; addItemTypeIndex: number; isMuted: boolean; player: Player; @@ -93,6 +96,8 @@ export function createInitialState(): GameState { itemPropertyKeys: [], itemPropertyIndex: 0, editingPropertyKey: null, + itemPropertyOptionValues: [], + itemPropertyOptionIndex: 0, addItemTypeIndex: 0, isMuted: false, player: {