diff --git a/client/public/version.js b/client/public/version.js index 488c6a8..38f75bf 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 R155"; +window.CHGRID_WEB_VERSION = "2026.02.22 R156"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/input/editSession.ts b/client/src/input/editSession.ts index 9b8bc25..f875664 100644 --- a/client/src/input/editSession.ts +++ b/client/src/input/editSession.ts @@ -1,5 +1,11 @@ +/** + * High-level edit session intents derived from keyboard input. + */ export type EditSessionAction = 'submit' | 'cancel' | null; +/** + * Maps Enter/Escape to submit/cancel semantics for text-editing flows. + */ export function getEditSessionAction(code: string): EditSessionAction { if (code === 'Enter') return 'submit'; if (code === 'Escape') return 'cancel'; diff --git a/client/src/input/listController.ts b/client/src/input/listController.ts index 111e78a..e5d84c6 100644 --- a/client/src/input/listController.ts +++ b/client/src/input/listController.ts @@ -1,11 +1,17 @@ import { cycleIndex, findNextIndexByInitial } from './listNavigation'; +/** + * Normalized control result for list-like menus. + */ export type ListControlResult = | { type: 'move'; index: number; reason: 'arrow' | 'initial' } | { type: 'select' } | { type: 'cancel' } | { type: 'none' }; +/** + * Applies common list key handling (arrows, first-letter jump, enter, escape). + */ export function handleListControlKey( code: string, key: string, diff --git a/client/src/input/listNavigation.ts b/client/src/input/listNavigation.ts index 942d913..0cf7704 100644 --- a/client/src/input/listNavigation.ts +++ b/client/src/input/listNavigation.ts @@ -1,3 +1,6 @@ +/** + * Cycles an index through a fixed-length list. + */ export function cycleIndex(currentIndex: number, length: number, direction: 'next' | 'prev'): number { if (length <= 0) return 0; if (direction === 'next') { @@ -6,6 +9,9 @@ export function cycleIndex(currentIndex: number, length: number, direction: 'nex return (currentIndex - 1 + length) % length; } +/** + * Finds the next entry whose label starts with the pressed initial. + */ export function findNextIndexByInitial( entries: readonly T[], currentIndex: number, diff --git a/client/src/input/mainCommandRouter.ts b/client/src/input/mainCommandRouter.ts index 8946a0e..cbcd2a4 100644 --- a/client/src/input/mainCommandRouter.ts +++ b/client/src/input/mainCommandRouter.ts @@ -1,3 +1,6 @@ +/** + * Declarative command ids for the primary gameplay input mode. + */ export type MainModeCommand = | 'editNickname' | 'toggleMute' @@ -28,6 +31,9 @@ export type MainModeCommand = | 'chatLast' | 'escape'; +/** + * Maps raw key events to a semantic command for main mode handling. + */ export function resolveMainModeCommand(code: string, shiftKey: boolean): MainModeCommand | null { if (code === 'KeyN') return 'editNickname'; if (code === 'KeyM') return shiftKey ? 'toggleOutputMode' : 'toggleMute'; diff --git a/client/src/input/numeric.ts b/client/src/input/numeric.ts index 6427d3f..b0dec05 100644 --- a/client/src/input/numeric.ts +++ b/client/src/input/numeric.ts @@ -1,3 +1,6 @@ +/** + * Snaps a numeric value to the nearest step anchored to a minimum/base value. + */ export function snapNumberToStep(value: number, step: number, anchor = 0): number { if (!(step > 0) || !Number.isFinite(value) || !Number.isFinite(anchor)) { return value; @@ -7,6 +10,9 @@ export function snapNumberToStep(value: number, step: number, anchor = 0): numbe return Number(normalized.toFixed(decimals)); } +/** + * Formats stepped numeric values for speech/status without trailing decimal zeros. + */ export function formatSteppedNumber(value: number, step: number): string { const decimals = step >= 1 ? 0 : Math.min(6, Math.ceil(Math.abs(Math.log10(step))) + 1); if (decimals <= 0) { diff --git a/client/src/items/itemPropertyEditor.ts b/client/src/items/itemPropertyEditor.ts index 46eb8dd..4921440 100644 --- a/client/src/items/itemPropertyEditor.ts +++ b/client/src/items/itemPropertyEditor.ts @@ -3,6 +3,9 @@ import { getEditSessionAction } from '../input/editSession'; import { formatSteppedNumber, snapNumberToStep } from '../input/numeric'; import { type WorldItem } from '../state/gameState'; +/** + * Dependencies required to drive item property inspect/edit flows. + */ type EditorDeps = { state: { mode: string; @@ -40,6 +43,9 @@ type EditorDeps = { sfxUiCancel: () => void; }; +/** + * Creates item property mode handlers so main input dispatch can stay lean. + */ export function createItemPropertyEditor(deps: EditorDeps): { handleItemPropertiesModeInput: (code: string, key: string) => void; handleItemPropertyEditModeInput: (code: string, key: string, ctrlKey: boolean) => void; @@ -155,7 +161,7 @@ export function createItemPropertyEditor(deps: EditorDeps): { deps.sfxUiCancel(); return; } - if (code === 'ArrowUp' || code === 'ArrowDown') { + if (code === 'ArrowUp' || code === 'ArrowDown' || code === 'PageUp' || code === 'PageDown') { const metadata = deps.getItemPropertyMetadata(item.type, propertyKey); if (metadata?.valueType === 'number') { const range = metadata.range; @@ -171,7 +177,8 @@ export function createItemPropertyEditor(deps: EditorDeps): { : Number.isFinite(min) ? min : 0; - const delta = code === 'ArrowUp' ? step : -step; + const multiplier = code === 'PageUp' || code === 'PageDown' ? 10 : 1; + const delta = (code === 'ArrowUp' || code === 'PageUp' ? step : -step) * multiplier; const anchor = Number.isFinite(min) ? min : 0; const attempted = snapNumberToStep(currentValue + delta, step, anchor); let nextValue = attempted; diff --git a/client/src/main.ts b/client/src/main.ts index 1955e34..c50c578 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1681,10 +1681,11 @@ function handleChatModeInput(code: string, key: string, ctrlKey: boolean): void } function handleMicGainEditModeInput(code: string, key: string, ctrlKey: boolean): void { - if (code === 'ArrowUp' || code === 'ArrowDown') { + if (code === 'ArrowUp' || code === 'ArrowDown' || code === 'PageUp' || code === 'PageDown') { const raw = Number(state.nicknameInput.trim()); const base = Number.isFinite(raw) ? raw : audio.getOutboundInputGain(); - const delta = code === 'ArrowUp' ? MIC_INPUT_GAIN_STEP : -MIC_INPUT_GAIN_STEP; + const multiplier = code === 'PageUp' || code === 'PageDown' ? 10 : 1; + const delta = (code === 'ArrowUp' || code === 'PageUp' ? MIC_INPUT_GAIN_STEP : -MIC_INPUT_GAIN_STEP) * multiplier; const attempted = snapNumberToStep(base + delta, MIC_INPUT_GAIN_STEP, MIC_CALIBRATION_MIN_GAIN); const next = clampMicInputGain(attempted); state.nicknameInput = formatSteppedNumber(next, MIC_INPUT_GAIN_STEP); diff --git a/client/src/network/messageHandlers.ts b/client/src/network/messageHandlers.ts index 6e2d4f5..70691d5 100644 --- a/client/src/network/messageHandlers.ts +++ b/client/src/network/messageHandlers.ts @@ -1,6 +1,9 @@ import { type IncomingMessage } from './protocol'; import { type WorldItem } from '../state/gameState'; +/** + * Dependency contract for creating a message handler without hard-coupling to `main.ts`. + */ type MessageHandlerDeps = { getWorldGridSize: () => number; setWorldGridSize: (size: number) => void; @@ -63,6 +66,9 @@ type MessageHandlerDeps = { playIncomingItemUseSound: (url: string, x: number, y: number) => void; }; +/** + * Builds the websocket message dispatcher used by the signaling client. + */ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: IncomingMessage) => Promise { return async function onMessage(message: IncomingMessage): Promise { switch (message.type) { diff --git a/client/src/ui/domBindings.ts b/client/src/ui/domBindings.ts index 8514f70..0e8af6e 100644 --- a/client/src/ui/domBindings.ts +++ b/client/src/ui/domBindings.ts @@ -1,3 +1,6 @@ +/** + * UI elements used by binder setup. + */ type UiDom = { connectButton: HTMLButtonElement; preconnectNickname: HTMLInputElement; @@ -11,6 +14,9 @@ type UiDom = { canvas: HTMLCanvasElement; }; +/** + * Dependency contract for binding DOM event handlers. + */ type UiBindingsDeps = { dom: UiDom; sanitizeName: (value: string) => string; @@ -30,6 +36,9 @@ type UiBindingsDeps = { persistOnUnload: () => void; }; +/** + * Attaches UI listeners (connect/settings/device changes) and focus traps. + */ export function setupUiHandlers(deps: UiBindingsDeps): void { window.addEventListener('pagehide', deps.persistOnUnload); window.addEventListener('beforeunload', deps.persistOnUnload); diff --git a/server/app/items/radio.py b/server/app/items/radio.py index 22def72..c18faf9 100644 --- a/server/app/items/radio.py +++ b/server/app/items/radio.py @@ -61,7 +61,7 @@ PROPERTY_METADATA: dict[str, dict[str, object]] = { "facing": { "valueType": "number", "tooltip": "Facing direction in degrees used for directional emit.", - "range": {"min": 0, "max": 360, "step": 0.1}, + "range": {"min": 0, "max": 360, "step": 1}, }, "emitRange": { "valueType": "number", @@ -133,7 +133,7 @@ def validate_update(item: WorldItem, next_params: dict) -> dict: raise ValueError("facing must be a number between 0 and 360.") from exc if not (0 <= facing <= 360): raise ValueError("facing must be between 0 and 360.") - next_params["facing"] = round(facing, 1) + next_params["facing"] = int(round(facing)) try: emit_range = int(next_params.get("emitRange", item.params.get("emitRange", 20))) diff --git a/server/app/items/widget.py b/server/app/items/widget.py index e30e1ed..d2db81b 100644 --- a/server/app/items/widget.py +++ b/server/app/items/widget.py @@ -53,7 +53,7 @@ PROPERTY_METADATA: dict[str, dict[str, object]] = { "facing": { "valueType": "number", "tooltip": "Facing direction in degrees used when directional is on.", - "range": {"min": 0, "max": 360, "step": 0.1}, + "range": {"min": 0, "max": 360, "step": 1}, }, "emitRange": { "valueType": "number", @@ -128,7 +128,7 @@ def validate_update(item: WorldItem, next_params: dict) -> dict: raise ValueError("facing must be a number between 0 and 360.") from exc if not (0 <= facing <= 360): raise ValueError("facing must be between 0 and 360.") - next_params["facing"] = round(facing, 1) + next_params["facing"] = int(round(facing)) try: emit_range = int(next_params.get("emitRange", item.params.get("emitRange", 15))) diff --git a/server/tests/test_item_use_cooldown.py b/server/tests/test_item_use_cooldown.py index 813d267..d90a9dd 100644 --- a/server/tests/test_item_use_cooldown.py +++ b/server/tests/test_item_use_cooldown.py @@ -304,7 +304,7 @@ async def test_widget_update_and_use(monkeypatch: pytest.MonkeyPatch) -> None: ) assert send_payloads[-1].ok is True assert item.params.get("directional") is True - assert item.params.get("facing") == 123.4 + assert item.params.get("facing") == 123 assert item.params.get("emitRange") == 7 assert item.params.get("emitVolume") == 42 assert item.params.get("emitSoundSpeed") == 25