What this is

Affixes are behavior primitives that modify enemy hosts. The catalog in data/affixes.ts is partitioned into two disjoint roll pools by host context: world-roaming elite-pack leaders and arena-bound bosses. The split is enforced in code by separate const arrays (BOSS_AFFIX_DEFS and WORLD_ELITE_AFFIX_DEFS) that concatenate into the final AFFIX_DEFS export, plus a separate exported id list WORLD_ELITE_AFFIX_IDS that scopes the roller. The two pools differ in their dependency surface — boss-flavored affixes assume a BossArena and anchor enemies; world-roaming affixes run on free-roaming hosts in the open world with no arena, no anchors, and no boss-bar plumbing.

The two pools

PoolSource arrayAffix idsRequired context
Boss-flavoredBOSS_AFFIX_DEFSshielded, shielded_respawn, gated, respawn_as, periodic_invuln, reflective, armoredBossArena + anchor enemies
World-roaming eliteWORLD_ELITE_AFFIX_DEFSburning_aura, volatile, regenerating, reflective_burst, phasing, summoner, hardened, gravity_wellNone — open world

Priority values (used by the dispatcher for hook ordering):

AffixPoolPriority
shieldedboss100
shielded_respawnboss100
respawn_asboss95
gatedboss90
periodic_invulnboss85
reflectiveboss80
armoredboss20
phasingworld75
reflective_burstworld70
burning_auraworld60
volatileworld60
gravity_wellworld55
regeneratingworld50
summonerworld40
hardenedworld25

How elites get affixes

Elite-pack leaders roll affixes at spawn time via engine/affixes/roll.ts → rollEliteAffixes(rarity, rng, pool, archetype). The roller samples from WORLD_ELITE_AFFIX_POOL (a local copy of WORLD_ELITE_AFFIX_IDS, kept in roll.ts to break a circular import). Boss-flavored affixes are excluded — the elite roller never returns ids from BOSS_AFFIX_DEFS.

Rarity drives the target count:

RarityTarget count
common0
uncommon0
rare1 with probability RARE_AFFIX_DROP_RATE (0.7), else 0
epic1 (50%) or 2 (50%)
legendary2 (50%) or 3 (50%)

Sampling is uniform without replacement (Fisher-Yates partial shuffle) so a host never carries the same affix twice. If an archetype is passed and matches an entry in ARCHETYPE_AFFIX_BIAS, sampling switches to weighted: the named affix gets BIAS_WEIGHT (3.0) × the weight of unbiased ids.

Archetype bias table:

ArchetypeBiased affix
chargerhardened
brutehardened
racerphasing
mortarvolatile
bombardiervolatile
sniperreflective_burst
lurkerreflective_burst
gunnerburning_aura
wispsummoner
fieldregenerating
orbgravity_well

How bosses get affixes

Bosses do not roll. Each BossDef declares its affix loadout statically via an affixIds: string[] array (and for phase-respawn bosses, a per-phase nextAffixIds: string[][] inside the respawn_as config). At spawn, the engine instantiates one AffixInstance per declared id with state: {}, then per-frame the dispatcher invokes onSpawn / onUpdate / filterIncomingDamage / onDeath hooks for each instance in priority order.

Declared loadouts in v1 bosses:

BossaffixIds
Killer Croc['shielded']
Junkrat Captains['gated']
Awakened Mech['respawn_as'] (phase 1) → ['respawn_as'] (phase 2 body) → no affixes (terminal Bigbot body)

Boss-flavored affixes assume runtime state that the world doesn’t carry. shielded / shielded_respawn call requireArena(game) and crash if game.bossArena is null. gated requires a state.gateGroupId and scans sibling enemies. respawn_as reads state.phases.{thresholds, nextTypes} to swap the host body when HP-fraction thresholds cross. Putting these affixes on a roving elite would either crash (arena missing) or silently no-op (no anchors, no siblings, no phase config). The pool split keeps the roller from ever producing those configurations.