engine/affixes/palette

Pure-data leaf module: per-affix halo colors plus the shared default tuning constants consumed by the affix runtime, the enemy-orbit halo, the world props layer, and the test suite.

PURPOSE

Hold the per-affix VFX palette and the numeric defaults for every world-roaming elite affix in one zero-import file. The split exists so consumers that only need a color tint or a single radius default don’t drag in the affix runtime chain (runtime.tsdata/bossesengine/affixes/indexdata/affixes → back into runtime). That chain is a real cycle that only resolves cleanly when entered through bridge.ts at boot; any other entry point sees partially-bound exports and crashes on hook references like shieldedHooks.onSpawn. Routing the palette through this leaf keeps lookups cycle-free.

OWNS

  • AFFIX_VFX_PALETTE — the color map. One { r, g, b } tint per world-roaming affix id: burning_aura, volatile, regenerating, reflective_burst, phasing, summoner, hardened, gravity_well. Boss-only affixes (shielded, shielded_respawn, gated, respawn_as, periodic_invuln, armored) are deliberately absent so they cannot trigger the world-elite halo.
  • getAffixVfxColor(affixId) — lookup function. Returns the matching { r, g, b } for any key in the palette; returns null for unknown or boss-only ids.
  • Burning-aura defaults: BURNING_AURA_DEFAULT_RADIUS (200 world px), BURNING_AURA_DEFAULT_DPS (6).
  • Volatile defaults: VOLATILE_DEFAULT_RADIUS (250 world px), VOLATILE_DEFAULT_DAMAGE (25).
  • Regenerating default: REGENERATING_DEFAULT_FRAC_PER_SEC (0.015 — 1.5% of max HP per second).
  • Reflective-burst defaults: REFLECTIVE_BURST_DEFAULT_THRESHOLD (12 hits), REFLECTIVE_BURST_DEFAULT_RADIUS (180 world px), REFLECTIVE_BURST_DEFAULT_DAMAGE (10).
  • Phasing defaults: PHASING_DEFAULT_WINDOW_DURATION (1.2 s of host invulnerability), PHASING_DEFAULT_CYCLE_INTERVAL (8 s window-to-window).
  • Summoner defaults: SUMMONER_DEFAULT_HP_THRESHOLD_FRAC (0.5 — proc when host crosses 50% HP), SUMMONER_DEFAULT_MINION_TYPE_ID ('orb_common'), SUMMONER_DEFAULT_MINION_COUNT (2), SUMMONER_DEFAULT_SPAWN_DISTANCE_PX (60).
  • Hardened defaults: HARDENED_DEFAULT_MAX_REDUCTION (0.5 — host takes 50% of incoming damage at full ramp), HARDENED_DEFAULT_RAMP_DURATION (30 s of host life to reach max reduction).
  • Gravity-well defaults: GRAVITY_WELL_PULL_RADIUS (150 world px), GRAVITY_WELL_PULL_ACCEL (60 world px/s²). See DOES NOT — both are currently dead.

READS FROM

Nothing. The file has no imports and depends on no other module. That isolation is the whole point of the split.

PUSHES TO

  • engine/affixes/runtime — imports AFFIX_VFX_PALETTE directly and references entries (AFFIX_VFX_PALETTE.burning_aura, .volatile, .reflective_burst, .regenerating, .gravity_well, .phasing, .summoner, .hardened) inside the per-affix hook implementations to emit colored VFX bursts and trails.
  • engine/vfx/enemy-orbit — imports getAffixVfxColor and calls it on each affix def attached to an enemy to pick the halo-ring tint for the orbit pass.
  • engine/world/props — imports getAffixVfxColor and calls getAffixVfxColor('burning_aura') to color the fire VFX on the Volatile Crystal afterburn proc.
  • data/affixes — re-exports the eight world-roaming default tuning constants (BURNING_AURA_DEFAULT_RADIUS/_DPS, VOLATILE_DEFAULT_RADIUS/_DAMAGE, REGENERATING_DEFAULT_FRAC_PER_SEC, REFLECTIVE_BURST_DEFAULT_THRESHOLD/_RADIUS/_DAMAGE) so authoring-side data tables can reference them without importing the runtime.
  • tests/engine/affixes/elite-affixes.test — imports getAffixVfxColor to verify every world-affix id resolves to a color and that unknown ids plus boss-only ids (shielded, armored, gated) return null.

DOES NOT

  • Does not import or depend on any other module. Adding any import would re-introduce the cycle the file was split off to avoid.
  • Does not own the affix definitions, hook implementations, or roll logic — those live in engine/affixes/runtime and engine/affixes/roll.
  • Does not host palette entries for boss-only affixes. shielded, shielded_respawn, gated, respawn_as, periodic_invuln, and armored have no color and resolve to null through getAffixVfxColor, which suppresses the world-elite halo on bosses.
  • Does not consume GRAVITY_WELL_PULL_RADIUS or GRAVITY_WELL_PULL_ACCEL. Both constants are defined and exported here but have no importers anywhere in the codebase. The gravity-well affix’s actual pull radius and acceleration are configured elsewhere; these two are dead constants kept for future reuse.
  • Does not validate that every entry in AFFIX_VFX_PALETTE corresponds to a real affix def, and does not validate the reverse. The palette and the affix def list are kept in sync by hand.

Signals

None. The file is pure data and a single synchronous lookup. No events, no telemetry, no side effects.

Entry points

  • AFFIX_VFX_PALETTEconst map; consumed by direct property access (e.g. AFFIX_VFX_PALETTE.burning_aura) in the runtime hooks, and by getAffixVfxColor for dynamic lookup.
  • getAffixVfxColor(affixId: string): { r: number; g: number; b: number } | null — string-keyed palette lookup used by the enemy-orbit halo renderer, the world props afterburn VFX, and the affix test suite.
  • All *_DEFAULT_* tuning constants — imported individually by engine/affixes/runtime for hook behavior and re-exported by data/affixes for authoring use.

Pattern notes

  • Pure-data leaf module with zero imports. Keep it that way. Any consumer that needs a color or a default radius should import from here, not from runtime.ts.
  • AFFIX_VFX_PALETTE is typed as const so each color entry is a readonly literal and the keys narrow to the union of affix ids — getAffixVfxColor uses keyof typeof AFFIX_VFX_PALETTE for the cast after the in check.
  • Boss-only affixes are kept out of the palette as the mechanism for suppressing the world-elite halo — there is no separate “is this a world affix?” flag. If a new world-roaming affix is added, its tint must be appended to AFFIX_VFX_PALETTE or the orbit halo will skip it.
  • The gravity_well tint (160, 90, 255 — violet) is intentionally matched to the Magnetar Pulse prop so the two related mechanics share a visual language. The cycle-comment in the runtime header preserves the same intent for any future reorganizing.
  • Constants are grouped by gameplay slice in source order (world-roaming → Slice 2 phasing/summoner/hardened → Slice 3 gravity-well). When adding a new affix, keep the slice grouping so the data history stays readable.
  • GRAVITY_WELL_PULL_RADIUS and GRAVITY_WELL_PULL_ACCEL are dead exports. Either wire them into the gravity-well hook in runtime.ts or delete them; do not add new dead constants alongside them.