What this is
The manual-fire override is a per-weapon mode that takes a weapon out of the auto-aim resolver loop and instead fires it only when the player triggers the slot. Each of the four weapon slots can be set independently to auto (default) or manual from the in-game settings cog; the choice is persisted to localStorage under the key ss_weapon_modes and reapplied to weapons that are picked up later in the run.
When manual mode is active for a slot, two coordinated things happen on the next fire: the bridge bypasses the auto-aim resolver and computes a fixed aim angle from ship facing alone, and a module-level flag is raised around the fire call so the spawn pipeline can tag every bullet produced by that fire with a _manualFire marker. Downstream behaviors then read that marker and disable their normal target-seeking logic.
| Field | Value | Where it lives |
|---|---|---|
| Per-slot mode | 'auto' or 'manual' | weapon.fireMode |
| Player trigger flag | Per-weapon boolean, cleared every frame | weapon._manualTrigger |
| Module-level fire flag | Set true around manual fire call, cleared after | _nextFireIsManual in engine/weapons/weapons.ts |
| Per-bullet marker | Stamped at spawn from the module flag | bullet._manualFire |
| Bridge entry point | Sets _manualTrigger = true on the slot’s weapon | mission.fireWeapon(slotIndex) |
| Persistence key | localStorage | ss_weapon_modes |
| Default slot mode | Auto | { fireMode: 'auto' } (all four slots) |
The player trigger is non-buffering: if the cooldown is not ready when the player presses fire, the trigger flag is cleared on that frame and the press is silently discarded. The next press is only honored after the cooldown has cleared.
How it overrides auto-fire
The override happens at three points in the fire pipeline, all gated on the manual flag.
| Stage | Auto-mode behavior | Manual-mode behavior |
|---|---|---|
| Target acquisition | Bridge calls WeaponManager.getAutoAimAngle and routes the resulting angle into the fire call. If no enemy is found, the fire aborts | Bridge skips the resolver. Aim angle is computed as ship.angle + (defaultAngle * pi / 180) and the fire call runs even if no enemy exists |
| Fire dispatch | Cooldown timer drives the call; the call happens every tick the timer is ready and a target exists | Call only runs on frames where _manualTrigger is set and the cooldown timer is at zero. Warmup frames continue ticking once a manual fire has begun a warmup |
| Bullet spawn tag | _manualFire field on each spawned bullet is set to false | Bridge wraps the fire call with setManualFireFlag(true) / setManualFireFlag(false). spawnBullet stamps bullet._manualFire = _nextFireIsManual onto every bullet produced |
| Spawn-time homing scrub | Bullet keeps its declared homingStrength | If homingStrength > 0 and the module flag is set, spawn code zeroes the bullet’s homingStrength outright (belt-and-suspenders alongside the _manualFire tag) |
Once a bullet carries _manualFire = true, every behavior that would normally steer or re-acquire is suppressed.
| Behavior | Auto-mode action | Manual-mode action |
|---|---|---|
Homing steering (homing behavior in bullets.ts) | Per-tick steer toward closest enemy by homingStrength rad/sec | Early-out at top of update — bullet flies straight in its fired direction for its entire lifetime |
| Manual-fire homing ramp (when the spawn-time scrub did not zero out homing) | homingMult = 1 over the full lifetime | homingMult = 0 at launch, ramps linearly to 1 at 50% of maxLifetime, then stays at 1 |
Burst-fire sub-shot targeting (burst_fire) | Lazily acquires and re-acquires a _burstTarget between sub-shots, aims each sub-shot at the lock | Skips target acquisition; every sub-shot uses the original _burstAimAngle from the cast |
Chain-arc first-hit search (chain_arc in collision-resolver.ts) | Picks the closest enemy inside firstTargetRange from the ship | Same closest search, but candidates are filtered to a 90° cone (cosine ≥ cos(pi/4)) centered on the arc’s aim angle. Enemies outside the cone are skipped |
Arc-mortar target prediction (fireArcMortar) | Scans world.enemies with the weapon’s target mode and leads the chosen enemy’s velocity over arcTime | Skips the scan entirely. Landing point is pinned to ship + cos/sin(aimAngle) * min(acquireRange * 0.8, 200) |
The composite effect is: no auto-target lock, no per-shot re-aim during travel, no target re-acquisition between burst sub-shots, and a fixed aim angle that comes from ship facing plus the weapon’s spec-defined offset.
Which weapons honor it
Manual mode is a property of the weapon slot, not of the weapon definition — any weapon placed in a manual-mode slot will receive the _manualFire tag on its bullets and will skip the resolver. What differs is how each weapon’s behavior responds to that tag.
| Weapon family | Effect of manual mode |
|---|---|
Standard projectile weapons (no homing behavior, no burst_fire, no chain_arc) | Aim direction switches from auto-aim to ship facing + defaultAngle. Projectile travel is unchanged |
Homing projectiles (homing behavior, e.g. missiles) | Bullets fly straight in the fired direction for their entire lifetime — the early-out in the homing update suppresses all steering |
Burst weapons (burst_fire, e.g. Revolver) | Sub-shots no longer track a locked target — every sub-shot in the burst uses the cast’s original aim angle |
Chain-arc weapons (chain_arc) | First-hit search is constrained to a 90° forward cone around the manual aim angle instead of picking the global nearest enemy |
Arc mortars (fireArcMortar) | Landing point is fixed at ship + aim direction * min(acquireRange * 0.8, 200), no lead prediction |
Rear-facing weapons (defaultAngle = 180, e.g. mines) | Manual aim angle becomes ship.angle + pi, so the weapon fires behind the ship rather than at the nearest auto-aim target |
| Always-forward defensive weapons (e.g. Barrier) | Already fire on ship facing in auto mode. Manual mode adds the per-trigger gating but does not change aim direction |
The aim-angle offset is read from weapon.defaultAngle (in degrees, copied from the weapon spec’s defaultAngle at activation time, defaulting to 0). A weapon with no declared defaultAngle fires straight ahead in manual mode regardless of where the auto-aim resolver would have aimed.
UX context
Manual mode is exposed per-slot through the in-game settings cog and persisted across sessions.
| Surface | Behavior |
|---|---|
| Slot mode storage | localStorage[ss_weapon_modes] — array of four { fireMode } records |
| Hot-swap | When a new weapon is picked up into a slot whose mode is unset, the saved slot mode is applied on the next tick |
| Mobile trigger | Tap on the on-screen weapon slot icon — hitTestWeaponSlot resolves the slot index and calls mission.fireWeapon(slotIndex) if that slot is in manual mode. Taps on auto-mode slots are ignored and fall through to ship movement |
| Desktop trigger | Keyboard keys 1 / 2 / 3 / 4 map to slots 0–3 and call mission.fireWeapon(slotIndex). The same keys are reused for reward-card selection when the reward screen is active — that consumer takes priority |
| Trigger buffering | None. _manualTrigger is cleared every frame regardless of whether the fire was eligible |
| Stall interaction | When the ship is stalled (heat overload), the per-frame loop clears _manualTrigger on every weapon so queued triggers do not fire on recovery |
| Auto-mode interaction | Auto-mode slots run their normal cooldown-driven fire path each tick, independent of the manual triggers on other slots. Mode is per-slot, not per-ship |