Weapon Fire Pipeline
Every active gameplay frame, the bridge runs each equipped weapon through the same fixed pipeline. The pipeline is shared by auto and manual modes; manual mode just substitutes the aim source and gates entry on player input.
Per-frame entry point
The bridge’s weapons pass (bridge.ts, the diagBeginPass('weapons') block) drives the loop:
WeaponManager.updateCooldowns(ship, dt)— decrements every weapon’sfireTimertoward 0 and decays flame heat. This runs whether the ship is stalled or not, so cooldowns stay current and weapons are ready the instant a stall ends.- If
ship.stalledis true (burnout), clear every weapon’s_manualTriggerand skip all firing for the frame. Cooldowns still ticked above, so no recovery debt. ShipRecoil.resetAnticipation()— wipes the per-frame anticipation accumulator so each warming weapon writes its own squeeze contribution.- For each
weaponinship.weapons(and only while!ship.stalled): resolve its effective range, pick the fire mode branch, and callWeaponManager.fire(ship, world, weapon, dt, aimAngle, game, slotIndex).
The same dt is passed all the way through — fire-rate timing is deterministic regardless of frame length.
Mode branch (per weapon)
Each weapon carries fireMode: 'auto' | 'manual' (default auto). The bridge branches before calling fire.
Auto mode
- Read the weapon’s
targetModefrom its def (closest,furthest,flanking,high_hp,low_hp; defaultclosest). - Call
WeaponManager.getAutoAimAngle(ship, world, weaponRange, slotIndex, gameTime, targetMode). getAutoAimAnglequeries the spatial enemy grid (enemyGrid.query) withinrange + 30, falls back to the full enemy list on grid miss, skips warping/dying/frozen enemies, then scores each candidate by mode (edge distance, sideways angle, hp).- If no enemy is found,
aimAngleisundefined— but weapons flagged_alwaysForward(Barrier) substituteship.angleso they keep firing as defensive cover. - Pass
aimAngleintoWeaponManager.fire. If it’s stillundefined, the fire call short-circuits at the cooldown check (see below).
Manual mode
- Manual mode bypasses target search entirely. The aim angle is
ship.angle + (weapon.defaultAngle * DEG2RAD)— a fixed offset from ship facing baked into the weapon def. setManualFireFlag(true)is set before the fire call (and cleared after), which tags any bullets the call spawns with_manualFireso the homing behaviors can carve out manual shots.- Two sub-cases:
- Warmup in progress (
weapon._warmingUp): keep ticking it every frame with the current ship-facing aim. Never reuseweapon._aimAnglehere — that may hold a stale auto-aim from before the player switched to manual. - Trigger this frame (
weapon._manualTrigger): clear the trigger flag (no buffering — discarded if cooldown isn’t ready), and iffireTimer <= 0, callfireonce.
- Warmup in progress (
- After a successful manual fire (
weapon.fireTimerjumped up), record telemetry and emit theweapon_fireSig event.
Inside WeaponManager.fire
Once invoked, fire runs the same stages for every weapon regardless of mode:
- Tick active warmup. If
weapon._warmingUpis true, accumulate_warmupTimerbydt, recompute_warmupProgress, cache the currentaimAngleinto_aimAngle, and apply pre-fire anticipation (ShipRecoil.anticipate) scaled byshipPulseStrength. If the timer hasn’t reachedgetWarmupDuration(weapon), return immediately. If it has, clear warmup flags, set_preWarmFlash = 1.0, fall back to the cached aim if the live one is nowundefined, and bail out (no fire this frame) if there’s still no aim — exceptorbit_ringweapons, which fire centered on the player. - Cooldown / locked check. If not flashing and
weapon.fireTimer > 0, return — the weapon is locked. Otherwise checkaimAngle: if it’sundefinedand the weapon isn’torbit_ring, return (no target, stay dormant). - Start warmup or fire immediately. If
getWarmupDuration(weapon) > 0.05, set_warmingUp = true, seed_warmupArch/_warmupC1/_warmupC2from the def for VFX, and return — the actual fire happens once warmup completes. Otherwise skip straight to fire by setting_preWarmFlash = 1.0. - Resolve damage. Compute
lvl(curved) andrawLvl(integer). Multiply baseline damage by the level damage curve, horizontals (damage_<tag>+damage_all), per-weapondmgPerH, the early-level nerf, any per-weaponearlyLevelDamageBuff, plus the flat+1 per horizontal. Apply the artifact_empowerMult(decrementing_empowerShots) and, if the ship has thestarpowerexclusive state, multiply bySTAR_POWER_DAMAGE_MULT = 2.0. - Reset cooldown. Compute
finalFireRate = baseRate * (1 + ratePerH * hDmg) * fireRateNerf. Setweapon.cooldownMax = 1 / finalFireRate(withFALLBACK_COOLDOWN = 0.5when the rate is 0), apply per-shotfireRateJitterif defined, and assignweapon.fireTimer = weapon.cooldownMax. Crash on non-finite cooldowns — bad config beats a silent dead weapon. - Dispatch to behavior path. Branch on
def.behaviorfirst (tesla_line,shield_arc,arc_mortar/plasma_mortar,orbit,flame_stream,cone_beam_dot,burst_fire,flame_drop,orbit_ring,beam_decay,artillery_rain,phoenix_aura,carpet_bomber,quad_burst), thendef.collisionMode(beam_trace,chain_arc), and finally fall through to the defaultfireProjectilespath. Each dispatch spawns its own bullets/beams/zones with the resolved damage and aim. - Per-weapon echoes (e.g.
lgd_flak_riflecasing scatter) run after dispatch on the same fire call.
Stall, lock, and heat gates
- Stall (burnout). When
ship.stalledis true the bridge loop skips the entire per-weapon block, butupdateCooldownsstill runs first sofireTimerkeeps ticking down. Manual triggers are cleared so the player can’t queue a shot through the stall. - Lock.
weapon.fireTimer > 0is the only “locked” gate — both modes return immediately if the cooldown isn’t elapsed. Manual triggers in this state are discarded silently with no buffering. - No-target gate. Auto mode passes
undefinedwhen nothing is in range;fireexits at the cooldown check without consuming the cooldown._alwaysForwardweapons (Barrier) andorbit_ringcasts (Star Halo) bypass this gate. - Heat. Flame weapons accumulate
_flameHeatwhile firing;updateCooldownsdecays it back toward 0 once_flameLastFireTimeis more than 0.2 s old. Heat itself doesn’t gate the fire call — it’s read by the flame-stream fire path to modulate output. - Star Power. The exclusive
starpowerstate checked in step 4 doubles damage on every shot for its duration. It doesn’t change cadence — only the resolved damage.
Echo and delayed shots
After all weapons have fired this frame, the bridge separately advances tickDelayedShots(world, dt) to play out echo bullets queued by fireProjectiles (Echo Double / Echo Triple horizontals push delayed clones onto the _delayedShots list with their own staggered delays). These spawn on their own schedule and don’t re-enter the per-weapon pipeline.
Source
engine/weapons/weapons.ts—WeaponManager.fire,updateCooldowns,getAutoAimAngle,setManualFireFlag,tickDelayedShots, and every behavior-specificfire*path.engine/bridge.ts— the per-frame'weapons'pass that drives the pipeline.
Related
warmup.md— warmup duration sourcing and VFX.cooldown-commit.md— whenfireTimeris committed relative to dispatch.target-modes.md— scoring rules pertargetMode.manual-fire-mode.md— manual trigger contract and bullet tagging.fire-pipeline.md— companion overview of the broader fire system.