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_REGISTRY and its lookup / registration entry points (collision check on insert, throw-on-unknown lookup, idempotent catalog init from the data module).
  • The AffixDef contract: 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.random default.
  • 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 state bag 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/types for AffixInstance, EnemyEntity, GameState, WorldState, ShipState, BossArena.
  • engine/core/state for the shared ship reference used by every world-roaming affix’s proximity gate and the damage-adapter call.
  • engine/enemies/spawner GameMaster for anchor and minion spawns.
  • engine/abilities for the next-phase ability instance constructor used in the body-swap path.
  • engine/boss/encounter for the shared boss VFX kit handed to the phase-change hook.
  • engine/vfx/particles Particles for every affix’s particle emission.
  • engine/telemetry/collector for proc, drop, blocked-damage, and interaction telemetry.
  • data/bosses BOSS_DEFS for the active boss def looked up at phase transition.
  • data/affixes for the catalog list consumed by the registry init.
  • The host EnemyEntity.affixes array (per-instance state, def-id back-reference).
  • The host’s hp, hpMax, alive, _frozenForLag, _dying, typeId, radius, x, y fields plus boss / anchor / sharing flags used by readers and the body-swap.
  • The active bossArena on GameState for any boss-flavored affix that places anchors via the arena’s ring-points helper.
  • The EnemyRarity type 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.spawnEnemy for anchor and summoned-minion entities.
  • engine/vfx/particles for every affix’s per-frame and on-death particle bursts (aura sparks, ring bursts, halo glints, inward-bias swirls, panic-blink overlays).
  • engine/telemetry/collector recordDirectorPhase for affix proc counts, drop outcomes, blocked-damage totals, and pairwise interaction events.
  • The active boss def’s onPhaseChanged hook 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 hp during 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 priority on every pass so a high-priority filter can preempt the rest.
  • Hook bodies live in runtime.ts, the registry and dispatch live in index.ts, the catalog is built in data/affixes, and pure tunables / colors live in palette.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.ts and initAffixRegistry is 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.ts rather than imported from data/affixes to 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.state bag; each affix has a typed read-and-hydrate helper that defaults missing values, validates types, and writes the hydrated values back. Misspelled keys crash via numParam / strParam.
  • Affix-by-affix interactions are written inline inside the hook bodies of the participating affixes, gated by hostHasAffix on 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 ship reference rather than threading the player through the call.
  • Anchor enemies are tagged with _isBossAnchor and 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.