diff --git a/client/index.html b/client/index.html index 7f8df3c..e85e3e9 100644 --- a/client/index.html +++ b/client/index.html @@ -63,7 +63,7 @@

M: Mute/unmute

Shift+M: Toggle stereo/mono output

! (Shift+1): Toggle loopback monitor

-

E: Cycle voice effect

+

E: Select voice effect

Dash or Equals: Lower/raise active effect value

diff --git a/client/public/version.js b/client/public/version.js index a6f6b21..8e8a18f 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.21 R80"; +window.CHGRID_WEB_VERSION = "2026.02.21 R81"; diff --git a/client/src/audio/audioEngine.ts b/client/src/audio/audioEngine.ts index ff647a1..e4405b3 100644 --- a/client/src/audio/audioEngine.ts +++ b/client/src/audio/audioEngine.ts @@ -111,6 +111,13 @@ export class AudioEngine { return EFFECT_SEQUENCE[this.effectIndex]; } + setOutboundEffect(effectId: EffectId): { id: EffectId; label: string } { + const nextIndex = EFFECT_SEQUENCE.findIndex((effect) => effect.id === effectId); + this.effectIndex = nextIndex >= 0 ? nextIndex : this.effectIndex; + this.rebuildOutboundEffectGraph(); + return EFFECT_SEQUENCE[this.effectIndex]; + } + getCurrentEffect(): { id: EffectId; label: string; value: number; defaultValue: number } { const effect = EFFECT_SEQUENCE[this.effectIndex]; return { diff --git a/client/src/main.ts b/client/src/main.ts index bdfa773..a7208b7 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1092,6 +1092,7 @@ function disconnect(): void { state.editingPropertyKey = null; state.itemPropertyOptionValues = []; state.itemPropertyOptionIndex = 0; + state.effectSelectIndex = 0; pendingEscapeDisconnect = false; connecting = false; @@ -1327,8 +1328,11 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { } if (code === 'KeyE') { - const effect = audio.cycleOutboundEffect(); - updateStatus(effect.label); + const currentEffect = audio.getCurrentEffect(); + const currentIndex = EFFECT_SEQUENCE.findIndex((effect) => effect.id === currentEffect.id); + state.effectSelectIndex = currentIndex >= 0 ? currentIndex : 0; + state.mode = 'effectSelect'; + updateStatus(`Select effect: ${EFFECT_SEQUENCE[state.effectSelectIndex].label}`); audio.sfxUiBlip(); return; } @@ -1625,6 +1629,46 @@ function handleChatModeInput(code: string, key: string): void { applyTextInputEdit(code, key, 500); } +function handleEffectSelectModeInput(code: string, key: string): void { + if (code === 'ArrowDown' || code === 'ArrowUp') { + state.effectSelectIndex = + code === 'ArrowDown' + ? (state.effectSelectIndex + 1) % EFFECT_SEQUENCE.length + : (state.effectSelectIndex - 1 + EFFECT_SEQUENCE.length) % EFFECT_SEQUENCE.length; + updateStatus(`Select effect: ${EFFECT_SEQUENCE[state.effectSelectIndex].label}`); + audio.sfxUiBlip(); + return; + } + + const nextByInitial = findNextIndexByInitial( + EFFECT_SEQUENCE, + state.effectSelectIndex, + key, + (effect) => effect.label, + ); + if (nextByInitial >= 0) { + state.effectSelectIndex = nextByInitial; + updateStatus(`Select effect: ${EFFECT_SEQUENCE[state.effectSelectIndex].label}`); + audio.sfxUiBlip(); + return; + } + + if (code === 'Enter') { + const selected = EFFECT_SEQUENCE[state.effectSelectIndex]; + const effect = audio.setOutboundEffect(selected.id); + state.mode = 'normal'; + updateStatus(effect.label); + audio.sfxUiBlip(); + return; + } + + if (code === 'Escape') { + state.mode = 'normal'; + updateStatus('Cancelled.'); + audio.sfxUiCancel(); + } +} + function handleListModeInput(code: string, key: string): void { if (state.sortedPeerIds.length === 0) { state.mode = 'normal'; @@ -2164,6 +2208,8 @@ function setupInputHandlers(): void { handleNicknameModeInput(code, event.key); } else if (state.mode === 'chat') { handleChatModeInput(code, event.key); + } else if (state.mode === 'effectSelect') { + handleEffectSelectModeInput(code, event.key); } else if (state.mode === 'listUsers') { handleListModeInput(code, event.key); } else if (state.mode === 'listItems') { diff --git a/client/src/state/gameState.ts b/client/src/state/gameState.ts index e6ea903..fb55ea8 100644 --- a/client/src/state/gameState.ts +++ b/client/src/state/gameState.ts @@ -26,6 +26,7 @@ export type GameMode = | 'normal' | 'nickname' | 'chat' + | 'effectSelect' | 'listUsers' | 'listItems' | 'addItem' @@ -69,6 +70,7 @@ export type GameState = { editingPropertyKey: string | null; itemPropertyOptionValues: string[]; itemPropertyOptionIndex: number; + effectSelectIndex: number; addItemTypeIndex: number; isMuted: boolean; player: Player; @@ -98,6 +100,7 @@ export function createInitialState(): GameState { editingPropertyKey: null, itemPropertyOptionValues: [], itemPropertyOptionIndex: 0, + effectSelectIndex: 0, addItemTypeIndex: 0, isMuted: false, player: {