engine/affixes
PURPOSE — Registry, rolling, and per-host lifecycle dispatch for the affix system: declares the affix-def contract, samples affix sets onto fresh elite-pack leaders, runs spawn / update / death hooks in priority order, and walks the damage-filter chain so individual affix hook bodies can short-circuit, scale, or pass through incoming damage.
OWNS
- The global
AFFIX_REGISTRYand its lookup / registration entry points (collision check on insert, throw-on-unknown lookup, idempotent catalog init from the data module). - The
AffixDefcontract: hook signatures for spawn / update / death / damage-filter and the per-def priority used to order dispatch. - Per-host dispatch loops for spawn, update, and death hooks (priority-descending walk over the host’s affix instances).
- The damage-filter chain: priority-descending walk that short-circuits when any hook reduces damage to zero.
- World-roaming elite affix pool, rarity-to-count distribution, rare-band drop rate, archetype-to-affix bias map and bias-weight constant.
- Uniform and weighted without-replacement sampling helpers used by the roll path; the RNG contract type and its
Math.randomdefault. - Per-affix VFX palette (color tints keyed by world-roaming affix id) and its lookup helper.
- All tunable constants for the runtime hook bodies (aura radius / dps, volatile radius / damage, regen fraction, reflective-burst threshold / radius / damage, phasing window / cycle, summoner threshold / minion type / count / spawn distance, hardened max-reduction / ramp duration, gravity-well pull radius / accel, shielded anchor count / type / ring fraction, shielded-respawn cadence, armored mult).
- Per-affix instance state shapes and their read-and-hydrate helpers (typed view onto the freeform
statebag with defaulted, validated tunables). - Late-bound adapter slots for the damage-player call and the prop-spawn call, plus the wiring entry points that the engine boot path uses to connect them; the throw-on-unwired guard for both adapters.
- Runtime hook bags for every affix shipping in the catalog (shielded, shielded-respawn, gated, respawn-as, periodic-invuln, reflective stub, armored, burning-aura, volatile, regenerating, reflective-burst, phasing, summoner, hardened, gravity-well).
- Body-swap mechanics for the phase-change affix: silent removal of the old enemy slot, carry-over of identity fields and HP, transplant of the next phase’s affix and ability lists, and the post-swap dispatch into the active boss def’s phase-change hook.
- Anchor-enemy spawn helper that tags spawned anchors for the boss-anchor reward gate and strips boss-only flags / shared HP from them.
- Affix-by-affix interaction beats (regen-during-phasing, regen-after-hardened-peak, phasing-hardens, summoner-volatile pretell, summoner-gravity clump, summoner-regen heal burst, summoner-phasing panic blink, summoner-reflective cohesion loss, reflective-gravity amp, gravity-phasing dense swirl, volatile-reflective chain).
- Affix-on-death prop drops (volatile-crystal, mineral-vein, comet-fragment, scrap-pile cluster, drone-wreck, magnetar-pulse, plus the volatile drop on reflective-burst death).
READS FROM
engine/core/typesforAffixInstance,EnemyEntity,GameState,WorldState,ShipState,BossArena.engine/core/statefor the sharedshipreference used by every world-roaming affix’s proximity gate and the damage-adapter call.engine/enemies/spawnerGameMasterfor anchor and minion spawns.engine/abilitiesfor the next-phase ability instance constructor used in the body-swap path.engine/boss/encounterfor the shared boss VFX kit handed to the phase-change hook.engine/vfx/particlesParticlesfor every affix’s particle emission.engine/telemetry/collectorfor proc, drop, blocked-damage, and interaction telemetry.data/bossesBOSS_DEFSfor the active boss def looked up at phase transition.data/affixesfor the catalog list consumed by the registry init.- The host
EnemyEntity.affixesarray (per-instance state, def-id back-reference). - The host’s
hp,hpMax,alive,_frozenForLag,_dying,typeId,radius,x,yfields plus boss / anchor / sharing flags used by readers and the body-swap. - The active
bossArenaonGameStatefor any boss-flavored affix that places anchors via the arena’s ring-points helper. - The
EnemyRaritytype from the enemy data module to drive the per-rarity affix-count distribution.
PUSHES TO
- Late-bound damage-player adapter (player health changes via the engine’s combat-damage path, never called directly here).
- Late-bound prop-spawn adapter (prop drops on affix death go through the world prop pool’s force-spawn entry).
GameMaster.spawnEnemyfor anchor and summoned-minion entities.engine/vfx/particlesfor every affix’s per-frame and on-death particle bursts (aura sparks, ring bursts, halo glints, inward-bias swirls, panic-blink overlays).engine/telemetry/collectorrecordDirectorPhasefor affix proc counts, drop outcomes, blocked-damage totals, and pairwise interaction events.- The active boss def’s
onPhaseChangedhook at body-swap time with the shared VFX kit and the 1-based transition index. - Mutations onto the replacement enemy entity during body-swap (HP, identity, affix list, ability list, phase index, silent-remove flag on the outgoing body).
- Direct mutations onto host
hpduring heals (regen tick and summoner heal-burst) and onto host affix state during interaction beats (panic-blink phasing timer reset, reflective-burst threshold halving on summoner proc).
DOES NOT
- Decide which enemy gets which affixes — only samples from a pool given a rarity / archetype / RNG; the spawner decides whether to roll at all.
- Pick the affix-roll archetype-bias map values from anywhere dynamic — the bias map is a static table in this module.
- Render anything. Particle emission is the only “draw” path and it writes to the particle pool, not to a renderer.
- Resolve hit detection or compute damage. Damage filtering only adjusts the incoming number; nothing here checks for collisions or applies damage to enemies.
- Decide the boss arena’s shape or radius — ring placement routes through the arena helper supplied on
GameState. - Drive boss phase scripting end-to-end — body-swap fires the boss def’s phase-change hook but does not own the phase plan; phase thresholds, next-types, and next-affix lists are carried in instance state by the caller.
- Manage the prop pool or enforce drop caps — drops fail silently when the pool is full; the boolean return from the adapter is only used to record telemetry.
- Apply chain-explosions, damage falloff, shield absorption, or boss damage caps — those live downstream of the damage-player adapter.
- Define the registered affix catalog itself — only consumes the array exported by the data module at boot.
Signals fired / Signals watched — none. All cross-system contact is by direct function call (spawner, particles, telemetry, boss def hook, damage adapter, prop-spawn adapter). No engine-signal subscribe or emit.
Entry points
registerAffix— insert a single def into the registry; throws on id collision.getAffix— strict lookup; throws on unknown id.applyAffixesOnSpawn/applyAffixesOnUpdate/applyAffixesOnDeath— priority-descending dispatch of the matching hook over every affix carried by a host.runDamageFilterChain— priority-descending damage-filter walk with zero-short-circuit; returns the surviving damage value.initAffixRegistry— idempotent boot-time catalog load; tolerates the data-not-yet-bound cycle path.rollEliteAffixes— per-spawn affix sampler given rarity, RNG, optional pool override, and optional archetype for the bias-weighted path.WORLD_ELITE_AFFIX_POOL— the in-module duplicate of the world-roaming id list kept here to break the catalog import cycle.RARE_AFFIX_DROP_RATE,BIAS_WEIGHT,ARCHETYPE_AFFIX_BIAS— exported tuning constants for the roll path.Rng— function type for the injectable RNG used by tests.getAffixVfxColor— palette lookup keyed by affix id; returns null for unknown or boss-only ids.AFFIX_VFX_PALETTE— exported color map consumed by the enemy-orbit halo renderer and the runtime hooks themselves.setAffixDamagePlayer/setAffixSpawnPropAt— adapter wiring entry points invoked once during engine boot.shieldedHooks/shieldedRespawnHooks/gatedHooks/respawnAsHooks/periodicInvulnHooks/reflectiveHooks/armoredHooks— boss-flavored runtime hook bags.burningAuraHooks/volatileHooks/regeneratingHooks/reflectiveBurstHooks/phasingHooks/summonerHooks/hardenedHooks/gravityWellHooks— world-roaming runtime hook bags.- Per-affix tuning constants exported from the palette module (aura radius / dps, volatile radius / damage, regen frac, reflective-burst threshold / radius / damage, phasing window / cycle, summoner threshold / minion type / count / spawn distance, hardened max-reduction / ramp duration, gravity-well pull radius / accel) and the shielded anchor / armored / shielded-respawn defaults exported from the runtime module.
Pattern notes
- Affix defs are pure data plus optional hook callbacks; the dispatcher orders them by descending
priorityon every pass so a high-priority filter can preempt the rest. - Hook bodies live in
runtime.ts, the registry and dispatch live inindex.ts, the catalog is built indata/affixes, and pure tunables / colors live inpalette.ts. The palette is its own leaf module so the orbit halo and tests can import colors without dragging in the runtime cycle. - The runtime ↔ data ↔ index import graph is a known circle; the data import sits at the end of
index.tsandinitAffixRegistryis called from the engine bridge after all modules have bound. Other entry paths see the registry empty and the init is a no-op. - The roll pool is duplicated in
roll.tsrather than imported fromdata/affixesto keep the roll path off the cycle; a test asserts the two lists agree at build time. - Cross-module engine calls inside hook bodies (damage to the player, prop spawning) route through late-bound adapters set during engine boot — calling a hook before wiring throws loudly.
- Hook state lives on the per-instance
AffixInstance.statebag; each affix has a typed read-and-hydrate helper that defaults missing values, validates types, and writes the hydrated values back. Misspelled keys crash vianumParam/strParam. - Affix-by-affix interactions are written inline inside the hook bodies of the participating affixes, gated by
hostHasAffixon the other affix’s id. There is no separate interaction registry — adding a new pair edits the relevant hooks directly. - The phase-swap path silently kills the outgoing enemy slot (
_silentRemove) to skip death VFX and kill signals, then transplants HP and identity onto the freshly spawned replacement so the boss bar reads continuously. - World-roaming affixes share a single proximity check (host-to-shared-ship squared distance) and rely on the shared
shipreference rather than threading the player through the call. - Anchor enemies are tagged with
_isBossAnchorand stripped of boss-only flags so the kill pipeline routes them through the anchor-destroyed signal rather than a boss-body kill. - On-death prop drops fail silently on a full pool; the telemetry call records the boolean outcome so cloud tuning can see drop suppression without changing runtime behavior.