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:
- Old host is silently marked
alive=falsewith_silentRemove=trueso it produces no death VFX orboss_body_killsignal. - A replacement enemy is spawned at the same position using the configured
nextTypes[i]enemy-type id. - HP /
hpMax/ bar identity (isBoss,sharesHealthWithBoss,displayName,barColor) transplant onto the new body for bar continuity. - The replacement’s affixes and abilities are swapped to the matching
nextAffixIds[i]/nextAbilityIds[i]entries (usingcreateAbilityInstanceso per-abilitystartDelayis respected). replacement.phaseIndexis stamped (1-based, post-increment) so the spawn-profilephasetrigger can fire its tied wave.- The active
BossDef’sonPhaseChanged(host, newPhaseIndex, world, arena, kit)hook fires (1-based index —1is 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.
nextAbilityIdsswaps the entire ability list. Each new ability respects its own configuredstartDelayandtelegraphMs. - AI / behavior. Via the new enemy type’s registered behavior.
- Affix loadout.
nextAffixIdscan layer or drop affixes per phase (e.g. terminal phase dropsrespawn_asso 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
onPhaseChangedhook is the place tokill()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 viasetColor/setAlpha. - Telegraph + transition cinematic.
onPhaseChangedis 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+ addsorbitalRing, phase 2 addsruneCircle+edgeVignette,floorGlowStripesbrightens to signal activehazard_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 fireonBodyDeath(visual lattice snap + recoil sparks), andonDeathfires once when the final gem dies. Norespawn_as, noonPhaseChanged— useful as a contrast to confirm phase transitions are body-swap on threshold cross, not body-death.
Source references
engine/affixes/runtime.ts:319-468—respawn_asaffix definition,readRespawnAsState,swapHostBody,onPhaseChangeddispatch.engine/boss/encounter.ts:42-44—getSharedBossVfxKit()accessor used by the phase-change hook to re-bind layers.data/bosses/index.ts:147-165—BossDef.onPhaseChanged/setupVfx/onBodyDeath/onDeath/onUpdatehook contracts.data/bosses/awakened-mech.ts— three-phase reference implementation with full transition + death cinematic.data/spawn-profiles.ts:95-121—crescendoprofile,phasetrigger wiring.