trigger-system.ts

PURPOSE

Proximity-based level triggers. Player position is sampled each frame against a flat list of LevelTrigger records authored in the level config. When the player crosses into a trigger’s radius, the system invokes the matching callback (dialogue, spawn wave, spawn zone, checkpoint, generic event). Used to drive scripted moments and mission objective gating in scripted levels.

OWNS

  • TriggerSystem runtime state: the working copy of triggers: LevelTrigger[], the firedIds set of one-shot trigger IDs that have already fired, the _inside set of trigger IDs the player is currently inside, and the user-supplied callbacks bundle.
  • The enter-edge detection (inside this frame, not inside last frame) per trigger.
  • The one-shot-vs-repeatable gating rule driven by LevelTrigger.oneShot.
  • The trigger-type dispatch switch that routes a fired trigger to the correct callback.

READS FROM

  • LevelTrigger records from ../../data/level-config (id, type, x, y, radius, oneShot, payload).
  • Player position passed in per tick as playerX, playerY (the bridge supplies the ship’s world position).
  • The trigger array supplied to createTriggerSystem — copied into the system at construction, then mutated by sandbox-only push/pop in the bridge.

PUSHES TO

  • The five TriggerCallbacks: onDialogue, onSpawnWave, onSpawnZone, onCheckpoint, onEvent. The fired LevelTrigger (including its payload) is handed to the callback verbatim; the system does no payload interpretation.
  • Its own firedIds set when a fired trigger has oneShot !== false.
  • Its own _inside set every tick (adds when inside, deletes when not), so enter-edge detection works the next frame.

DOES NOT

  • Does not interpret TriggerPayload fields. Enemy spawning, dialogue text rendering, checkpoint logic, and event handling all live in the callbacks the bridge wires up.
  • Does not check exit edges, dwell time, or re-arm timers — repeatable triggers fire on every fresh entry only.
  • Does not deduplicate triggers, validate radius > 0, or guard against NaN positions.
  • Does not persist firedIds or _inside across runs; both reset when a new system is created on level (re)load.
  • Does not subscribe to any signal bus, render anything, or touch physics. It is a pure tick over a flat array.
  • Does not own the trigger list’s source of truth — the level config does. The runtime copy is mutable for sandbox authoring only.

Signals

None. The module emits no signals and subscribes to none. All notifications flow through the TriggerCallbacks struct supplied at construction.

Entry points

  • createTriggerSystem(triggers, callbacks): TriggerSystem — constructs the runtime system, shallow-copying triggers and initialising empty firedIds and _inside sets.
  • tickTriggers(system, playerX, playerY): void — single per-frame call. Iterates every trigger, computes inside-radius via Math.hypot, fires on the enter edge subject to one-shot gating, then updates _inside.
  • Exported types: TriggerCallbacks, TriggerSystem.

Pattern notes

  • Enter-edge fires only: a trigger fires the frame the player transitions from outside to inside. Standing inside does not refire.
  • One-shot is the default. oneShot === false is the explicit opt-in for repeatable triggers; any other value (including undefined) marks the trigger one-shot and adds its id to firedIds on first fire.
  • One-shot bookkeeping uses id — duplicate IDs across triggers would share fired state. IDs are assumed unique by the caller.
  • Distance test uses Math.hypot(playerX - t.x, playerY - t.y) < t.radius (strict less-than). Triggers are circular only.
  • The callbacks struct is required for all five types even when a level only uses one — the bridge supplies empty stubs (onDialogue() {}, etc.) for unused types.
  • Mission objective gating is layered on top of triggers by the bridge: spawn-wave triggers drive enemy authoring for scripted missions, and the bridge’s mission-state machine listens to those spawns (and other game events) to determine objective completion. The trigger system itself has no concept of missions.
  • Sandbox-only mutation: the bridge exposes sandboxAddTrigger / sandboxUndoTrigger / sandboxGetTriggerCount that push and pop directly on system.triggers. Newly pushed triggers participate in ticks from the next frame on.
  • A single _triggerSystem instance lives on the bridge, recreated on level load and on the level-transition rebuild path; cleared to null on full teardown.
  • Triggers only tick while game.phase === 'playing' — the bridge gates the tickTriggers call on phase, so paused/menu frames do not fire entries.