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:
| Band | Value | Used by |
|---|---|---|
| Core | 100 | Damage application, kill bookkeeping, score, XP, run-state mutations |
| Effects | 50 | Affixes, on-kill / on-hit triggers, gameplay reactions |
| Audio / VFX | 0 | Sound 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 destroyedtagged_kill— kill with a specific tag discriminator (used for affix triggers)damage_dealt— damage application (num1 = amount, str1 = damage type)player_hit— player took damageplayer_healed— player gained HPweapon_fire— a weapon discharged (used for SFX + affix on-fire hooks)crate_break— destructible crate destroyedevent_complete— world event (props event, mini-boss, sealed arena) finishedlevel_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.