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
| Pool | Source array | Affix ids | Required context |
|---|---|---|---|
| Boss-flavored | BOSS_AFFIX_DEFS | shielded, shielded_respawn, gated, respawn_as, periodic_invuln, reflective, armored | BossArena + anchor enemies |
| World-roaming elite | WORLD_ELITE_AFFIX_DEFS | burning_aura, volatile, regenerating, reflective_burst, phasing, summoner, hardened, gravity_well | None — open world |
Priority values (used by the dispatcher for hook ordering):
| Affix | Pool | Priority |
|---|---|---|
shielded | boss | 100 |
shielded_respawn | boss | 100 |
respawn_as | boss | 95 |
gated | boss | 90 |
periodic_invuln | boss | 85 |
reflective | boss | 80 |
armored | boss | 20 |
phasing | world | 75 |
reflective_burst | world | 70 |
burning_aura | world | 60 |
volatile | world | 60 |
gravity_well | world | 55 |
regenerating | world | 50 |
summoner | world | 40 |
hardened | world | 25 |
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:
| Rarity | Target count |
|---|---|
common | 0 |
uncommon | 0 |
rare | 1 with probability RARE_AFFIX_DROP_RATE (0.7), else 0 |
epic | 1 (50%) or 2 (50%) |
legendary | 2 (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:
| Archetype | Biased affix |
|---|---|
charger | hardened |
brute | hardened |
racer | phasing |
mortar | volatile |
bombardier | volatile |
sniper | reflective_burst |
lurker | reflective_burst |
gunner | burning_aura |
wisp | summoner |
field | regenerating |
orb | gravity_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:
| Boss | affixIds |
|---|---|
| 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.