Sig Dispatch Flow (Kill Chain)
The Sig bus is a synchronous, fixed-payload event dispatcher (see sig-bus). All gameplay reactions to combat — artifacts, effects, achievements, telemetry, juice, VFX — hang off named signals fired from the damage and death sites. The kill chain is the canonical example: one bullet landing on an enemy walks through up to five named signals in a fixed order, each delivering a context object listeners can react to.
Typical chain on enemy kill
When a player bullet hits an enemy with enough damage to kill it, the following signals fire in this order within a single damageEnemy call:
bullet_hit— fired fromcollision-resolver.tsimmediately after the bullet’s damage is committed. Payload:uid1 = enemy.eid,num1 = b.dmg,str1 = enemy._lastDmgTag(the weapon’s damage tag). Legendary weapons with asecondaryDamageTagfire a secondbullet_hitwith the secondary tag so listeners keyed on either parent tag react to the same hit.damage_dealt— fired insidedamageEnemyafterenemy.hp -= rawDmgis applied. Payload:num1 = rawDmg,num2 = enemy.x,str1 = enemy._lastDmgTag. This is the generic “amount + position + damage tag” signal that effects use for type-conditional reactions regardless of whether the hit kills.enemy_kill— fired whenenemy.hp <= 0and the enemy is not a boss anchor or shared-health body. Payload:num1 = enemy.x,num2 = enemy.y,str1 = enemy.typeId. Artifacts like Soul Leech and ghost spawners listen here.tagged_kill— fired immediately afterenemy_kill, on the same regular-kill branch. Payload:num1 = enemy.x,num2 = enemy.y,str1 = enemy._lastDmgTag. Carries the damage tag for type-conditional kill effects (e.g. “on fire kill”, “on bullet kill”) so listeners can short-circuit when the tag doesn’t match.boss_kill— fired only ifenemy.isBossand the dead body was the final sharer in a shared-health encounter. Payload:num1 = enemy.x,num2 = enemy.y,str1 = enemy.typeId. Bosses also receive a per-bodyboss_body_killsignal on every shared-health body death.
Streak milestones additionally emit kill_streak_milestone from _checkStreakMilestone, and boss anchors emit boss_anchor_destroyed instead of the regular enemy_kill / tagged_kill pair (anchors don’t grant XP, streak, or elite bonuses by design).
Other dispatch sites that follow the same pattern
level_up— fired fromLevelingSystem.levelUpinworld/leveling.tswithnum1 = game.level. Drives effect-engine listeners (level-gated triggers), the level-up orchestrator, and reward queue side effects.- Player damage chain —
damagePlayerfiresshield_hit+player_damage(withstr1 = 'shield') on shield absorption,hull_damage+player_damage(str1 = 'hull') on hull damage, andshield_breakwhen the shield drops to zero.
Priority bands
Listeners register with a numeric priority; higher fires first. The bus sorts the listener list at registration time, so dispatch is a simple in-order walk. The three conventional bands (documented in sig-bus and priority-bands):
- 100 — Core logic. State mutations that other listeners may read (e.g. effect-engine bookkeeping, status accumulators).
- 50 — Effects. Artifact reactions, on-hit / on-kill effects that read state set by core logic.
- 0 — Audio / VFX / juice. Cosmetic reactions; never mutate gameplay state.
A depth cap of 8 nested Sig.fire calls prevents infinite signal loops; once breached the call short-circuits and returns without dispatching.
Snapshot ctx pattern
The bus reuses a single shared _ctx object across every fire call (see signals.ts). On dispatch, fire mutates the six fields (name, uid1, uid2, num1, num2, str1) and passes the same object reference to every listener in sequence. Listeners must read the values they need synchronously and not retain the ctx reference past their own callback — by the time the next signal fires, the same object has been overwritten with the next signal’s payload.
Practical consequences:
- Payloads are five primitive slots, no objects or arrays. Anything richer has to be looked up by
uid1/str1(e.g. “find the enemy with this eid”, “look up the weapon by id”). - All dispatch is synchronous within a single frame. By the time
Sig.fire('enemy_kill', ...)returns, every listener has run to completion. Re-entrant fires inside a listener execute immediately, bounded by the depth cap. - Zero allocation per fire (after listener-list lookup). The reused ctx is the reason
Sig.fireis cheap enough to call from the hot damage path on every bullet hit.
Where to read the source
src/starship-survivors/engine/core/signals.ts— the bus, the ctx, the depth cap.src/starship-survivors/engine/combat/damage.ts—damage_dealt/enemy_kill/tagged_kill/boss_kill/boss_body_kill/boss_anchor_destroyeddispatch sites (and the player-damage signals).src/starship-survivors/engine/combat/collision-resolver.ts—bullet_hitdispatch (including the legendary secondary-tag re-fire).src/starship-survivors/engine/world/leveling.ts—level_upandkill_streak_milestonedispatch.
Related
- sig-bus — the underlying dispatcher contract.
- priority-bands — the three-band convention.
- effect-engine — the primary consumer of these signals.
- damage-defense-chain — the chain that produces
damage_dealtand the player-damage signals. - damage-tag-taxonomy — what
str1carries forbullet_hit/damage_dealt/tagged_kill. - telemetry-events — telemetry listeners on these signals.