// gamepadShim.ts — Mega 4: the W3C Gamepad API shim. Every JOINED seat appears inside the GAME FRAME // as a standard `Gamepad` (`navigator.getGamepads()` + `gamepadconnected` / `gamepaddisconnected`), // so any web game that already supports gamepads works on the couch unchanged. // // STANDALONE (like shortplay-sdk.ts / shortplay-players.ts): imports only the frozen players contract // types. It runs INSIDE the sandboxed game frame; the SDK constructs it when the parent says the game // opted in (manifest `inputs` ∋ `gamepad`, and NOT `managesPlayers` — those games use `onInput`). // // ── Mapping (W3C "standard" layout) ───────────────────────────────────────────────────────────── // phone retro pad : A=0 B=1 Select=8 Start=9 d-pad up/down/left/right=12/13/14/15; left stick // axes[0..1] (twin-stick adds axes[2..3]). Sent as `button` / `axes` on the event // (Mega 2 writes them); when ABSENT we map `dpadDirection` / `token` / `code`. // TV remote seat : OK(select)=0 Back(Escape/Backspace)=1 arrows=12..15. // Seat index : P1→0 … P4→3. The index IS the slot, so it is stable across a drop + rejoin. // // ── Drop ──────────────────────────────────────────────────────────────────────────────────────── // A seat leaving `joined` (held or empty) first RELEASES every pressed button + zeroes the axes // (the cancel), THEN fires `gamepaddisconnected` — so a game never sees a disconnected pad still // holding a button. The seat's index is reserved while held; rejoin re-fires `gamepadconnected`. // // ── Timing (the jitter buffer) ────────────────────────────────────────────────────────────────── // Each edge carries the DEVICE clock `t` (+ `heldMs` on release). Wi-Fi jitter (30-100 ms per edge) // lives in arrival time, never in `t`. We replay every edge at `t + minOffset + jitterBufferMs`, // where minOffset = min(arrival - t) seen for that seat (the best-case path), so the down→up spacing // is the phone's, shifted by a constant. An edge that still arrives later than its slot (jitter > // buffer) applies at once, and a RELEASE is additionally held to `pressApplied + max(heldMs | end.t - // start.t, GAMEPAD_MIN_PRESS_MS)` so a late press can never shorten the press: a 150 ms tap stays 150 ms. // LATENCY COST: a constant `jitterBufferMs` (default 100 ms) added to every press/release vs the // best-case network path. It absorbs up to that much per-edge jitter. A frame with no device clock // (an older controller) is applied on arrival, unbuffered. Slow clock drift between devices is not // modelled (a min-offset tracker self-corrects toward the lowest-delay path only). // // ── SELF-CONTAINED (Mega 6) — a HARD REQUIREMENT, same as controllerInputDispatch.ts ─────────── // This module is consumed TWO ways: // (1) imported normally by the child SDK (shortplay-sdk.ts); // (2) `.toString()`'d into the literal inline