[lane E] The boss banner: five fights stop being guesswork

boss.js has emitted boss:start / boss:phase / boss:window / boss:telegraph / boss:end since the
framework landed, and NOTHING has ever listened. Every boss in the game was therefore illegible:
the Pyloric Guardian's whole design is "shoot only while the iris relaxes", and the only tell a
player had was a node's flash uniform going white-hot — a cue nobody is told to read.

A top-centre banner, fed entirely by events the boss already broadcasts:
  - the boss's NAME (five of them; there was no way to know what you were fighting)
  - phase pips, filling as you break each phase
  - the state: SEALED / opening (blinking, off boss:telegraph) / VULNERABLE in green
The Sovereign's Communion reports `phases: 0`, so a symbiotic player gets "stand down" and no
pip rail rather than the promise of a battle they have just earned their way out of.

Verified in the Guardian's lair: banner on, "PYLORIC GUARDIAN / VULNERABLE", 3 pips, state
tracking boss.open exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-26 19:51:04 +10:00
parent b9d9d816ad
commit caa5f95339

View File

@ -31,6 +31,18 @@ const CSS = `
.hud .bl { bottom:18px; left:18px; } .hud .bl { bottom:18px; left:18px; }
.hud .br { bottom:18px; right:18px; align-items:flex-end; } .hud .br { bottom:18px; right:18px; align-items:flex-end; }
.hud .bc { bottom:18px; left:50%; transform:translateX(-50%); align-items:center; } .hud .bc { bottom:18px; left:50%; transform:translateX(-50%); align-items:center; }
.hud .tc { top:14px; left:50%; transform:translateX(-50%); align-items:center; gap:6px;
opacity:0; transition:opacity .35s ease; pointer-events:none; }
.hud .tc.on { opacity:1; }
.hud .boss-name { font-size:13px; letter-spacing:.30em; color:${C.bad}; }
.hud .boss-state { font-size:9.5px; letter-spacing:.26em; opacity:.85; }
.hud .boss-state.open { color:${C.good}; }
.hud .boss-state.shut { color:${C.warn}; opacity:.55; }
.hud .boss-state.tele { color:${C.warn}; animation:bosstele .5s steps(2,end) infinite; }
@keyframes bosstele { 50% { opacity:.25 } }
.hud .phase { display:flex; gap:5px; }
.hud .phase i { width:16px; height:3px; background:currentColor; opacity:.22; }
.hud .phase i.on { opacity:1; }
.hud .dim { opacity:.5; } .hud .dim { opacity:.5; }
.hud .row { display:flex; align-items:center; gap:8px; white-space:nowrap; } .hud .row { display:flex; align-items:center; gap:8px; white-space:nowrap; }
.hud .lbl { font-size:9px; opacity:.65; letter-spacing:.2em; } .hud .lbl { font-size:9px; opacity:.65; letter-spacing:.2em; }
@ -107,6 +119,17 @@ export function createHUD({ bus, flags = {}, level = null, mount = null } = {})
const br = el('div', 'q br', root); const br = el('div', 'q br', root);
const bc = el('div', 'q bc', root); const bc = el('div', 'q bc', root);
// --- top-centre: the boss banner ------------------------------------------------------
// boss.js has emitted boss:start/phase/window/telegraph/end since the framework landed and
// NOTHING has ever listened. That made every fight illegible: the Pyloric Guardian's entire
// design is "shoot only while the iris relaxes", and the only tell was a node's flash uniform.
// This is the same information the boss already broadcasts, made visible.
const tc = el('div', 'q tc', root);
const bossName = makeText(tc, 'boss-name');
const phaseWrap = el('div', 'phase', tc);
const bossState = makeText(tc, 'boss-state');
let phasePips = [];
// --- top-left: where we are ----------------------------------------------------------- // --- top-left: where we are -----------------------------------------------------------
const where = makeText(tl, 'big'); const where = makeText(tl, 'big');
where.set(level?.name ?? '—'); where.set(level?.name ?? '—');
@ -210,6 +233,34 @@ export function createHUD({ bus, flags = {}, level = null, mount = null } = {})
offs.push(bus.on('combo', (e) => chain.set(e.n > 1 ? `×${e.n} chain` : ''))); offs.push(bus.on('combo', (e) => chain.set(e.n > 1 ? `×${e.n} chain` : '')));
// --- boss banner ----------------------------------------------------------------------
const BOSS_NAMES = {
mast_cell: 'MAST CELL', pyloric_guardian: 'PYLORIC GUARDIAN', tapeworm: 'TAENIA',
the_blockage: 'THE BLOCKAGE', appendix_sovereign: 'THE SOVEREIGN',
};
const setState = (txt, cls) => {
bossState.set(txt);
bossState.node.className = `boss-state ${cls}`;
};
offs.push(bus.on('boss:start', (e) => {
bossName.set(BOSS_NAMES[e?.id] ?? 'CONTACT');
// The Sovereign's Communion is a boss that resolves to no fight at all — announcing phases
// for it would promise a battle the player has just earned their way out of.
const n = Math.max(0, e?.phases ?? 0);
phaseWrap.textContent = '';
phasePips = Array.from({ length: n }, () => el('i', null, phaseWrap));
if (phasePips[0]) phasePips[0].classList.add('on');
setState(n ? 'sealed' : 'stand down', n ? 'shut' : 'open');
tc.classList.add('on');
}));
offs.push(bus.on('boss:phase', (e) => {
const p = (e?.phase | 0);
phasePips.forEach((el_, i) => el_.classList.toggle('on', i < p));
}));
offs.push(bus.on('boss:window', (e) => setState(e?.open ? 'VULNERABLE' : 'sealed', e?.open ? 'open' : 'shut')));
offs.push(bus.on('boss:telegraph', () => setState('opening', 'tele')));
offs.push(bus.on('boss:end', () => tc.classList.remove('on')));
// biome standing — reveal on first event, then scale-only (60fps law). Brightens near full // biome standing — reveal on first event, then scale-only (60fps law). Brightens near full
// to telegraph an incoming ally. // to telegraph an incoming ally.
let biomeShown = false, lastStand = -1, lastStandC = ''; let biomeShown = false, lastStand = -1, lastStandC = '';