What this is

Shared finite-state machine that drives every enemy that has a weaponId. Runs in enemyFire(e, dt, ship, world) inside engine/enemies/behaviors.ts. The machine is implemented as four guarded branches that the function checks in order each frame: post-fire pause, burst phase, charge-up phase, cooldown phase. Per-enemy state is stored as underscore-prefixed fields on the enemy entity (_postFirePause, _burstRemaining, _burstTimer, _burstAim, _fireCharging, _fireChargeT, _fireChargeDur, _fireChargeAim, fireTimer). Numeric timing comes from data/weapons/_enemy.ts (ENEMY_WEAPON_STATS) plus a handful of module-scoped constants.

The phases

Phases are evaluated top-down each tick. The first phase whose guard matches owns the frame and returns.

OrderPhaseGuardWhat happensExit condition
1Post-fire pause_postFirePause > 0Decrement timer. Enemy stands still — no fire, no charge.Timer reaches 0 → fall through to cooldown next frame.
2Burst_burstRemaining > 0Decrement _burstTimer. On tick-out, call fireBurstShot, decrement _burstRemaining, reset _burstTimer = ENEMY_BURST_DELAY. When _burstRemaining hits 0, set _postFirePause = SHOOTER_POST_FIRE_PAUSE.All burst shots fired.
3Charge-up_fireChargingDecrement _fireChargeT. For non-timed weapons, re-aim toward player during first ARROW_TRACK_FRAC of charge (last 20% is locked — dodge window). On timeout: mortars spawn a timedStrikes entry, projectile shooters set _burstRemaining = ENEMY_BURST_COUNT and mark the forecast as done (arrow shatters)._fireChargeT <= 0.
4Cooldown_fireCharging === false and _burstRemaining === 0 and _postFirePause === 0Decrement e.fireTimer. Mortars also gate on global _lastMortarTime (one mortar shot per GLOBAL_MORTAR_COOLDOWN across all mortars). When ready, set fireTimer = (1 / fireRate) / attackSpeedMult. If chargeTime > 0.05, begin charge: _fireCharging = true, _fireChargeT = chargeDur, seed _fireChargeAim, and call spawnForecastForEnemy.Charge starts (or weapon has no charge and the cooldown loops).

Constants in behaviors.ts:

ConstantValuePurpose
DEFAULT_CHARGE_TIME1.4 sFallback when wep.chargeTime is undefined.
ENEMY_BURST_COUNT12Projectiles fired per burst (shooter machine-gun).
ENEMY_BURST_DELAY0.06 sGap between burst shots.
SHOOTER_POST_FIRE_PAUSE2.5 sStand-still window after burst completes.
ARROW_TRACK_FRAC0.8Fraction of charge during which the arrow re-aims toward the player.
GLOBAL_MORTAR_COOLDOWN2.0 sMaximum one mortar shot across all mortars in this window.

Per-enemy timing

Pulled from ENEMY_WEAPON_STATS in data/weapons/_enemy.ts. chargeTime feeds phase 3; fireRate feeds the phase-4 cooldown reset (fireTimer = 1 / fireRate / attackSpeedMult). Mortars override type to 'timed', which skips the projectile burst and spawns a timedStrikes entry with a fuse.

Weapon idDamageFire rate (Hz)Cooldown (s)Charge (s)Special
enemy_charger1891.250.800.17Short-range shock-on-contact.
enemy_shooter780.07513.331.68Triggers 12-shot burst, then 2.5 s pause.
enemy_mortar11520.25.001.12type: 'timed', blast radius 25, fuse 7.2 s, global 2 s cooldown.
enemy_gunner30.42.500burstCount: 3, burstDelay: 0.15. No charge phase.
enemy_field150n/a3.0City-set field weapon.
enemy_sniper20n/a2.5City-set sniper.

Burst phase numerics are constants (ENEMY_BURST_COUNT, ENEMY_BURST_DELAY, SHOOTER_POST_FIRE_PAUSE), so all enemies that take the projectile-burst branch share the same 12 shots × 0.06 s + 2.5 s pause regardless of weapon id.

Which enemies use it

Any enemy whose data entry has a non-null weaponId runs the FSM each tick.

Enemy fileWeapon idPath taken
data/enemies/charger.tsenemy_chargerCharge → instant fire → cooldown.
data/enemies/shooter.tsenemy_shooterArrow telegraph charge → 12-shot burst → 2.5 s pause → cooldown.
data/enemies/spitter.tsenemy_shooterSame as shooter.
data/enemies/mortar.tsenemy_mortarCircle forecast charge → spawn timedStrikes entry (fuse 7.2 s) → global mortar cooldown.
data/enemies/bombardier.tsenemy_mortarSame as mortar.
data/enemies/gunner.tsenemy_gunnerNo charge; phase-4 cooldown loops, three-shot rifle burst via weapon burstCount.
data/enemies/suppressor.tsenemy_gunnerSame as gunner.
data/enemies/lurker.tsenemy_sniperLong 2.5 s charge → single aimed shot.
data/enemies/sniper.tsenemy_sniperSame as lurker.
data/enemies/burner.tsenemy_field3 s charge → field weapon.
data/enemies/field.tsenemy_fieldSame as burner.

Enemies with weaponId: null skip the FSM entirely: brute, orb, racer, sprinter, wisp, plus the charger sub-variant in data/enemies/index.ts that uses melee shock without a projectile weapon.