first commit
This commit is contained in:
134
docs/gaming/games/controller/src/keyboard/keyboard.md
Normal file
134
docs/gaming/games/controller/src/keyboard/keyboard.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# `keyboard` — `KeyboardController`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Browser-tier device adapter that normalises DOM `keydown`/`keyup`
|
||||
events into the canonical 37-field `ControllerInput` shape exposed by
|
||||
`BaseController`. The adapter exists so games, UIs, and headless test
|
||||
harnesses can consume the same controller surface whether the user is
|
||||
pressing physical keys, dispatching synthetic events from a test
|
||||
double, or driving the adapter from a non-DOM emitter such as a
|
||||
custom virtual keyboard.
|
||||
|
||||
The class never touches a global at module load time. The target is
|
||||
either supplied via `options.target` or resolved lazily from
|
||||
`globalThis.window` when `attach()` runs. Tests inject an
|
||||
`EventTargetLike` double and drive the adapter directly — no jsdom or
|
||||
browser environment is required.
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable `keyMap` covering arrow keys, WASD, ZXCV face buttons,
|
||||
QERT triggers, and `Digit1`–`Digit4` function keys out of the box.
|
||||
- Validates every key-map entry against `CONTROLLER_INPUT_KEYS` at
|
||||
construction; invalid entries throw
|
||||
`ControllerError(INVALID_KEY_MAP)` with the offending tuple in
|
||||
`context.entry`.
|
||||
- `attach()` / `detach()` are idempotent and survive `destroy()`
|
||||
cleanly (no listeners leak, `attach()` after destroy is a no-op).
|
||||
- Repeats and double-presses are suppressed (`event.repeat === true`
|
||||
and already-pressed keys are ignored).
|
||||
- Optional `preventDefault: true` to suppress default browser
|
||||
behaviour (e.g. spacebar scrolling) without leaking that decision
|
||||
into the input pipeline.
|
||||
- Inherits the full `BaseController` surface — `onInput`,
|
||||
`onAnyInput`, `onCombo`, `setThreshold`, `getDirectionalVector`,
|
||||
`update()`, telemetry instrumentation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { KeyboardController } from "@suaveplan/controller/keyboard";
|
||||
|
||||
const kb = new KeyboardController();
|
||||
kb.onInput("up", (_input, value) => {
|
||||
console.log(value === 255 ? "pressed up" : "released up");
|
||||
});
|
||||
// later
|
||||
kb.destroy();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Drive the adapter from a synthetic emitter — no DOM required:
|
||||
|
||||
```ts
|
||||
import { KeyboardController } from "@suaveplan/controller/keyboard";
|
||||
import type { EventTargetLike } from "@suaveplan/controller/keyboard";
|
||||
|
||||
const listeners = new Map<string, Set<(e: unknown) => void>>();
|
||||
const target: EventTargetLike = {
|
||||
addEventListener(type, listener) {
|
||||
const bucket = listeners.get(type) ?? new Set();
|
||||
bucket.add(listener as never);
|
||||
listeners.set(type, bucket);
|
||||
},
|
||||
removeEventListener(type, listener) {
|
||||
listeners.get(type)?.delete(listener as never);
|
||||
},
|
||||
};
|
||||
|
||||
const kb = new KeyboardController({
|
||||
target,
|
||||
keyMap: { Space: "a", KeyJ: "b" },
|
||||
preventDefault: true,
|
||||
pollRate: 120,
|
||||
});
|
||||
|
||||
const dispatch = (type: string, event: unknown) => {
|
||||
for (const cb of listeners.get(type) ?? []) cb(event as never);
|
||||
};
|
||||
dispatch("keydown", { code: "Space" });
|
||||
console.log(kb.getValue("a")); // 255
|
||||
dispatch("keyup", { code: "Space" });
|
||||
kb.destroy();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class KeyboardController extends BaseController`
|
||||
- `constructor(options?: KeyboardControllerOptions)`
|
||||
- `attach(): void` — bind `keydown`/`keyup` to the resolved
|
||||
target. Throws `ControllerError(KEYBOARD_TARGET_MISSING)` when
|
||||
no target is available.
|
||||
- `detach(): void` — idempotent removal of listeners.
|
||||
- `getPressedKeys(): readonly string[]` — snapshot of physical
|
||||
keys currently held down (by `code` or `key` identifier).
|
||||
- `getKeyMap(): KeyboardKeyMap` — immutable key → input map.
|
||||
- `destroy(): void` — detaches listeners and clears state.
|
||||
- `DEFAULT_KEYBOARD_KEY_MAP: KeyboardKeyMap` — frozen default map
|
||||
used when `options.keyMap` is omitted.
|
||||
- `interface KeyboardControllerOptions extends BaseControllerOptions`
|
||||
— adds `target`, `keyMap`, `autoAttach`, `preventDefault`.
|
||||
- `interface EventTargetLike` — structural subset of `EventTarget`.
|
||||
- `interface KeyboardEventLike` — structural subset of
|
||||
`KeyboardEvent` (`code`, `key`, `repeat`, `preventDefault`).
|
||||
- `type KeyboardKeyMap = Readonly<Record<string, keyof ControllerInput>>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The adapter writes through `BaseController.setState({ [input]:
|
||||
value })` rather than reaching into `pendingStateUpdates`. This
|
||||
keeps the OTel counters (`controller_inputs_total`,
|
||||
`controller_state_changes_total`) and the `InputEventEmitter`
|
||||
firing on the same code path as direct programmatic updates.
|
||||
- Key identification consults `event.code` first, then `event.key`.
|
||||
Both must be strings; the adapter rejects events whose identifier
|
||||
is `undefined`. This matches the upstream behaviour and aligns
|
||||
with W3C UI Events.
|
||||
- Repeat suppression is double-guarded: the adapter ignores
|
||||
`event.repeat === true` and also short-circuits when the key is
|
||||
already tracked as pressed.
|
||||
- The Zod options schema covers every public option; rejections are
|
||||
surfaced as `ControllerError(INVALID_OPTIONS)` with the Zod issues
|
||||
in `context.issues`. The schema is `.strict()` so unknown keys are
|
||||
rejected at the boundary — typos surface as construction errors
|
||||
rather than silent ignores.
|
||||
- `setState` accepts a partial `ControllerInput`, so the adapter
|
||||
computes the field per event rather than buffering. This keeps the
|
||||
per-event latency below the `pollRate` gate that `BaseController`
|
||||
applies to `update()`.
|
||||
Reference in New Issue
Block a user