diff --git a/client/public/help.json b/client/public/help.json index 750c6f7..4b3f4c5 100644 --- a/client/public/help.json +++ b/client/public/help.json @@ -87,7 +87,7 @@ }, { "keys": "Piano mode", - "description": "When using a piano: 1-9 (and 0 for the 10th slot) changes instrument, -/= changes octave, ASDFGHJKL;' plays C major notes, WETYUOP] plays sharps, Z starts/stops recording, X plays recording, C stops playback, Escape exits" + "description": "When using a piano: press question mark for piano help. 1-9 (and 0 for the 10th slot) changes instrument, -/= changes octave, ASDFGHJKL;' plays C major notes, WETYUOP] plays sharps, Z starts/stops recording, X plays recording, C stops playback, Escape exits" } ] }, diff --git a/client/public/piano.json b/client/public/piano.json new file mode 100644 index 0000000..959c516 --- /dev/null +++ b/client/public/piano.json @@ -0,0 +1,41 @@ +{ + "sections": [ + { + "title": "Piano Mode", + "items": [ + { + "keys": "A S D F G H J K L ; '", + "description": "Play white keys (C major scale from C4)." + }, + { + "keys": "W E T Y U O P ]", + "description": "Play sharp keys." + }, + { + "keys": "1-9, 0", + "description": "Change instrument presets." + }, + { + "keys": "- / =", + "description": "Shift octave down or up." + }, + { + "keys": "Z", + "description": "Start or stop recording (up to 30 seconds)." + }, + { + "keys": "X", + "description": "Play the saved recording." + }, + { + "keys": "C", + "description": "Stop recording playback." + }, + { + "keys": "Escape", + "description": "Exit piano mode." + } + ] + } + ] +} diff --git a/client/public/version.js b/client/public/version.js index 6ac1468..bee9e73 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.22 R208"; +window.CHGRID_WEB_VERSION = "2026.02.22 R209"; // 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 48d7e5d..708cbfc 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -49,6 +49,7 @@ import { getDirection, getNearestItem, getNearestPeer, + type GameMode, type WorldItem, } from './state/gameState'; import { @@ -188,6 +189,18 @@ type HelpData = { sections: HelpSection[]; }; +/** Builds linearized help-view lines from sectioned help content. */ +function buildHelpLines(help: HelpData): string[] { + const lines: string[] = []; + for (const section of help.sections) { + lines.push(section.title); + for (const item of section.items) { + lines.push(`${item.keys}: ${item.description}`); + } + } + return lines; +} + const APP_VERSION = String(window.CHGRID_WEB_VERSION ?? '').trim(); const DISPLAY_TIME_ZONE = resolveDisplayTimeZone(); dom.appVersion.textContent = APP_VERSION @@ -230,8 +243,11 @@ let internalClipboardText = ''; let replaceTextOnNextType = false; let pendingEscapeDisconnect = false; let micGainLoopbackRestoreState: boolean | null = null; +let mainHelpViewerLines: string[] = []; +let pianoHelpViewerLines: string[] = []; let helpViewerLines: string[] = []; let helpViewerIndex = 0; +let helpViewerReturnMode: GameMode = 'normal'; let heartbeatTimerId: number | null = null; let heartbeatNextPingId = -1; let heartbeatAwaitingPong = false; @@ -311,6 +327,7 @@ loadAudioLayerState(); loadMicInputGain(); loadMasterVolume(); void loadHelp(); +void loadPianoHelp(); void loadChangelog(); /** Fetches a required DOM element and casts it to the requested element type. */ @@ -369,7 +386,7 @@ function setUpdatesExpanded(expanded: boolean): void { /** Renders help sections into the footer help container and builds linearized viewer lines. */ function renderHelp(help: HelpData): void { - const lines: string[] = []; + const lines = buildHelpLines(help); dom.instructions.innerHTML = ''; const heading = document.createElement('h2'); heading.textContent = 'Help'; @@ -386,9 +403,9 @@ function renderHelp(help: HelpData): void { line.appendChild(keys); line.append(` ${item.description}`); dom.instructions.appendChild(line); - lines.push(`${item.keys}: ${item.description}`); } } + mainHelpViewerLines = lines; helpViewerLines = lines; helpViewerIndex = 0; } @@ -410,6 +427,23 @@ async function loadHelp(): Promise { } } +/** Loads piano-mode help content from `piano.json` for in-mode help viewing. */ +async function loadPianoHelp(): Promise { + try { + const response = await fetch(withBase('piano.json'), { cache: 'no-store' }); + if (!response.ok) { + return; + } + const help = (await response.json()) as HelpData; + if (!Array.isArray(help.sections) || help.sections.length === 0) { + return; + } + pianoHelpViewerLines = buildHelpLines(help); + } catch { + // Keep piano help unavailable if loading fails. + } +} + /** Renders changelog sections into the collapsible updates panel. */ function renderChangelog(changelog: ChangelogData): void { dom.updatesPanel.innerHTML = ''; @@ -887,7 +921,7 @@ async function startPianoUseMode(itemId: string): Promise { activePianoMonophonicKey = null; state.mode = 'pianoUse'; await audio.ensureContext(); - updateStatus(`using ${item.title}, press escape to stop.`); + updateStatus(`using ${item.title}, press question mark for help.`); audio.sfxUiBlip(); } @@ -1112,12 +1146,14 @@ function stopRemotePianoNotesForSource(senderId: string, itemId: string): void { } /** Enters help-view mode and announces the first help line. */ -function openHelpViewer(): void { - if (helpViewerLines.length === 0) { +function openHelpViewer(lines: string[], returnMode: GameMode = 'normal'): void { + if (lines.length === 0) { updateStatus('Help unavailable.'); audio.sfxUiCancel(); return; } + helpViewerLines = lines; + helpViewerReturnMode = returnMode; state.mode = 'helpView'; helpViewerIndex = 0; updateStatus(helpViewerLines[helpViewerIndex]); @@ -2236,7 +2272,7 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void { return; } case 'openHelp': - openHelpViewer(); + openHelpViewer(mainHelpViewerLines); return; case 'openChat': state.mode = 'chat'; @@ -2305,7 +2341,7 @@ function handleHelpViewModeInput(code: string): void { return; } if (code === 'Escape') { - state.mode = 'normal'; + state.mode = helpViewerReturnMode; updateStatus('Closed help.'); audio.sfxUiCancel(); } @@ -2406,6 +2442,10 @@ function handlePianoUseModeInput(code: string): void { stopPianoUseMode(true); return; } + if (code === 'Slash') { + openHelpViewer(pianoHelpViewerLines, 'pianoUse'); + return; + } const itemId = activePianoItemId; if (!itemId) { state.mode = 'normal'; diff --git a/docs/controls.md b/docs/controls.md index 48ab30c..e99122c 100644 --- a/docs/controls.md +++ b/docs/controls.md @@ -80,6 +80,7 @@ Applies to effect select, user/item list modes, item selection, item property li - `A S D F G H J K L ; '`: Play white keys (C major from C4 upward) - `W E T Y U O P ]`: Play sharps - Multiple keys can be held/played at once +- `?`: Open piano-mode help viewer - `-` / `=`: Shift octave down/up - `Z`: Start/stop recording on this piano (max 30s) - `X`: Play back saved recording on this piano