enemy-status.ts

PURPOSE

Lightweight per-enemy status effect system. Manages three status types — stunned, shredded, burning — with stack counts, duration timers, and tick-driven decay. Storage is lazily allocated on each enemy as e._statuses (a plain Record, not a Map, for mobile performance).

OWNS

  • applyStatus(e, type, duration, value, maxStacks) — applies or refreshes a status entry on an enemy, increments stacks up to cap, refreshes timer to the max of existing and incoming duration.
  • hasStatus(e, type) — boolean check for whether an enemy has an active entry of a given type with timer > 0.
  • getStacks(e, type) — returns active stack count, or 0 if no entry or timer expired.
  • getShredMult(e) — returns the damage multiplier from shredded stacks; 1 + stacks × value, or 1.0 if no shred.
  • tickStatuses(enemies, dt, game, ship, world) — per-frame decay loop across all enemies; decrements timers, applies burning DPS, clears expired entries.
  • clearStatuses(e) — wipes e._statuses (used on enemy death/despawn).
  • Lazy allocation of the e._statuses record on first applyStatus call.
  • Maintenance of the legacy e._stunTimer field for backward compatibility with existing behavior handlers.

READS FROM

  • EnemyStatusEntry type from ./types.
  • GameState, ShipState, WorldState types from ../core/types.
  • Each enemy’s e._statuses record, e.hp, and e._stunTimer.
  • game.stats.damageDealt for burn damage accounting.

PUSHES TO

  • e._statuses[type] — writes new entries, mutates stacks, timer, value.
  • e._stunTimer — sets legacy stun field when applying or expiring a stunned status.
  • e.hp — subtracts burning damage per tick (value × stacks × dt).
  • game.stats.damageDealt — accumulates burn damage for run stats.

DOES NOT

  • Apply stun-related movement or attack gating directly — behavior handlers read hasStatus('stunned') or e._stunTimer themselves.
  • Apply shred damage scaling directly — damageEnemy() in damage.ts calls getShredMult() at hit-time.
  • Render status icons, particle effects, or any VFX.
  • Trigger sounds or telemetry events on apply/expire.
  • Spawn new statuses from triggers — only actions.ts calls applyStatus.
  • Defend against missing enemies, negative durations, or unknown status types (assumes valid inputs from callers).
  • Despawn enemies — burning damage that drops hp to 0 or below is detected by the standard damage pipeline elsewhere.

Signals

  • No external event bus or pub/sub. All consumers poll via hasStatus, getStacks, or getShredMult at the point of use.
  • Burn damage accumulates into game.stats.damageDealt as a passive side effect of tickStatuses.

Entry points

  • actions.tsapply_enemy_status and apply_status_aoe effect actions call applyStatus().
  • damage.tsdamageEnemy() reads getShredMult() to scale incoming damage.
  • behaviors.ts — enemy AI reads hasStatus('stunned') to skip movement/attack ticks.
  • bridge.ts — calls tickStatuses() once per frame in the enemy update loop.

Pattern notes

  • Status entries follow a uniform { stacks, timer, value } shape (EnemyStatusEntry), so the same storage handles all three types.
  • Re-apply semantics: timer refreshes to Math.max(existing, incoming) so longer-duration applications never shorten an existing effect; stacks increment up to maxStacks (stun uses default maxStacks = 1, so it cannot stack).
  • Burning DPS is computed as value × stacks × dt per frame, applied directly to e.hp. No tick rate — it’s continuous per-frame.
  • Shred is read-only at damage-time via getShredMult rather than precomputed; this keeps the multiplier always-fresh and avoids cache invalidation.
  • Stun maintains a parallel e._stunTimer field purely for legacy behavior code that hasn’t been migrated to hasStatus. New code should prefer the unified API.
  • Storage uses a plain object (Record) rather than Map as an explicit mobile-performance choice noted in the file header.
  • The tick loop short-circuits on !e._statuses and e.hp <= 0 to avoid touching dead or status-free enemies.
  • Expired entries are reset in-place (stacks = 0, timer = 0) rather than deleted, leaving the record key in place for cheap reuse.
  • clearStatuses sets e._statuses = undefined rather than emptying it — full reset on death/despawn.