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 idPriorityPoolHas filterIncomingDamageNotes
shielded100bossyesReturns 0 while any anchor alive
shielded_respawn100bossyesReturns 0 while any anchor alive
respawn_as95bossnoPhase swap on HP-frac crossing (onUpdate only)
gated90bossyesReturns 0 while any sibling sharing gateGroupId alive
periodic_invuln85bossyesReturns 0 during active window
reflective80bossyesReserved stub — returns input unchanged
phasing75worldyesReturns 0 during active phase window
reflective_burst70worldyesSide-effect only — returns input unchanged
burning_aura60worldnoAura DPS (onUpdate only)
volatile60worldnoDeath explosion (onDeath only)
gravity_well55worldnoVisual swirl + prop drop
regenerating50worldnoSelf-heal over time (onUpdate only)
summoner40worldnoOne-shot minion spawn on HP threshold
hardened25worldyesDamage multiplier ramping over lifetime
armored20bossyesFlat 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 — the armored filter never runs.
  • A host with phasing (P75) + reflective_burst (P70) blocks reflective_burst entirely during the phasing window — phasing returns 0, reflective_burst.filterIncomingDamage is 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.
  • reflective and reflective_burst never 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:

DispatcherMethodSide effect when present
applyAffixesOnSpawnsequential, descending priorityEach onSpawn(host, game, world) fires once at enemy spawn
applyAffixesOnUpdatesequential, descending priorityEach onUpdate(host, dt, game, world) fires every frame
applyAffixesOnDeathsequential, descending priorityEach onDeath(host, game, world) fires once on enemy death
runDamageFilterChainsequential, descending priority, short-circuits on 0Pipes 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.