engine/effects

PURPOSE — Unified trigger → condition → action pipeline that drives every reactive gameplay procedure on the player ship. Artifacts, passives, and ship mods declare effects as data; this system registers them at run start, listens to engine signals, evaluates conditions, dispatches actions (modifier add/remove, AoE damage, projectile spawn, enemy status, VFX, custom handlers), maintains cooldowns and charges, and ticks auras, timers, counters, and per-frame custom handlers.

OWNS

  • The central effect registry — every EffectInstance registered for the current run (definition reference, owner tag, value dict for $var resolution, cooldown remaining, charges used, counter accumulator, timer accumulator, aura active flag, enabled flag).
  • Indexed dispatch tables — signal-name → instances, aura instance list, timer instance list, counter signal-name → instances, custom-tick instance list.
  • The Sig listener subscriptions registered on the effects priority band, plus the bookkeeping that lets clear() unsubscribe them cleanly at run end.
  • The signal-context snapshot taken before each action dispatch, guarding against the shared mutable context being overwritten by nested signals fired from within actions.
  • Cached references to the current run’s game / ship / world state, refreshed each tick and read by signal handlers, aura ticks, counter listeners, and timer-reset listeners.
  • Initialization state — whether listeners have been wired up, used to decide whether register() should auto-subscribe and whether run_start effects fire immediately.
  • The action dispatch map (built-in action types → executor functions) and the condition dispatch map (built-in condition types → evaluator functions).
  • The two custom-handler registries — signal-triggered custom actions and per-frame custom tick handlers — populated at module load time.
  • The standalone collections owned by the custom-handlers module — active flame zones (position, radius, dps, lifetime, damage tick countdown, vfx tick countdown) and active delayed AoE bursts (position, radius, damage, delay, color pair), each with their own max-size cap and oldest-first eviction.
  • Enemy status state, lazy-allocated on each enemy — entries by status type with stack count, remaining timer, and per-stack magnitude, plus the legacy stun timer mirror used by enemy AI.
  • Parameter resolver — the $varName substitution that swaps placeholder strings for numbers from the instance value dict.
  • Telemetry side channel — a per-owner trigger counter on the global window object for gauntlet metric collection.

READS FROM

  • engine/core/signals — the global signal bus, including its mutable shared context fields that get snapshotted on every fire.
  • engine/core/modifiers — to add stat modifiers, to remove modifiers by source tag for aura deactivation and scaled-stat re-evaluation, and to read the per-target bucket for buff-presence condition checks.
  • engine/core/types for GameState, ShipState, WorldState shapes and engine/core/state for the module-level game/ship/world/camera/canvas-dimension singletons consumed by the custom-handlers tick paths.
  • engine/core/spatial-grid — broad-phase enemy queries for flame-zone damage ticks and delayed AoE detonations.
  • engine/core/clock for the shared run clock used by flame-zone visual flicker phase.
  • engine/combat via damageEnemy for AoE actions, custom-handler damage application, flame-zone DPS ticks, and delayed-AoE detonations.
  • data/artifacts for the per-artifact effect definitions, color palette, optional proc audio cue, and tier-value lookup used at run-start registration.
  • The artifact world layer for the active artifact instance list and the artifact-flash setter used by the activation flash action.
  • engine/vfx — particles, sonar rings, explosion FX, player glow, post-FX rings — consumed by the built-in vfx and flash_artifact actions and by custom handlers.
  • engine/audio via Juice for the per-artifact proc audio cue on activation flash.
  • engine/rendering camera for screen-space projection in the flame-zone draw pass and the artifact banner pusher for activation banners.
  • Ship state fields — HP, shield, velocity, position, angle, radius, heat, hpMax/shieldMax bases, weapon list, eid — for condition evaluation, scaled-stat interpolation, action centers, and modifier targeting.
  • Game state fields — time, kill streak, upgrade counts, artifacts list, boss arena, current level, stats, reward queue — for condition evaluation and upgrade-count / reward-queue mutation actions.
  • Enemy state fields — hp, hpMax, position, velocity, alive flag, dying flag, status bag, stun timer, pack-leader flag, id — for AoE iteration, status application, knockback, and shred multiplier lookup.

