What this is
Each affix def carries a numeric priority. When an enemy host carries multiple affixes that each define a filterIncomingDamage hook, the affix dispatcher (engine/affixes/index.ts → runDamageFilterChain) sorts the host’s instances by descending priority and pipes the incoming damage through each filter in turn. The output of one filter is the input to the next. If any filter in the chain returns exactly 0, the chain short-circuits — no further filters run, and the host takes zero damage.
The same priority value also governs hook ordering for the three lifecycle hooks (onSpawn, onUpdate, onDeath) on the same host — all four dispatchers (applyAffixesOnSpawn, applyAffixesOnUpdate, applyAffixesOnDeath, runDamageFilterChain) call sortedDefs(host) which sorts b.priority - a.priority (descending).
Priority table (all 15 affixes)
| Affix id | Priority | Pool | Has filterIncomingDamage | Notes |
|---|---|---|---|---|
shielded | 100 | boss | yes | Returns 0 while any anchor alive |
shielded_respawn | 100 | boss | yes | Returns 0 while any anchor alive |
respawn_as | 95 | boss | no | Phase swap on HP-frac crossing (onUpdate only) |
gated | 90 | boss | yes | Returns 0 while any sibling sharing gateGroupId alive |
periodic_invuln | 85 | boss | yes | Returns 0 during active window |
reflective | 80 | boss | yes | Reserved stub — returns input unchanged |
phasing | 75 | world | yes | Returns 0 during active phase window |
reflective_burst | 70 | world | yes | Side-effect only — returns input unchanged |
burning_aura | 60 | world | no | Aura DPS (onUpdate only) |
volatile | 60 | world | no | Death explosion (onDeath only) |
gravity_well | 55 | world | no | Visual swirl + prop drop |
regenerating | 50 | world | no | Self-heal over time (onUpdate only) |
summoner | 40 | world | no | One-shot minion spawn on HP threshold |
hardened | 25 | world | yes | Damage multiplier ramping over lifetime |
armored | 20 | boss | yes | Flat damage multiplier (default 0.5) |
Affixes with identical priority (shielded vs shielded_respawn at 100; burning_aura vs volatile at 60) have undefined relative order — JavaScript’s Array.prototype.sort is stable in modern engines, so ties resolve to registration order, which is data/affixes.ts declaration order.
Zero short-circuit rule
runDamageFilterChain walks the sorted def list and threads current = def.filterIncomingDamage(host, current, game, world) through each filter. After every call it checks if (current === 0) return 0; and exits immediately. The check is a strict-equality compare against 0, not a threshold — a filter returning a tiny positive value (e.g. armored at 0.5 × 0.01 dmg = 0.005) does not short-circuit.
Practical consequences:
- A host with
shielded(P100) +armored(P20) takes zero damage while any shielded anchor is alive — thearmoredfilter never runs. - A host with
phasing(P75) +reflective_burst(P70) blocks reflective_burst entirely during the phasing window — phasing returns 0,reflective_burst.filterIncomingDamageis skipped, so no AoE burst fires on big hits. - A host with
periodic_invuln(P85) +hardened(P25) takes zero damage during the invuln window. Outside the window, hardened’s ramp multiplier applies. reflectiveandreflective_burstnever return 0 themselves — they pass damage through (reflective is a no-op stub; reflective_burst’s effect is a separate radial AoE on the player and is a pure side-effect of the filter).
A filter returning a negative value is not specially handled — the dispatcher would carry it into the next filter unchanged. No current affix produces negative output; armored and hardened both multiply by values in [0, 1].
Hook ordering
All four dispatchers share sortedDefs(host) (descending priority) and iterate the result with a plain for loop:
| Dispatcher | Method | Side effect when present |
|---|---|---|
applyAffixesOnSpawn | sequential, descending priority | Each onSpawn(host, game, world) fires once at enemy spawn |
applyAffixesOnUpdate | sequential, descending priority | Each onUpdate(host, dt, game, world) fires every frame |
applyAffixesOnDeath | sequential, descending priority | Each onDeath(host, game, world) fires once on enemy death |
runDamageFilterChain | sequential, descending priority, short-circuits on 0 | Pipes damage through each filterIncomingDamage |
onSpawn, onUpdate, and onDeath have no short-circuit — every hook in the chain runs regardless of state. Only runDamageFilterChain exits early on a 0 return.
Affix × affix interactions (e.g. phasing doubles regenerating’s heal rate; summoner halves reflective_burst’s threshold; volatile + reflective_burst fire a chained burst on death) live inside the affected hook bodies in runtime.ts and check co-presence via the hostHasAffix(host, defId) helper. These interactions are independent of priority ordering — they read sibling instance state directly rather than relying on chain position.