99 lines
5.6 KiB
Markdown
99 lines
5.6 KiB
Markdown
# enterprise-controller
|
|
|
|
**Package**: `@suaveplan/controller`
|
|
**Status**: PROPOSED
|
|
**Type**: new-package (migration from `../suaveplan/packages/universal/controller/`)
|
|
|
|
- **Design**: ./design.md
|
|
- **Spec delta**: ./specs/controller/spec.md
|
|
|
|
## Why
|
|
|
|
Genesis lacks a universal-tier input controller. The upstream `@suaveplan/controller` package
|
|
(67 files, ~5000 LoC) provides a unified `ControllerInput` shape across gamepad, keyboard, mouse,
|
|
touch, plus pattern recognition and input buffering. Games built on `@suaveplan/ecs` and the
|
|
forthcoming `@suaveplan/game-server` need a deterministic, injectable input layer that does not
|
|
read wall-clock time directly and can be exercised under a `FakeClock`.
|
|
|
|
The upstream code uses `Date.now()` throughout, mutates module-level state, and lacks structured
|
|
errors, Zod boundary validation, and OTel instrumentation. This migration re-implements the
|
|
public surface with genesis primitives (`Clock`/`IdGenerator` injection, `defineCodes`, OTel
|
|
counter + histogram, Zod schemas) while preserving the `ControllerInput` shape, the
|
|
`BaseController` lifecycle, the `ControllerMerger` semantics, and the device adapters.
|
|
|
|
## What changes
|
|
|
|
- ADDED: `BaseController` — abstract base with state, thresholds, deadzone/threshold logic,
|
|
pollRate gating, lifecycle, event emission, `Clock` injection.
|
|
- ADDED: `KeyboardController` — DOM keyboard adapter with configurable key→input mapping.
|
|
- ADDED: `GamepadController` — `navigator.getGamepads()` polling adapter with axis deadzone.
|
|
- ADDED: `ControllerMerger` — merges N child controllers into a single `ControllerInput`
|
|
state via max-value reduction with stable iteration order.
|
|
- ADDED: `InputEventEmitter` — typed event dispatcher with `onInput`/`onStateChange`/`onCombo`
|
|
and listener disposal.
|
|
- ADDED: `ControllerError` + `CONTROLLER_CODES` — stable error-code registry via `defineCodes`.
|
|
- ADDED: OTel `controller_inputs_total` counter (tags: `device`, `input`) and
|
|
`controller_pattern_match_duration_ms` histogram, no-op without exporter.
|
|
- ADDED: Zod-validated public input boundaries (options, snapshots, patterns).
|
|
- ADDED: Sibling `.md` (≥200 words, 6 sections) for every implementation file.
|
|
|
|
## Out of scope (Priority 2 — Deferred 2026-05-24)
|
|
|
|
Per the migration brief's explicit allowance ("ship the input-device adapters + controller
|
|
merger as Priority 1 and defer pattern-recognition + input-buffering"), the following upstream
|
|
modules are deferred to a follow-up change:
|
|
|
|
- `TouchController` — touch event adapter (Priority 2; requires DOM `Touch` polyfills in test).
|
|
- `MouseController` — 1296-line upstream module requires structural decomposition into
|
|
≤300-line genesis modules per CLAUDE.md §3; deferred to Priority 2.
|
|
- `InputBuffer` — circular buffer for combo sequences (Priority 2; pattern-recognition layer).
|
|
- `InputStateMachine` — pattern-recognition state machine (Priority 2).
|
|
- `ModalController` — input-mode router (Priority 2).
|
|
- Default `defaultPatterns` constant (hadoken / shoryuken / charge-shot / rapid-fire) — moves
|
|
to Priority 2 with the pattern engine.
|
|
- `demo/`, `test-utils/`, `e2e/` — non-shipping; demo + integration harnesses are downstream
|
|
concerns and not part of the published surface.
|
|
|
|
Justification for deferral (≤1 sentence per upstream §0a anti-patterns rule): every deferred
|
|
module depends on a co-shipped component that is itself deferred or on a structural
|
|
decomposition that requires its own design review — pattern recognition + input buffering
|
|
form one cluster (combo grammar + buffer + state machine) that should land atomically, and
|
|
the mouse rewrite needs its own 1296→multi-file split design.
|
|
|
|
## Source mapping
|
|
|
|
This change derives from `../suaveplan/packages/universal/controller/`. There is no upstream
|
|
`enterprise/controller.md` audit document; this section maps to upstream source files instead.
|
|
|
|
| Requirement | Source | Notes |
|
|
|---|---|---|
|
|
| ControllerInput shape | `src/types.ts` L1-L66 | verbatim 32-field shape |
|
|
| BaseController lifecycle | `src/base/base-controller.ts` L1-L386 | re-authored to inject `Clock` instead of `Date.now()`; threshold logic preserved |
|
|
| KeyboardController | `src/keyboard/keyboard-controller.ts` L1-L226 | re-authored; DOM event listeners + key map |
|
|
| GamepadController | `src/gamepad/gamepad-controller.ts` L1-L315 | re-authored; `navigator.getGamepads()` polling |
|
|
| ControllerMerger | `src/controller-merger.ts` L1-L298 | re-authored; max-value reduction preserved |
|
|
| InputEventEmitter | `src/input/input-event-emitter.ts` L1-L115 | re-authored; typed listener registry |
|
|
|
|
### Requirements with no source line
|
|
|
|
| Requirement | Justification |
|
|
|---|---|
|
|
| `defineCodes` error registry | Genesis convention — upstream threw plain `Error` |
|
|
| OTel instrumentation | Genesis convention — upstream had no observability |
|
|
| Zod boundary schemas | Genesis convention — upstream had no runtime boundary validation |
|
|
| `Clock` injection | Genesis convention — upstream read `Date.now()` directly |
|
|
|
|
## Verification approach
|
|
|
|
- `src/base/base-controller.test.ts` — lifecycle, threshold, deadzone, poll-rate gating.
|
|
- `src/keyboard/keyboard-controller.test.ts` — key→input mapping, focus loss cleanup.
|
|
- `src/gamepad/gamepad-controller.test.ts` — axis deadzone, multi-pad fanout.
|
|
- `src/controller-merger/controller-merger.test.ts` — max-value reduction, add/remove.
|
|
- `src/event-emitter/event-emitter.test.ts` — listener add/remove/dispose.
|
|
- `src/errors/errors.test.ts` — every code in `CONTROLLER_CODES` reachable.
|
|
- `src/observability/observability.test.ts` — counter + histogram emit under in-memory meter.
|
|
|
|
### Affected dependents
|
|
|
|
None — additive, new package.
|