data/affixes.ts

Affix library — data table that composes runtime behavior primitives by id. Two pools: boss-flavored affixes (assume BossArena + anchor enemies) and world-roaming elite affixes (rolled at spawn time on elite-pack leaders). Hook implementations live in engine/affixes/runtime.ts; this file only declares the defs. The affix dispatcher (engine/affixes/index.ts) auto-registers the catalog at module-init.

λ — Charge across the river

ExportKindPurpose
WORLD_ELITE_AFFIX_DEFSAffixDef[]World-roaming elite affix definitions. No BossArena required. Rolled by engine/affixes/roll.rollEliteAffixes().
WORLD_ELITE_AFFIX_IDSstring[]Ordered ids of WORLD_ELITE_AFFIX_DEFS. Roll pool.
AFFIX_DEFSAffixDef[]Full catalog = BOSS_AFFIX_DEFSWORLD_ELITE_AFFIX_DEFS. Consumed by the dispatcher.

Re-exported constants:

ConstantFromSource-of-truth domain
SHIELDED_DEFAULT_ANCHOR_COUNTengine/affixes/runtimeShielded affix anchor count
SHIELDED_DEFAULT_ANCHOR_TYPEengine/affixes/runtimeShielded anchor enemy id
SHIELDED_ANCHOR_RING_FRACengine/affixes/runtimeRing radius as fraction of arena
SHIELDED_RESPAWN_DEFAULT_INTERVALengine/affixes/runtimeShielded-respawn cadence (s)
ARMORED_DEFAULT_MULTengine/affixes/runtimeArmored incoming-damage multiplier
BURNING_AURA_DEFAULT_RADIUSengine/affixes/paletteBurning-aura radius
BURNING_AURA_DEFAULT_DPSengine/affixes/paletteBurning-aura damage-per-second
VOLATILE_DEFAULT_RADIUSengine/affixes/paletteVolatile on-death blast radius
VOLATILE_DEFAULT_DAMAGEengine/affixes/paletteVolatile on-death blast damage
REGENERATING_DEFAULT_FRAC_PER_SECengine/affixes/paletteRegen as fraction of max HP / sec
REFLECTIVE_BURST_DEFAULT_THRESHOLDengine/affixes/paletteReflective-burst HP-loss trigger
REFLECTIVE_BURST_DEFAULT_RADIUSengine/affixes/paletteReflective-burst blast radius
REFLECTIVE_BURST_DEFAULT_DAMAGEengine/affixes/paletteReflective-burst blast damage

Κ — The dispatcher chain

Each AffixDef binds an id to a priority and up to four lifecycle hooks (onSpawn, onUpdate, onDeath, filterIncomingDamage) imported from engine/affixes/runtime. The dispatcher iterates affixes in priority order (highest first) per event.

BOSS_AFFIX_DEFS (module-private)

Excluded from the world-roaming elite-affix roll pool.

idPriorityHooks
shielded100onSpawn, filterIncomingDamage
shielded_respawn100onSpawn, onUpdate, filterIncomingDamage
gated90filterIncomingDamage
respawn_as95onUpdate
periodic_invuln85onUpdate, filterIncomingDamage
reflective80filterIncomingDamage
armored20filterIncomingDamage

WORLD_ELITE_AFFIX_DEFS (exported)

idPriorityHooksNotes
burning_aura60onUpdate
volatile60onDeath
regenerating50onUpdate
reflective_burst70filterIncomingDamage
phasing75onUpdate, filterIncomingDamageFires before reflective_burst (70) so phasing invuln short-circuits the burst chain.
summoner40onUpdateFires AFTER damage filters; host must actually drop below HP threshold for the trigger to land.
hardened25onUpdate, filterIncomingDamageFires close to last so phasing / reflective_burst get their say first.
gravity_well55onUpdate, onDeathMostly visual tell + death-drop; priority placed in mid-band for clean log ordering.

Σ — Where this flows

  • Consumed by: engine/affixes/index.ts (dispatcher auto-registers AFFIX_DEFS at module-init).
  • Roll pool: engine/affixes/roll.rollEliteAffixes() reads WORLD_ELITE_AFFIX_IDS.
  • Hook implementations: all hooks resolve to functions exported from engine/affixes/runtime.ts.
  • Tuning constants: balance lives in engine/affixes/runtime.ts (shielded / armored) and engine/affixes/palette.ts (burning_aura / volatile / regenerating / reflective_burst). Re-exported here for downstream importers.
  • Slice provenance: Slice 2 (tick 9) added phasing, summoner, hardened. Slice 3 (tick 23) added gravity_well as the 8th world-roaming affix.

Π — Priority bands

The priority field defines dispatch order within an affix list. The catalog uses three rough bands:

  • 100–80 — gating / invulnerability (shielded, shielded_respawn, respawn_as, gated, periodic_invuln, reflective). Run first so they can short-circuit damage entirely.
  • 75–50 — reactive damage / behavior (phasing, reflective_burst, burning_aura, volatile, gravity_well, regenerating). Run middle.
  • 40–20 — late triggers / multipliers (summoner, hardened, armored). Run after gating + reactive so they see the post-filter state.

EXTRACT-CANDIDATE

  • Hard-coded priority literals (100, 95, 90, 85, 80, 75, 70, 60, 55, 50, 40, 25, 20) could be named bands (AFFIX_PRIORITY_GATE, AFFIX_PRIORITY_REACTIVE, AFFIX_PRIORITY_LATE) to make the dispatch model explicit and prevent drift between affixes.
  • BOSS_AFFIX_DEFS is module-private but WORLD_ELITE_AFFIX_DEFS is exported — asymmetry. Either export both for parity (so tests can introspect the boss pool) or document why the boss list intentionally has no roll-pool sibling.
  • The re-export block spans two source modules (runtime and palette). A consolidated engine/affixes/constants.ts barrel would let data/affixes.ts re-export from one place and clarify that tuning lives there, not here.
  • WORLD_ELITE_AFFIX_IDS is derived from WORLD_ELITE_AFFIX_DEFS.map((d) => d.id). If the roll pool ever needs weighting, this string[] would extract cleanly to Array<{ id: string; weight: number }> co-located here.