PUSHES TO

  • engine/core/modifiers — stat modifier add for modify_stat, scaled re-application for modify_stat_scaled, direct bucket splice for aura deactivation and remove_modifiers, dirty-set marking after every mutation.
  • engine/core/signals — re-emits via the emit_signal action and fires player_healed from the heal action; subscribes/unsubscribes signal listeners on the effects priority band over its full lifecycle.
  • The world’s player-bullet array — spawn_projectile and the projectile-spawning custom handlers push fully-formed bullet records (position, velocity, damage, radius, lifetime, color pair, pierce count, hit set, weapon id, archetype tag, homing strength, blast radius, optional range).
  • The world’s enemy list — direct velocity perturbation for knockback, direct HP subtraction for burning DPS and custom-handler damage fallback, lazy status-bag allocation on apply.
  • Ship state — HP and shield mutation for heals, invuln flag and timer for grant-invuln, heat reset, and per-weapon empower mult/shots fields.
  • Game state — upgrade-count mutation and reward-queue push for the random-upgrade grant.
  • engine/combat via damageEnemy for AoE damage, custom-handler damage, flame-zone DPS ticks, and delayed-AoE detonations.
  • engine/vfx — particle bursts, sonar rings, shockwaves, explosion FX, player glow flash, starbursts, post-FX impact rings, and the persistent flame-zone gradient draw stack.
  • engine/audio via Juice for the per-artifact proc audio cue.
  • The artifact world layer — UI icon flash via the flash setter and the on-screen activation banner via the banner pusher.
  • The global window object — per-owner trigger counters for gauntlet metric collection and a per-trigger console.log line with resolved params for telemetry.

DOES NOT

  • Define which effects exist, what their tier values are, or how they map to artifact / passive / mod content — those are data and live in data/artifacts and (Phase 6) the mod template layer.
  • Decide which artifacts the player has equipped or own the active artifact instance list — only consumes whatever the artifact world layer hands the run-effects orchestrator at mission start.
  • Resolve damage math, knockback math, life steal, shield absorption, affixes, boss caps, or i-frames on its damage actions — those live downstream of damageEnemy; this system only feeds raw values in.
  • Implement projectile flight, collision, on-hit, on-death, or behavior callbacks — spawn_projectile and the spawn-bullet custom handlers only push records onto the player-bullet array; the weapons / bullets path owns everything after spawn.
  • Render anything itself except the persistent flame-zone gradient stack — every other visual is delegated to the VFX module.
  • Define signal names, signal priority bands, or the signal context shape — only consumes the existing Sig API and snapshots its context fields.
  • Drive enemy AI — only mutates status fields and the legacy stun-timer mirror; enemy behaviors read hasStatus/getStacks to decide what to do.
  • Compute scaling curves, tier interpolation, or artifact rarity — only consumes the resolved tier value dict supplied at registration.
  • Run any logic for installed mod abilities — the mod registration phase in the run-effects orchestrator is a documented placeholder pending the mod-system follow-up.
  • Provide a generic event bus — emit_signal re-emits through the existing Sig channel; there is no second routing layer here.

Signals fired

  • player_healed — emitted by the heal action when the heal delta is positive.
  • run_start — emitted indirectly via the trigger-type dispatch path the first time init() runs (and immediately on mid-run re-registration if the engine is already initialized).
  • Any signal named by an emit_signal action’s params — those re-enter the same bus the engine listens on, so effects can chain.

Signals watched

  • Every signal-trigger effect’s named signal — one bus listener per unique name, multiplexed across all instances watching it.
  • Every counter-trigger effect’s counted signal — separate listener that increments per-instance accumulators and dispatches at threshold.
  • Every timer-trigger effect’s optional reset signal — separate listener that zeros the timer accumulator and tears down any active aura modifiers attached to that instance.

