first commit
This commit is contained in:
142
docs/gaming/games/controller/src/gamepad/gamepad.md
Normal file
142
docs/gaming/games/controller/src/gamepad/gamepad.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# `gamepad` — `GamepadController`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Browser-tier device adapter that polls the Web Gamepad API and
|
||||
normalises button + axis state into the canonical 37-field
|
||||
`ControllerInput` shape exposed by `BaseController`. The adapter
|
||||
covers the W3C "standard mapping" layout (face buttons, bumpers,
|
||||
triggers, sticks, D-pad, system buttons) out of the box and exposes
|
||||
the button + axis maps so non-standard pads or custom layouts can be
|
||||
described per-instance.
|
||||
|
||||
The class never reads from `navigator` at module-load time. The
|
||||
`GamepadSource` is either supplied via `options.source` or resolved
|
||||
lazily from `globalThis.navigator.getGamepads()` when `update()`
|
||||
runs. Tests inject a deterministic source returning fixture pads and
|
||||
drive the adapter directly — no jsdom or `navigator` polyfill
|
||||
required.
|
||||
|
||||
## Features
|
||||
|
||||
- Standard W3C button mapping for all 17 indices, including analog
|
||||
triggers (`lt`/`rt`) and D-pad-to-digital mapping.
|
||||
- Standard axis mapping covering both digital halves
|
||||
(`right`/`left`/`up`/`down`) and centred analog representations
|
||||
(`lx`/`ly`/`rx`/`ry`).
|
||||
- Per-axis deadzone (defaults to `0.1`) applied before the magnitude
|
||||
is rescaled to the `[0, 255]` field range.
|
||||
- Per-instance `padIndex` so two adapters on the same source can
|
||||
drive split-screen co-op without further routing.
|
||||
- Connection diff — when the source returns `null`, `undefined`, or
|
||||
a pad with `connected: false`, the adapter zeroes the mapped
|
||||
fields exactly once and stops emitting until the pad reappears.
|
||||
- Validates every map entry against `CONTROLLER_INPUT_KEYS`; invalid
|
||||
entries throw `ControllerError(INVALID_GAMEPAD_MAP)` with the
|
||||
offending tuple in `context.entry`.
|
||||
- Inherits the full `BaseController` surface — `onInput`,
|
||||
`onAnyInput`, `setThreshold`, `getDirectionalVector`,
|
||||
telemetry instrumentation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { GamepadController } from "@suaveplan/controller/gamepad";
|
||||
|
||||
const gp = new GamepadController();
|
||||
gp.onInput("a", (_input, value) => {
|
||||
if (value === 255) console.log("A pressed");
|
||||
});
|
||||
|
||||
// pump every frame
|
||||
function tick() {
|
||||
gp.update();
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
tick();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Drive the adapter from a synthetic source with deterministic timing
|
||||
— no browser globals required:
|
||||
|
||||
```ts
|
||||
import { GamepadController } from "@suaveplan/controller/gamepad";
|
||||
import type { GamepadLike } from "@suaveplan/controller/gamepad";
|
||||
|
||||
let pad: GamepadLike | null = null;
|
||||
const gp = new GamepadController({
|
||||
source: () => [pad],
|
||||
deadzone: 0.2,
|
||||
padIndex: 0,
|
||||
axisMap: {
|
||||
0: { positive: "right", negative: "left", analog: "lx" },
|
||||
1: { positive: "down", negative: "up", analog: "ly" },
|
||||
},
|
||||
buttonMap: { 0: "a", 1: "b" },
|
||||
clock: () => performance.now(),
|
||||
pollRate: 120,
|
||||
});
|
||||
|
||||
pad = {
|
||||
buttons: [
|
||||
{ pressed: true, value: 1 },
|
||||
{ pressed: false, value: 0 },
|
||||
],
|
||||
axes: [0.75, -0.5],
|
||||
};
|
||||
gp.update();
|
||||
console.log(gp.getValue("a")); // 255
|
||||
console.log(gp.getValue("right")); // 191
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class GamepadController extends BaseController`
|
||||
- `constructor(options?: GamepadControllerOptions)`
|
||||
- `update(): void` — polls the source, diffs against last frame,
|
||||
pushes updates through `setState()`. Throws
|
||||
`ControllerError(GAMEPAD_API_UNAVAILABLE)` when no source is
|
||||
available. No-op after `destroy()`.
|
||||
- `getDeadzone(): number` — resolved deadzone.
|
||||
- `getPadIndex(): number` — bound pad index.
|
||||
- `getButtonMap(): GamepadButtonMap` — immutable button map.
|
||||
- `getAxisMap(): GamepadAxisMap` — immutable axis map.
|
||||
- `DEFAULT_GAMEPAD_BUTTON_MAP: GamepadButtonMap` — W3C standard
|
||||
mapping.
|
||||
- `DEFAULT_GAMEPAD_AXIS_MAP: GamepadAxisMap` — W3C standard axis
|
||||
layout (left + right sticks).
|
||||
- `interface GamepadControllerOptions extends BaseControllerOptions`
|
||||
— adds `source`, `buttonMap`, `axisMap`, `deadzone`, `padIndex`.
|
||||
- `interface GamepadLike` — structural subset of `Gamepad`
|
||||
(`buttons[i].pressed`, `buttons[i].value`, `axes[i]`,
|
||||
`connected`).
|
||||
- `type GamepadSource = () => readonly (GamepadLike | null | undefined)[]`.
|
||||
- `interface GamepadAxisBinding` — `positive`, `negative`, `analog`
|
||||
fields, all optional.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Button values are taken from `buttons[i].value` when present so
|
||||
analog triggers report the full `[0, 255]` range. Pads that omit
|
||||
`value` (older drivers, software emulators) fall back to a binary
|
||||
`pressed ? 255 : 0`.
|
||||
- Axis values are clamped to `[-1, 1]` before the deadzone test so
|
||||
drivers that report slightly over-range readings (common on dead
|
||||
or worn sticks) cannot push the field above `255`.
|
||||
- The disconnect zero-out is one-shot — guarded by a `wasConnected`
|
||||
flag — so a permanently absent pad does not flood downstream
|
||||
consumers with redundant `value=0` events every poll.
|
||||
- The Zod options schema is `.strict()` so unknown options are
|
||||
rejected at the boundary as `ControllerError(INVALID_OPTIONS)`;
|
||||
field-level rejections (e.g. `deadzone: -1`) flow through the
|
||||
same code with the Zod issues in `context.issues`.
|
||||
- `super.update()` re-applies the `pollRate` gate, so callers may
|
||||
invoke `update()` more often than the configured rate without
|
||||
paying repeated traversal cost — the source function is only
|
||||
invoked once per gated tick.
|
||||
Reference in New Issue
Block a user