Back to Pursuit PIT
SOURCE / PINNED RELEASE

Made of little things.

Pursuit PIT

Release
565f4981bdc3…
Author-recorded commit
197afdbed44b…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-951414e7fd7

Archive hash verified: 3b0485589942fd5c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

docs/gamepad-plan.md
# Gamepad support plan

Status: implemented for standard-mapped controllers; see gamepad-verification.md
for evidence and remaining manual coverage.

## Feature brief

Add optional, single-player controller input to Pursuit PIT, including every
essential in-game menu. Preserve keyboard, touch, the driving model and current
art direction. Target controllers reported by the browser as `mapping: 'standard'`.

Use the native browser Gamepad API through the supplied
`docs/examples/gamepad.ts` helper, following `docs/napplet-controllers.md`.
This feature adds no NAP domain, SDK call, network request, storage requirement
or manifest requirement. Do not introduce a `gamepad` requires entry or private
wire protocol. Browser availability and host policy can still prevent access;
keyboard and touch remain available.

## Proposed bindings

Use positional names in help so Xbox and PlayStation lettering is not assumed.

| Context | Control | Action |
| --- | --- | --- |
| Driving | Left stick horizontal | Analog steering |
| Driving | D-pad left/right | Full steering alternative |
| Driving | Right trigger | Analog accelerator |
| Driving | Left trigger | Analog brake/reverse, matching current simulation |
| Driving | Menu/Start | Pause |
| Driving | South face button | Wipers |
| Driving | West face button | Siren; existing unavailable feedback in Getaway |
| Driving | North face button | Radio toggle / Getaway stereo cycle |
| Driving | View/Select | Sound mute toggle |
| Menus | D-pad or left stick | Navigate |
| Menus | South face button | Confirm focused item |
| Menus | East face button | Back; resume from pause |
| Paused | Menu/Start | Resume |
| Garage | Up/down | Select option row |
| Garage | Left/right | Change selected option |
| Garage | Shoulder buttons | Switch police/getaway car |
| Garage | East face button or Menu/Start | Done |
| Replay | South face button | Skip after existing one-second guard |

Restart and mode switching use existing menu buttons, avoiding accidental
destructive actions during driving. Garage South confirms a focused control
when applicable; row editing always remains available through directions.

## Input and lifecycle rules

- Poll once per animation frame, including menus. Feed continuous analog values
  into all physics steps for that frame; consume button edges only once.
- Start with the helper's rescaled 0.18 stick dead zone. Preserve analog trigger
  ranges. Evaluate steering feel before changing physics or adding extra smoothing.
- Keep keyboard/touch and controller values separate. Combine each action by
  strongest magnitude; cancel equal opposing steering values. Combine accelerator
  sources by maximum and brake sources by maximum, then subtract for throttle.
- The first standard controller making a fresh button press becomes active.
  Other slots cannot drive or activate menus simultaneously. Show a brief join hint.
- On active-controller disconnect/replacement, neutralize controller input and
  pause driving. Require a fresh button press to claim a controller again, then
  explicit resume. Keyboard/touch can resume without a controller.
- Blur/hidden state clears held keyboard/touch input and controller actions and
  pauses driving. Focus return never resumes automatically.
- Entering a menu, starting a run, resuming or finishing a replay suppresses
  carried button edges. Require neutral driving controls before applying controller
  movement after start/resume, avoiding an already-held trigger launching the car.
- Menu directions act immediately, then repeat after 350 ms at 120 ms intervals.
  Use a separate navigation threshold (initially 0.5), return-to-neutral detection
  and one dominant direction to avoid stick drift and diagonal double movement.
- Route actions by current screen: a confirm cannot also activate a newly opened
  screen, and garage directions cannot leak into driving.
- Unsupported/nonstandard controllers receive an explanatory help message;
  arbitrary raw layouts are not automatically assigned standard bindings.
- Dispose helper listeners on teardown. No controller identifiers are persisted.

## Interface and form factor

Preserve the existing low-poly driving scene, dashboard, typography and app-owned
palette. Extend the existing controls help with the binding table and a compact
status: press a controller button, connected, disconnected, unsupported mapping,
or input unavailable. Do not display raw device IDs. Clearly distinguish blocked
API access from having no connected device.

Use existing focus styling for menu navigation and visible current-row feedback
in the garage. Show controller hints after controller activity, while leaving
touch controls reachable. No new minimum frame size. Keep hints compact/collapsible
at 200×160 and 320×560; inspect 900×600, full screen and touch portrait/landscape.
Keep colors intact in light/dark hosts and without the optional theme domain.
Audio may still need a pointer/key activation in some browsers; retain existing
activation paths and verify this with physical hardware.

## Implementation sequence

1. Add an app-owned controller adapter under `src/` using the supplied helper.
   Own bindings, active-slot selection, normalized driving actions, navigation
   repeat, status and transition resets there. Keep injected `read`/`active`
   seams for deterministic tests.
2. Extract shared semantic actions from keyboard handlers in `src/main.ts`.
   Route keyboard and controller actions to the same pause, sound, siren, radio
   and wiper functions without synthesizing keyboard events.
3. Merge analog input in `readInput()` and poll before the existing fixed-step
   simulation. Implement disconnect, focus and neutral-before-resume behavior.
4. Add screen-scoped menu navigation and semantic garage methods in
   `src/garage-menu.ts`; reuse them from keyboard and controller paths.
5. Update existing help in `index.html` and minimal focus/status CSS after reading
   napplet-ui. Update advertised description only once support is implemented.
6. Add meaningful input regression tests to the normal verification workflow,
   then complete hosted and hardware checks below.

## Acceptance and evidence

Automated tests cover dead zone/drift, proportional triggers/steering, mixed input
and opposing sources, one edge across multiple physics steps, navigation repeat,
held confirm across screens, focus/visibility loss, disconnect/replacement,
neutral-before-resume, missing/blocked API, nonstandard mappings and distinct slots.

Run `pnpm type-check`, `pnpm build`, `pnpm test:guidance` and
`pnpm test:conformance`; report skipped conformance evidence separately. Run input
regressions through `pnpm verify` as well.

Use `soyli dev` and its printed URL. Verify iframe load, injected-domain status
and a real user-triggered existing SDK operation. Exercise all three driving
modes, time selection, garage edits, pause/resume, retry, return to title and
replay skip. Check missing optional domains and all frame/theme cases above.
Stop the owned preview afterward. Check the deployed shell when available.

Record actual controller model, USB/Bluetooth connection, OS and browser for
physical tests. Synthetic snapshots and the workshop tester are separate evidence
and cannot establish physical device compatibility. Explicitly report any absent
hardware or deployed-shell coverage.

## Deferred

Custom mapping UI and saved profiles, adjustable dead-zone settings, rumble,
wheel/pedal-specific layouts, local multiplayer and controller navigation of the
surrounding host. These are outside this first implementation.