Gamepad Input
Input.pollGamepad() runs per frame. It calls navigator.getGamepads() and iterates the returned list, picking the first entry that is non-null and reports connected. Only that first pad is read — additional connected pads are ignored.
The left stick drives motion. axes[0] and axes[1] are sampled, and the raw magnitude mag = sqrt(lx*lx + ly*ly) is compared to a flat deadzone constant dz = 0.2. Below the deadzone, joystickMagnitude is forced to 0 and isThrusting is cleared.
Above the deadzone, the magnitude is linearly rescaled so that the deadzone edge maps to 0 and full stick deflection maps to 1:
scale = (mag - dz) / (1 - dz)
The direction is preserved by normalizing (lx, ly) by mag and multiplying by scale before atan2. The result is written to playerInput.joystickAngle and playerInput.joystickMagnitude, and isThrusting is set true.
Because the gamepad path sets joystickAngle / joystickMagnitude (not ship.targetAngle directly), it feeds the same bridge-style joystick channel that touch uses. ship.targetAngle is driven downstream by the bridge when joystickActive is true; mouse-based targetAngle override in Input.update() is skipped in that state.
Fire and menu buttons are stubbed — comments mark right-trigger / A-button as fire and reserve a slot for rising-edge menu detection, but no button reads are wired. Fire is handled by the weapon system; menu input is handled by the React UI.