# Flight controller input

The flight scene accepts the shell's normalized Xbox-style gamepad snapshot through `view.handleGamepad(pad, realDt)`. The shell polls once per animation frame, including while animation is paused. Stored flight axes are consumed by the next fixed update; direct camera changes invalidate the next render. Inactive frames deliver a neutral snapshot. Hardware polling, deadzones, arming, Menu/pause, and View/help remain owned by the shell.

| Input | Flight | Landscape inspection |
| --- | --- | --- |
| Left stick | Pitch and roll | Pan in camera right/up directions |
| Right stick | Look around cockpit or chase view | Orbit |
| LB / RB | Rudder left / right | No aircraft input |
| RT / LT | Increase / decrease throttle | Zoom in / out |
| A | Switch cockpit / chase | Resume the previous flight view |
| X | Toggle landing gear | No aircraft input |
| Y | Toggle guided / manual | No aircraft input |
| B or right-stick click | Center flight look | Flight look is reset for later use |
| D-pad up / down | Retract / extend flaps by 0.5 | No aircraft input |
| D-pad left / right | Toggle navigation / landing lights | No aircraft input |
| Left-stick click | Level wings and enter manual mode | No aircraft input |

Default pitch is conventional aircraft control: pull the left stick down to raise the nose. `settings.invertFlightPitch = true` reverses that axis. `settings.gamepadSensitivity` defaults to 1 and adjusts stick and view response; it is bounded to 0.25–3. Trigger throttle rate is independent of stick sensitivity, at 0.32 of full range per second. Elapsed time is capped at 0.1 seconds per input frame to avoid jumps after stalls.

Pitch, roll, rudder, or net throttle input takes control from the guided tour automatically. Camera movement and gear/light/flap buttons do not. If a guided toggle and active flight-axis input arrive together, manual input takes precedence. `view.setTour(value)` and controller transitions report state changes through `settings.onFlightTour(value)`. `view.tourEnabled` exposes the current state. Aircraft button/throttle changes notify `settings.onAircraftControl(name, value)`.

Paused controller frames can change views and camera look, including inspection pan/orbit/zoom. They cannot change aircraft controls, guided state, or body orientation. Flight-axis state clears on pause and on every disconnected, unsupported, unarmed, or otherwise inactive controller snapshot. The caller must send its neutral snapshot on focus loss/disconnection; there is no polling timer inside the scene. No paused button actions are queued for resumption.

Gamepad button actions use `pressed` edges, so held buttons do not repeat. Existing keyboard and mouse controls remain available. Keyboard and gamepad flight axes are added and clamped before angular rates are applied; opposing commands cancel. Mouse cockpit look and controller look share the same yaw/pitch limits. The existing keyboard V/G toggles now also ignore keyboard-repeat events.

The pure mapping and combination functions live in `shell/lib/flight-controls.js`. Focused Node tests in `tests/flight-controls.test.mjs` cover body-coordinate signs, inverted pitch, frame-rate-independent throttle, pause/inspection gating, neutral/disconnect states, held buttons, sensitivity, and combined-axis limits. These tests do not claim verification with a physical controller.
