Boss Phase Transitions

Multi-phase bosses escalate by crossing HP-fraction thresholds. At each crossing the host body is swapped for a new enemy type, ability/affix loadouts flip, and a transition cinematic fires. Phase index 0 is the encounter-start body; phases 1+ activate as the bar drains past each declared threshold.

Mechanism

Phase transitions are driven by the respawn_as affix (engine/affixes/runtime.ts). On every tick the affix samples the host’s hp / hpMax fraction and compares it against the next pending threshold. The moment the current frac drops at or below the threshold (prev > t && curr <= t), the runtime fires swapHostBody:

  1. Old host is silently marked alive=false with _silentRemove=true so it produces no death VFX or boss_body_kill signal.
  2. A replacement enemy is spawned at the same position using the configured nextTypes[i] enemy-type id.
  3. HP / hpMax / bar identity (isBoss, sharesHealthWithBoss, displayName, barColor) transplant onto the new body for bar continuity.
  4. The replacement’s affixes and abilities are swapped to the matching nextAffixIds[i] / nextAbilityIds[i] entries (using createAbilityInstance so per-ability startDelay is respected).
  5. replacement.phaseIndex is stamped (1-based, post-increment) so the spawn-profile phase trigger can fire its tied wave.
  6. The active BossDef’s onPhaseChanged(host, newPhaseIndex, world, arena, kit) hook fires (1-based index — 1 is the first transition).

Multiple thresholds can fire in one frame if a single hit deals a large chunk of HP — the runtime walks the threshold list in order until no more crossings match.

Declaration shape

A phase-shift boss declares its phase ladder on the boss-anchor roster entry via the respawn_as affix state.phases:

{
  phases: {
    thresholds: number[],          // descending HP fractions, e.g. [0.66, 0.33]
    nextTypes: string[],           // enemyTypeId per threshold (aligned by index)
    nextAffixIds?: string[][],     // optional per-phase affix loadout
    nextAbilityIds?: string[][],   // optional per-phase ability loadout
  }
}

thresholds[i], nextTypes[i], nextAffixIds[i], and nextAbilityIds[i] are index-aligned: entry i is the body to swap into on the ith crossing. The respawn_as affix must itself be present in the next phase’s nextAffixIds entry for chained transitions to work — without it the replacement body has no state to detect later crossings.

What a phase can change

  • Sprite / enemy type. The replacement uses a different enemyTypeId, driving a sprite-identity swap automatically (Loader → Drillbot → Bigbot).
  • Active ability. nextAbilityIds swaps the entire ability list. Each new ability respects its own configured startDelay and telegraphMs.
  • AI / behavior. Via the new enemy type’s registered behavior.
  • Affix loadout. nextAffixIds can layer or drop affixes per phase (e.g. terminal phase drops respawn_as so no further transitions arm).
  • Persistent VFX. Host-attached layers (aura, corePulse, orbitalRing, runeCircle, edgeVignette) are dangling after the swap because the old host is dead. The onPhaseChanged hook is the place to kill() the old layer set and re-bind a fresh set on the new host. Arena-scoped layers (dustMotes, floorGlowStripes) persist across swaps; their colors can be mutated via setColor / setAlpha.
  • Telegraph + transition cinematic. onPhaseChanged is where the boss stages its armor-shed beat — time-slow push, camera zoom punch, shockwave + shards burst in the old palette, screen tint pulse, then cross-fade into the new palette.

Spawn-profile phase triggers

Spawn profiles (data/spawn-profiles.ts) can tie add-waves to phase crossings via trigger: { kind: 'phase', index: N }. The crescendo profile fires waves on indices 1 and 2, matching the host’s stamped phaseIndex. Phase 0 is the encounter-start body, so the first phase-tied wave is index 1 (the first threshold crossing).

Example — Awakened Mech with thresholds [0.66, 0.33] runs the crescendo profile:

  • t=0: 2 gunners at cardinals (time trigger)
  • Phase 1 (66% HP): 4 gunners on a ring
  • Phase 2 (33% HP): 6 gunners on a ring

BossDef hook contract

onPhaseChanged?(
  host: EnemyEntity,
  newPhaseIndex: number,    // 1-based: 1 = first transition, 2 = second, ...
  world: WorldState,
  arena: BossArena,
  kit: VfxLayerKit,
): void;

The hook is visual only — bar drain, body identity, ability swap, and phase counter are all handled by the respawn_as runtime before this fires. Typical responsibilities:

  • Kill old host-attached VFX layer handles, re-bind for the new phase.
  • Stage the transition cinematic (time-slow, zoom, shockwave, shards, tint pulses).
  • Mutate arena-scoped layer colors / alphas to match the new palette.

The kit reference is the shared encounter kit (getSharedBossVfxKit()) — the same instance setupVfx used at encounter start.

Reference implementations

  • Awakened Mech (data/bosses/awakened-mech.ts): three-phase escalation on a single sharing body. 8000 HP total, thresholds [0.66, 0.33], Loader → Drillbot → Bigbot. Each phase swaps the active ability (aimed volley → spiral vortex → cone slam), the palette (cool steel → orange heat → white-hot), and the persistent VFX layer set (phase 1+ adds orbitalRing, phase 2 adds runeCircle + edgeVignette, floorGlowStripes brightens to signal active hazard_pads). 1.4-second armor-shed cinematic on each transition; 3.0-second triple-blast death.

  • Prism Cluster (data/bosses/prism-cluster.ts) is not a phase boss — it’s a four-body equal-share roster (Citrine / Ruby / Jade / Pearl, 25% bar each). Per-gem deaths fire onBodyDeath (visual lattice snap + recoil sparks), and onDeath fires once when the final gem dies. No respawn_as, no onPhaseChanged — useful as a contrast to confirm phase transitions are body-swap on threshold cross, not body-death.

Source references

  • engine/affixes/runtime.ts:319-468respawn_as affix definition, readRespawnAsState, swapHostBody, onPhaseChanged dispatch.
  • engine/boss/encounter.ts:42-44getSharedBossVfxKit() accessor used by the phase-change hook to re-bind layers.
  • data/bosses/index.ts:147-165BossDef.onPhaseChanged / setupVfx / onBodyDeath / onDeath / onUpdate hook contracts.
  • data/bosses/awakened-mech.ts — three-phase reference implementation with full transition + death cinematic.
  • data/spawn-profiles.ts:95-121crescendo profile, phase trigger wiring.