From 4bcb942312b6160c2199803b0662a0425a7522bd Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 19 Jul 2026 18:05:44 +1000 Subject: [PATCH] [lane E] The biome meter: the reputation you're building, made visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A teal BIOME STANDING bar joins coat/hull in the HUD (matches the thin-cyan line-work; scale- only per the 60fps law). Hidden until the first `standing` event, so a flora-less level never shows a bar you can't fill. Brightens near full to telegraph an incoming phage ally. combat/index.js: rescaled standing to a clean 0..100 (tend +5, harm -35) — a FULL bar buds an ally and drops to a 50 recharge floor, so the meter reads as "charge it to summon help." First- pass numbers; tune to taste. Verified in-engine: tending flora reveals the bar and fills it proportionally (0.20 -> 0.45), teal, below coat/hull. Co-Authored-By: Claude Opus 4.8 --- web/js/combat/index.js | 11 +++++++---- web/js/ui/hud.js | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/web/js/combat/index.js b/web/js/combat/index.js index c76ce09..a435c79 100644 --- a/web/js/combat/index.js +++ b/web/js/combat/index.js @@ -76,15 +76,18 @@ export function createCombat({ scene, world, bus, rng, flags = {}, assets = null // (flora:harmed) to lose a chunk. At the symbiotic threshold the biome BUDS phage allies near // you that hunt your foes — the payoff. A `standing` event feeds a future HUD meter (Lane E). // ponytail: one run-scoped float, not a system. Persistence/tiers/L5-gate come when C needs them. + // Scaled 0..100 so E can meter it honestly: tending fills the bar, a full bar buds an ally + // and drops it to 50 (the recharge floor), harming flora empties a big chunk. Numbers are a + // first pass — C/playtest tunes the feel. let standing = 0, phageBudCd = 0; const setStanding = (v) => { standing = Math.max(0, Math.min(100, v)); bus.emit('standing', { value: standing }); }; - const offTended = bus.on('flora:tended', () => { setStanding(standing + 2); budPhage(); }); - const offHarmed = bus.on('flora:harmed', () => setStanding(standing - 25)); + const offTended = bus.on('flora:tended', () => { setStanding(standing + 5); budPhage(); }); + const offHarmed = bus.on('flora:harmed', () => setStanding(standing - 35)); function budPhage() { - if (standing < 40 || phageBudCd > 0) return; + if (standing < 100 || phageBudCd > 0) return; // a FULL bar buds an ally if (enemies.live.filter((e) => e.type === 'phage' && e.alive).length >= 3) return; // ally cap if (enemies.spawn('phage', { s: player.state.s + 8 })) { - setStanding(standing - 20); // spend standing to bud + setStanding(50); // recharge floor — the next ally is halfway earned phageBudCd = 3; // seconds between buds (decremented below) bus.emit('audio:cue', { name: 'pickup' }); // a friendly chirp when an ally is born } diff --git a/web/js/ui/hud.js b/web/js/ui/hud.js index f382205..015359a 100644 --- a/web/js/ui/hud.js +++ b/web/js/ui/hud.js @@ -14,6 +14,8 @@ const C = { warn: '#ffb13a', bad: '#ff5a2a', surf: '#b06aff', + ally: '#33ffbe', // teal — matches EMISSIVE.ally; the flora/phage reputation + allyHot: '#c9fff0', // near-full: an ally is imminent }; const RAIL_W = 190; // px; shared by the CSS and the blip transform below @@ -129,6 +131,15 @@ export function createHUD({ bus, flags = {}, level = null, mount = null } = {}) // --- bottom-left: what's keeping you alive -------------------------------------------- const coat = makeBar(bl, 'coat'); const hull = makeBar(bl, 'hull'); + // biome standing — the flora reputation. Fills as you tend flora; a full bar buds a phage + // ally (it drops to the recharge floor). Starts hidden and reveals on the first `standing` + // event, so a level with no flora never shows an empty bar the player can't act on. + const biomeRow = el('div', 'row', bl); + biomeRow.style.display = 'none'; + const biomeLbl = el('span', 'lbl', biomeRow); + biomeLbl.textContent = 'biome'; + const biomeRail = el('div', 'rail thin', biomeRow); + const biomeFill = el('div', 'fill', biomeRail); // --- bottom-right: what you're shooting with ------------------------------------------ const heat = makeBar(br, 'heat', true); @@ -195,6 +206,17 @@ export function createHUD({ bus, flags = {}, level = null, mount = null } = {}) offs.push(bus.on('combo', (e) => chain.set(e.n > 1 ? `×${e.n} chain` : ''))); + // 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 = ''; + offs.push(bus.on('standing', (e) => { + if (!biomeShown) { biomeRow.style.display = ''; biomeShown = true; } + const f = Math.max(0, Math.min(1, (e.value || 0) / 100)); + if (f !== lastStand) { biomeFill.style.transform = `scaleX(${f})`; lastStand = f; } + const col = f >= 0.9 ? C.allyHot : C.ally; + if (col !== lastStandC) { biomeRail.style.color = col; lastStandC = col; } + })); + offs.push(bus.on('level:event', (ev) => { if (ev.type === 'checkpoint' && ev.name) where.set(ev.name); }));