Shared-Health Boss Roster Pattern

The shared-health pattern lets a boss encounter spawn as multiple physical bodies that all drain a single boss HP bar. Damage dealt to any sharing body routes to the shared pool, so the player sees one bar but fights several enemies simultaneously.

The sharesHealthWithBoss flag

The mechanic is driven by the sharesHealthWithBoss: boolean field on EnemyEntity (declared in engine/core/types.ts). When true:

  • Damage applied to this body counts toward the boss bar pool.
  • The body is exempt from distance-based pruning and AI culling (sharers stay live no matter where the player is — Digbot Swarm has 100 bodies, many routinely off-screen).
  • Damage is capped per-hit at 30 + 0.02 × totalBarHpMax so a single sub-body can’t be chunked independently of the encounter’s real HP.
  • Non-player damage sources are ignored — sharers only take damage from the player (sourceKind === 'player').
  • On death, the body fires boss_body_kill (not the normal enemy_kill / streak / XP signals).

The flag is set per-entry on the boss roster (BossRosterEntry.sharesHealthWithBoss) and stamped onto each spawned enemy in engine/boss/encounter.ts → spawnBoss.

Bar pool = sum of live bodies

The HUD boss bar’s hpMax is computed by walking world.enemies and summing enemy.hpMax across every alive sharer (see getBossBarHpMax in engine/combat/damage.ts). The current bar value is the live sum of enemy.hp across the same set. The encounter ends when the last sharer dies — hasRemainingBossSharer returns false, which routes the final death through the encounter-end path.

Exactly one body per roster carries isBoss: true — the bar anchor. The anchor’s displayName and barColor drive the HUD label and bar tint. Other sharers can carry their own per-body displayName and barColor for inspection / sprite-tinting, but the HUD reads from the anchor.

Roster bosses currently shipping

Three bosses use the pattern today, each in a different shape:

Prism Cluster — 4-body symmetric

data/bosses/prism-cluster.ts. Four equal gems (Citrine, Ruby, Jade, Pearl) at the four cardinal points of a 400-radius circle arena. Each gem is 1,500 HP = 25% of the 6,000-HP bar pool. Citrine is the anchor (isBoss: true); the other three are sharers. Each gem fires its own beam_sweep ability with a 90° rotation phase offset.

Junkrat Captains — 2-body asymmetric with gate

data/bosses/junkrat-captains.ts. One Pierre (3,000 HP, 50% of bar, isBoss: true) plus two Marcos (1,500 HP each, 25% each). All three share the bar pool of 6,000 HP. Pierre carries the gated affix on gateGroupId='captains_marcos' — he is invulnerable to damage while either Marco is alive. The Marcos are flagged with the same gate group via affixState.gateGroup.gateGroupId so Pierre’s gated filter recognizes them as living gatekeepers.

Digbot Swarm — 100-body industrial mass

data/bosses/digbot-swarm.ts. One hundred bodies of 60 HP each (1% of bar pool per body). One Foreman (count: 1, isBoss: true) anchors the bar and owns the radial_burst ability; ninety-nine silent swarm bodies (count: 99, isBoss: false) carry no abilities. All hundred share the 6,000-HP pool. Position 'ring' distributes the 99 bodies around the Foreman; AI 'disperse_to_fill' spreads them across the arena.

Death cleanup

Bodies despawn on individual death (alive = false), but the boss encounter stays live until every sharer is dead. Each body death triggers the boss def’s onBodyDeath hook for per-body cinematic FX. The final sharer’s death triggers the def’s onDeath hook for the encounter-end cinematic, then onBossEncounterEnd('win', ...) fires the lump reward, drops the supply pod cascade at the death anchor, and culls any straggler bodies / boss-fired projectiles / arena terrain.

Asymmetric rosters (where the anchor isn’t necessarily the last to die — e.g. Prism Cluster, where Citrine the anchor may die before the other gems) are handled via a _lastBossDeathX/Y snapshot in damage.ts. The encounter-end portal and supply pod cascade anchor on the final sharer’s death position, not on the anchor body’s position.

Why this exists

Multi-body bosses give encounters distinct identities without forking the boss runtime — every shared-health roster reuses the same spawn / damage / teardown plumbing. A designer authors a new roster boss by adding entries to BossDef.roster with sharesHealthWithBoss: true and tuning per-body HP so the sum lands at the desired bar pool (~6,000-10,000 HP for the shipped tier). One body must be isBoss: true to anchor the HUD.

  • gameplay/concepts/boss-arena-positions.md — the 'center' | 'cardinal' | 'ring' | {x,y} position primitives used to lay out roster bodies.
  • gameplay/concepts/boss-phase-transitions.mdrespawn_as and other affix-driven body mutations.
  • engine/boss/encounter.tsspawnBoss (roster materialization), onBossEncounterEnd (teardown).
  • engine/combat/damage.tsgetBossBarHpMax, hasRemainingBossSharer, per-hit damage cap.
  • Spec: docs/superpowers/specs/2026-04-25-bosses-as-enemies-design.md §2.5, §3.6, §4.