From caa5f95339bd41369e5f55959bd65a4177e5d25a Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 26 Jul 2026 19:51:04 +1000 Subject: [PATCH] [lane E] The boss banner: five fights stop being guesswork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- web/js/ui/hud.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/web/js/ui/hud.js b/web/js/ui/hud.js index d3f7fc9..ca832f9 100644 --- a/web/js/ui/hud.js +++ b/web/js/ui/hud.js @@ -31,6 +31,18 @@ const CSS = ` .hud .bl { bottom:18px; left:18px; } .hud .br { bottom:18px; right:18px; align-items:flex-end; } .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 .row { display:flex; align-items:center; gap:8px; white-space:nowrap; } .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 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 ----------------------------------------------------------- const where = makeText(tl, 'big'); 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` : ''))); + // --- 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 // to telegraph an incoming ally. let biomeShown = false, lastStand = -1, lastStandC = '';