Add TS JSDoc coverage and improve numeric step controls

This commit is contained in:
Jage9
2026-02-22 17:12:28 -05:00
parent 3e321df56c
commit 5f1f1022fc
13 changed files with 63 additions and 10 deletions

View File

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

View File

@@ -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<T>(
code: string,
key: string,

View File

@@ -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<T>(
entries: readonly T[],
currentIndex: number,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<void> {
return async function onMessage(message: IncomingMessage): Promise<void> {
switch (message.type) {

View File

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