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
TriggerSystemruntime state: the working copy oftriggers: LevelTrigger[], thefiredIdsset of one-shot trigger IDs that have already fired, the_insideset of trigger IDs the player is currently inside, and the user-suppliedcallbacksbundle.- 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
LevelTriggerrecords 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 firedLevelTrigger(including itspayload) is handed to the callback verbatim; the system does no payload interpretation. - Its own
firedIdsset when a fired trigger hasoneShot !== false. - Its own
_insideset every tick (adds when inside, deletes when not), so enter-edge detection works the next frame.
DOES NOT
- Does not interpret
TriggerPayloadfields. 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 againstNaNpositions. - Does not persist
firedIdsor_insideacross 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-copyingtriggersand initialising emptyfiredIdsand_insidesets.tickTriggers(system, playerX, playerY): void— single per-frame call. Iterates every trigger, computes inside-radius viaMath.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 === falseis the explicit opt-in for repeatable triggers; any other value (includingundefined) marks the trigger one-shot and adds itsidtofiredIdson 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/sandboxGetTriggerCountthat push and pop directly onsystem.triggers. Newly pushed triggers participate in ticks from the next frame on. - A single
_triggerSysteminstance lives on the bridge, recreated on level load and on the level-transition rebuild path; cleared tonullon full teardown. - Triggers only tick while
game.phase === 'playing'— the bridge gates thetickTriggerscall on phase, so paused/menu frames do not fire entries.