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.
| Order | Phase | Guard | What happens | Exit condition |
|---|---|---|---|---|
| 1 | Post-fire pause | _postFirePause > 0 | Decrement timer. Enemy stands still — no fire, no charge. | Timer reaches 0 → fall through to cooldown next frame. |
| 2 | Burst | _burstRemaining > 0 | Decrement _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. |
| 3 | Charge-up | _fireCharging | Decrement _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. |
| 4 | Cooldown | _fireCharging === false and _burstRemaining === 0 and _postFirePause === 0 | Decrement 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:
| Constant | Value | Purpose |
|---|---|---|
DEFAULT_CHARGE_TIME | 1.4 s | Fallback when wep.chargeTime is undefined. |
ENEMY_BURST_COUNT | 12 | Projectiles fired per burst (shooter machine-gun). |
ENEMY_BURST_DELAY | 0.06 s | Gap between burst shots. |
SHOOTER_POST_FIRE_PAUSE | 2.5 s | Stand-still window after burst completes. |
ARROW_TRACK_FRAC | 0.8 | Fraction of charge during which the arrow re-aims toward the player. |
GLOBAL_MORTAR_COOLDOWN | 2.0 s | Maximum 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 id | Damage | Fire rate (Hz) | Cooldown (s) | Charge (s) | Special |
|---|---|---|---|---|---|
enemy_charger | 189 | 1.25 | 0.80 | 0.17 | Short-range shock-on-contact. |
enemy_shooter | 78 | 0.075 | 13.33 | 1.68 | Triggers 12-shot burst, then 2.5 s pause. |
enemy_mortar | 1152 | 0.2 | 5.00 | 1.12 | type: 'timed', blast radius 25, fuse 7.2 s, global 2 s cooldown. |
enemy_gunner | 3 | 0.4 | 2.50 | 0 | burstCount: 3, burstDelay: 0.15. No charge phase. |
enemy_field | 15 | 0 | n/a | 3.0 | City-set field weapon. |
enemy_sniper | 2 | 0 | n/a | 2.5 | City-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 file | Weapon id | Path taken |
|---|---|---|
data/enemies/charger.ts | enemy_charger | Charge → instant fire → cooldown. |
data/enemies/shooter.ts | enemy_shooter | Arrow telegraph charge → 12-shot burst → 2.5 s pause → cooldown. |
data/enemies/spitter.ts | enemy_shooter | Same as shooter. |
data/enemies/mortar.ts | enemy_mortar | Circle forecast charge → spawn timedStrikes entry (fuse 7.2 s) → global mortar cooldown. |
data/enemies/bombardier.ts | enemy_mortar | Same as mortar. |
data/enemies/gunner.ts | enemy_gunner | No charge; phase-4 cooldown loops, three-shot rifle burst via weapon burstCount. |
data/enemies/suppressor.ts | enemy_gunner | Same as gunner. |
data/enemies/lurker.ts | enemy_sniper | Long 2.5 s charge → single aimed shot. |
data/enemies/sniper.ts | enemy_sniper | Same as lurker. |
data/enemies/burner.ts | enemy_field | 3 s charge → field weapon. |
data/enemies/field.ts | enemy_field | Same 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.