Entry points

  • EffectEngine.register — register a batch of effect definitions for an owner with a tier-value dict; indexes by trigger type and auto-subscribes mid-run if already initialized.
  • EffectEngine.unregister — remove every effect for an owner, tear down any active aura modifiers it held, and rebuild the indexes.
  • EffectEngine.init — wire up signal / counter / timer-reset listeners and fire any pending run_start effects.
  • EffectEngine.tick — per-frame cooldown decay, aura interval evaluation with state-transition modifier add/remove, timer accumulation and one-shot or repeat fire, and dispatch of every custom_tick action.
  • EffectEngine.clear — unsubscribe every listener, drop every instance, drop the cached state refs, and reset the initialized flag.
  • EffectEngine.getInstances — read-only view of every registered instance.
  • EffectEngine.isInitialized — bridge-side flag query.
  • registerRunEffects — run-effects orchestrator called from the bridge at mission start; clears, registers each active artifact’s effects with its resolved tier values, primes the cached state via a zero-dt tick, and runs init().
  • teardownRunEffects — bridge-side run-end teardown that delegates to clear().
  • executeAction / executeAllActions — action dispatcher and per-instance loop, called from signal handlers, the aura tick, the timer tick, and the run-start path.
  • evaluateCondition / evaluateAllConditions — condition dispatcher and AND-folded evaluator, called before any action dispatch.
  • resolveParam / resolveAllParams / resolveNumber / resolveString$var substitution against the instance value dict, used by every action and condition that reads params.
  • CustomHandlers / CustomTickHandlers — the two name-keyed handler registries.
  • registerCustomAction / registerCustomTick — module-load-time registration helpers used by the custom-handlers module to wire in named behaviors (battering-ram restore, t-bone shockwave beam, crate-buster pulse, soul-leech ghosts, killstreak rain, lingering-flames zone spawn, random upgrade grant, versatility tracker).
  • applyStatus / hasStatus / getStacks / getShredMult — enemy status read/write API consumed by actions, enemy AI, and the combat damage path.
  • tickStatuses — per-frame status timer decay and burning DPS application, called by the bridge in the enemy update loop.
  • clearStatuses — per-enemy status teardown on death or despawn.
  • spawnFlameZone / tickFlameZones / drawFlameZones / clearFlameZones — persistent fire-patch lifecycle, exposed both for the lingering-flames custom handler and the fire-trail weapon path.
  • spawnDelayedAoE / tickDelayedAoEs / clearDelayedAoEs — fire-and-forget delayed AoE burst lifecycle for thematic legendary echoes.

Pattern notes

  • Effects are pure data — EffectDef carries a trigger description, condition list, action list, optional cooldown / charges / priority / banner flag. Adding a new artifact behavior is normally a data edit plus, if needed, one new condition or action entry in the dispatch maps.
  • The system is closed-vocabulary for built-ins (named action and condition types via two dispatch maps) and open-vocabulary for anything that can’t fit data params (the two custom-handler registries keyed by string name).
  • Signal context is snapshotted to a local object before any action dispatches, so actions that themselves fire signals can’t corrupt the outer handler’s view of the original event.
  • One bus listener per unique signal name — instances multiplex through the per-signal instance list. Listener teardown walks the listener-ref map at clear() time, with key prefixes distinguishing direct, counter, and timer-reset subscriptions on the same underlying signal name.
  • Auras run on an interval rather than every frame; state transitions add or remove modifiers via the instance’s owner tag as the source field, and a third “stay active” branch handles scaled-stat re-evaluation by tearing down and re-adding modifiers each interval.
  • unregister rebuilds every index from scratch rather than surgically removing entries — fine for the small instance counts a single run accumulates.
  • $var resolution is the only string-typed indirection layer — every other param is a literal number, string, or boolean. Unknown $var keys throw rather than silently defaulting.
  • The activation banner is dev-curated via a flag on the definition and only fires for owners tagged artifact:<id>, so high-frequency procs and auras don’t spam the screen.
  • Mid-run registration paths exist for both signal-trigger and counter-trigger instances and for run_start (which fires immediately if the engine is already initialized) — needed for artifacts picked up after mission start.
  • Custom handlers receive a fully numeric param dict; non-numeric params are silently filtered for custom_tick and coerced to numbers (boolean → 0/1) for custom, so handlers can declare their inputs as plain Record<string, number>.
  • Flame zones and delayed AoEs are run-scoped collections with hard caps that evict oldest-first; both expose explicit clear functions for run reset and their own draw and tick passes called from the bridge.
  • Enemy statuses are stored lazily as a plain record on the enemy entity (cheaper on mobile than a Map). Burning stacks tick damage every frame via dt-scaled DPS; shredded stacks expose a multiplier read by the combat path; stunned mirrors a legacy timer field for the existing AI handlers.
  • The custom_tick action type is a no-op in the normal action dispatch path — its real work happens in the engine tick loop, which walks the _tickHandlers index separately so per-frame work doesn’t have to fake a trigger.
  • Telemetry is two-pronged — every fire logs a [EFFECT] line with resolved params, and a per-owner counter on the global window object is bumped for gauntlet metric collection.