Touch / Mobile Controls
On mobile, the ship is driven by a virtual joystick anchored to wherever the player first puts their finger down. There is no fixed on-screen stick — the joystick origin recenters on every touchstart, so the player can place their thumb anywhere comfortable on the canvas.
Recenter on touchstart
When a touchstart fires and playerInput.isMobile is true, the touch’s clientX / clientY is recorded as touchStartX / touchStartY and joystickActive flips on. That point becomes the joystick origin for the duration of the touch. Lifting the finger (touchend) clears joystickActive and zeroes joystickMagnitude.
This means there is no concept of “drift” or “needing to find the stick” — every fresh touch resets the origin under the thumb.
Magnitude saturates at 60 px
During touchmove, the engine computes dx, dy from the touch origin and derives:
joystickAngle = atan2(dy, dx)— direction from origin to current finger positionjoystickMagnitude = min(1, hypot(dx, dy) / 60)— normalized 0..1, saturating at 60 px radius
So 60 px of finger travel from the origin produces full magnitude (1.0). Pulling further does nothing extra — the stick is already maxed. This keeps the usable joystick area thumb-sized rather than screen-sized.
Thrust threshold: 10 px
isThrusting is set to hypot(dx, dy) > 10. Below 10 px of travel, the ship coasts; above 10 px, thrusters fire. This gives a small dead zone around the origin so micro-movements don’t cause unintended thrust pulses.
Bridge has authority while joystick is active
The on-screen React joystick (rendered by the metagame shell) calls bridge.setInput() to drive ship.targetAngle directly. While playerInput.joystickActive is true, Input.update() short-circuits — it does not override targetAngle from raw DOM mouse coordinates.
The contract:
- React on-screen joystick owns input on mobile. It sets
joystickActive = trueand writesship.targetAnglevia the bridge. input.tsDOM mouse path is the fallback for desktop. It only runs whenjoystickActiveis false andisMobileis false.
This split means the React UI layer can render a styled joystick (with its own visual feedback) and the engine’s raw touch handling sits behind it as a backstop. The two never fight for targetAngle because the engine yields whenever the bridge has claimed the input.
See also
engine/input.ts— full input system (mouse, touch, keyboard, gamepad)engine/bridge.ts— React-to-engine input handoffship.targetAngle— what the joystick actually steers