[lane E] The biome meter: the reputation you're building, made visible

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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-19 18:05:44 +10:00
parent 71b0663b77
commit 4bcb942312
2 changed files with 29 additions and 4 deletions

View File

@ -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
}

View File

@ -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);
}));