Add question-mark help viewer mode backed by help JSON

This commit is contained in:
Jage9
2026-02-21 16:55:41 -05:00
parent 35f837e96d
commit 433dd7c8d1
6 changed files with 77 additions and 2 deletions

View File

@@ -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."
></canvas>
<div id="instructions" class="hidden"></div>

View File

@@ -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" }
]

View File

@@ -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";

View File

@@ -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<void> {
@@ -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') {

View File

@@ -25,6 +25,7 @@ export type SelectionContext = 'pickup' | 'drop' | 'delete' | 'edit' | 'use' | '
export type GameMode =
| 'normal'
| 'helpView'
| 'nickname'
| 'chat'
| 'effectSelect'

View File

@@ -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