Boss Bar Resolution

The HUD boss bar is derived live every frame from world.enemies — never stored. Its identity (name + color) and its pool (current + max HP) come from two different surfaces: identity resolves through the anchor, the pool sums across every shared body. The two split lets multi-body bosses like Prism Cluster show one unified bar (“PRISM CLUSTER”) even though the HP pool spans several enemies, and lets the cap drop dynamically as sharing bodies die mid-fight.

Identity vs. pool — two separate resolutions

The HUD’s getBossBar(world) reads four fields and returns null when no sharer is alive:

FieldSourceHow it resolves
nameBar identityActive BossDef.displayName via game._activeBossDefId lookup. Falls back to the anchor’s enemy.displayName (the enemy flagged isBoss: true) if the def lookup misses. Final fallback: literal 'BOSS'.
colorBar identityActive BossDef.barColor. Same fallback chain through the anchor’s enemy.barColor, then '#ff4444'.
hpBar poolSum of enemy.hp across every alive enemy with sharesHealthWithBoss === true.
hpMaxBar poolSum of enemy.hpMax across the same alive sharers.

Anchor enemies (isBoss: true) carry the BossDef’s displayName so the fallback path renders the correct top-level name. Per-entry display names like "CITRINE" or "MARCO-A" exist on non-anchor sharers for inspection/debug only — the HUD never surfaces them.

The sharer-sum loop

Both the HUD’s getBossBar (hud.ts:80) and damage’s getBossBarHpMax (damage.ts:217) walk world.enemies and apply identical predicates before counting a body:

  1. enemy.alive === true
  2. enemy.sharesHealthWithBoss === true
  3. enemy._frozenForLag === false and enemy._dying === false
  4. enemy.hp > 0

A body that fails any predicate contributes nothing to bar HP or bar max. The cap drops as sharing bodies die — once the last living sharer fails the predicate, the bar disappears entirely (anySharer === false → return null).

This live-sum has two consequences:

  • Asymmetric rosters work transparently. Prism Cluster flags multiple gem bodies as anchors of equal weight; each contributes its hpMax to the pool, and damage to any gem reduces that gem’s own hp.
  • Damage caps scale with the live pool, not per-body. damage.ts uses getBossBarHpMax(world) * 0.02 + 30 as the per-hit cap on shared-health bodies. When sharing bodies die, the cap shrinks alongside the pool — preventing late-fight nuke spikes when only one sharer remains.

Two death signals — anchor vs. final sharer

The death path in damage.ts (lines 405–437) routes a dying boss-roster enemy through one of three branches based on flags:

Flag comboSignal fired
_isBossAnchor === true (anchor-only frame)boss_anchor_destroyed
sharesHealthWithBoss === true and other sharers still aliveboss_body_kill
sharesHealthWithBoss === true and final sharer (hasRemainingBossSharer returns false)boss_body_kill and boss_kill

The final-sharer branch also snapshots enemy.x/y into game._lastBossDeathX/_lastBossDeathY so the encounter-end portal lands on the killing blow’s position — required for asymmetric rosters where the last body to die isn’t the isBoss anchor (e.g. Prism Cluster’s Topaz outliving Citrine).

boss_kill is re-fired once by onBossEncounterEnd('win') carrying def.reward.xp, def.reward.currency, and def.id so the reward contract sees a guaranteed close. Listeners must be idempotent because boss_kill fires twice on a clean win: once at last-sharer death, once at encounter end.

Why two signals, not one

boss_anchor_destroyed and boss_kill exist as separate signals because anchor destruction and pool depletion aren’t the same event:

  • Anchor death without pool depletion — possible in scripted multi-anchor encounters. boss_anchor_destroyed lets cinematic hooks fire on the structurally significant body while combat continues against remaining sharers.
  • Pool depletion without isBoss anchor death — asymmetric rosters where the last sharer isn’t flagged isBoss. boss_kill fires on the final sharer regardless of which flag that body carried.

Anchor death also suppresses enemy_kill / streak / elite / XP signals (§6 ruling) — anchors are encounter-scale events, not roll-up combat kills.