diff --git a/client/index.html b/client/index.html index 32eece7..20e2c5b 100644 --- a/client/index.html +++ b/client/index.html @@ -29,7 +29,7 @@ height="600" tabindex="0" class="hidden" - aria-label="Chat Grid, press arrows to move." + aria-label="Chat Grid, press question mark for help." > diff --git a/client/public/help.json b/client/public/help.json index cb9afe3..595e86f 100644 --- a/client/public/help.json +++ b/client/public/help.json @@ -4,6 +4,7 @@ "title": "Movement", "items": [ { "keys": "Arrow Keys", "description": "Move" }, + { "keys": "Question Mark", "description": "Open help viewer" }, { "keys": "C", "description": "Speak coordinates" }, { "keys": "Escape", "description": "Disconnect/cancel" } ] diff --git a/client/public/version.js b/client/public/version.js index 642107e..c2f0443 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.21 R105"; +window.CHGRID_WEB_VERSION = "2026.02.21 R106"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/main.ts b/client/src/main.ts index 779c5d6..4e2dc0f 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -236,6 +236,8 @@ const itemEmitRuntime = new ItemEmitRuntime(audio, resolveIncomingSoundUrl); let internalClipboardText = ''; let replaceTextOnNextType = false; let pendingEscapeDisconnect = false; +let helpViewerLines: string[] = []; +let helpViewerIndex = 0; let audioLayers: AudioLayerState = { voice: true, item: true, @@ -313,6 +315,7 @@ function setUpdatesExpanded(expanded: boolean): void { } function renderHelp(help: HelpData): void { + const lines: string[] = []; dom.instructions.innerHTML = ''; const heading = document.createElement('h2'); heading.textContent = 'Help'; @@ -321,6 +324,7 @@ function renderHelp(help: HelpData): void { const sectionHeading = document.createElement('h3'); sectionHeading.textContent = section.title; dom.instructions.appendChild(sectionHeading); + lines.push(section.title); for (const item of section.items) { const line = document.createElement('p'); const keys = document.createElement('b'); @@ -328,8 +332,11 @@ function renderHelp(help: HelpData): void { line.appendChild(keys); line.append(` ${item.description}`); dom.instructions.appendChild(line); + lines.push(`${item.keys}: ${item.description}`); } } + helpViewerLines = lines; + helpViewerIndex = 0; } async function loadHelp(): Promise { @@ -581,6 +588,18 @@ function itemPropertyLabel(key: string): string { return key; } +function openHelpViewer(): void { + if (helpViewerLines.length === 0) { + updateStatus('Help unavailable.'); + audio.sfxUiCancel(); + return; + } + state.mode = 'helpView'; + helpViewerIndex = 0; + updateStatus(helpViewerLines[helpViewerIndex]); + audio.sfxUiBlip(); +} + function getItemsAtPosition(x: number, y: number): WorldItem[] { return Array.from(state.items.values()).filter((item) => !item.carrierId && item.x === x && item.y === y); } @@ -1533,6 +1552,11 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { return; } + if (code === 'Slash' && shiftKey) { + openHelpViewer(); + return; + } + if (code === 'Slash' && !shiftKey) { state.mode = 'chat'; state.nicknameInput = ''; @@ -1574,6 +1598,45 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { } } +function handleHelpViewModeInput(code: string): void { + if (helpViewerLines.length === 0) { + state.mode = 'normal'; + updateStatus('Help unavailable.'); + audio.sfxUiCancel(); + return; + } + + if (code === 'ArrowDown') { + helpViewerIndex = Math.min(helpViewerLines.length - 1, helpViewerIndex + 1); + updateStatus(helpViewerLines[helpViewerIndex]); + audio.sfxUiBlip(); + return; + } + if (code === 'ArrowUp') { + helpViewerIndex = Math.max(0, helpViewerIndex - 1); + updateStatus(helpViewerLines[helpViewerIndex]); + audio.sfxUiBlip(); + return; + } + if (code === 'Home') { + helpViewerIndex = 0; + updateStatus(helpViewerLines[helpViewerIndex]); + audio.sfxUiBlip(); + return; + } + if (code === 'End') { + helpViewerIndex = helpViewerLines.length - 1; + updateStatus(helpViewerLines[helpViewerIndex]); + audio.sfxUiBlip(); + return; + } + if (code === 'Escape') { + state.mode = 'normal'; + updateStatus('Closed help.'); + audio.sfxUiCancel(); + } +} + function handleChatModeInput(code: string, key: string, ctrlKey: boolean): void { if (code === 'Enter') { const message = state.nicknameInput.trim(); @@ -2220,6 +2283,8 @@ function setupInputHandlers(): void { handleChatModeInput(code, event.key, event.ctrlKey); } else if (state.mode === 'effectSelect') { handleEffectSelectModeInput(code, event.key); + } else if (state.mode === 'helpView') { + handleHelpViewModeInput(code); } 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 a7d3f36..bd770a5 100644 --- a/client/src/state/gameState.ts +++ b/client/src/state/gameState.ts @@ -25,6 +25,7 @@ export type SelectionContext = 'pickup' | 'drop' | 'delete' | 'edit' | 'use' | ' export type GameMode = | 'normal' + | 'helpView' | 'nickname' | 'chat' | 'effectSelect' diff --git a/docs/controls.md b/docs/controls.md index 707642b..b08e7be 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -6,6 +6,7 @@ This document is the authoritative keymap for the client. ### Movement - `Arrow Keys`: Move +- `?`: Open help viewer - `C`: Speak coordinates - `Escape`: Press once for disconnect prompt, press again to disconnect @@ -61,3 +62,10 @@ Applies to effect select, user/item list modes, item selection, item property li - `Enter`: Confirm selection - `Escape`: Exit/cancel - First-letter navigation: jump to next matching entry + +## Help Viewer Mode + +- `ArrowUp` / `ArrowDown`: Previous/next help line +- `Home` / `End`: First/last help line +- `Escape`: Exit help viewer +- No first-letter navigation in this mode