Sig — Signal Bus

Sig is the game’s synchronous in-process event bus. Systems listen for named events and fire them with a fixed-width payload — no allocations per dispatch, no async, no queueing. Every listener runs to completion before Sig.fire returns.

Source: src/starship-survivors/engine/core/signals.ts.

API

Sig.on(name, listener, priority?)   // register; higher priority fires first
Sig.off(name, listener)             // remove a specific listener
Sig.fire(name, uid1, uid2, num1, num2, str1)  // dispatch
Sig.has(name)                       // any listeners?
Sig.clear()                         // wipe everything (used on run reset)

The payload is a single shared SignalContext object reused across every fire — listeners read it inside the callback only, never store the reference.

interface SignalContext {
  name: string;
  uid1: number;   // primary entity uid (killer, source, etc.)
  uid2: number;   // secondary entity uid (victim, target, etc.)
  num1: number;   // generic numeric slot (damage, value, count)
  num2: number;   // generic numeric slot (x, multiplier, kind)
  str1: string;   // tag, archetype, or string discriminator
}

All payload fields are optional — they default to 0 / '' if not passed.

Priority bands

Listeners are sorted by descending priority on register. The three load-bearing bands:

BandValueUsed by
Core100Damage application, kill bookkeeping, score, XP, run-state mutations
Effects50Affixes, on-kill / on-hit triggers, gameplay reactions
Audio / VFX0Sound effects, particles, screen shake, floating text

Core fires before Effects which fire before Audio/VFX. This guarantees a kill is registered (Core) before an affix reads the kill state (Effects) before the SFX plays (Audio/VFX).

Recursion cap

Sig enforces a hard depth limit of 8 nested fires (_depth > 8 short-circuits). A listener that fires another signal is fine; a listener that fires itself transitively beyond 8 levels deep is silently dropped to prevent infinite loops.

Common signals

A non-exhaustive list of the signals load-bearing across the run:

  • enemy_kill — any enemy died (uid1 = killer, uid2 = victim, str1 = enemy tag)
  • boss_kill — boss killed (terminal event for boss arenas)
  • boss_body_kill — multi-part boss segment destroyed
  • tagged_kill — kill with a specific tag discriminator (used for affix triggers)
  • damage_dealt — damage application (num1 = amount, str1 = damage type)
  • player_hit — player took damage
  • player_healed — player gained HP
  • weapon_fire — a weapon discharged (used for SFX + affix on-fire hooks)
  • crate_break — destructible crate destroyed
  • event_complete — world event (props event, mini-boss, sealed arena) finished
  • level_up — XP threshold crossed

Listeners register during system init and unregister on Sig.clear() at run reset.

Why a shared context

Allocating a fresh payload object per fire was measured to allocate megabytes per minute under heavy combat. The shared _ctx is mutated in place — listeners that need to retain a value copy primitives out of the context immediately.