The banner keyed on `out || ratio > 1`, so any time draw exceeded generation the panel screamed BROWNOUT — even while the buffer tanks were quietly covering it, the sim's flag was false and THE SCREEN was calm. That state is the factory working as designed, not failing. Only the HUD was panicking. BROWNOUT is now the sim's flag and nothing else. A covered deficit gets its own amber, steady, non-flashing state: "ON RESERVE · 34s", where the seconds are stored/(draw-gen) — real arithmetic, because v3 ruled stored is bandwidth-SECONDS and gen/draw are per-second. Same maths SCREEN uses. Four states (ok / tight / reserve / brownout) now live in a pure power.ts and are pinned by tests, including the exact case that lied: gen 100, draw 134, stored 1000, brownout false -> reserve, not brownout. Also uses v4 bandwidth.capacity to turn the buffer readout into a fuel gauge (0/900) rather than a bare number. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
/**
|
|
* LANE-UI — top strip: bandwidth meter, shipped ticker, run state.
|
|
*
|
|
* The brownout has to be legible from this panel alone — it is the one failure the
|
|
* player must read across the room, so it gets the border, the color, and the flash.
|
|
*/
|
|
import type { GameData, ItemDef, SimSnapshot } from '../contracts';
|
|
import { createChipRow, type ChipRow } from './chips';
|
|
import { cls, el, panel, style, text } from './dom';
|
|
import { formatReserve, powerState } from './power';
|
|
import { COPY } from './voice';
|
|
|
|
export interface TopStrip {
|
|
el: HTMLElement;
|
|
update(snap: SimSnapshot): void;
|
|
}
|
|
|
|
function stat(label: string, valueCls?: string): { row: HTMLElement; v: HTMLElement } {
|
|
const v = el('span', valueCls ? `fk-stat-v ${valueCls}` : 'fk-stat-v');
|
|
const row = el('div', 'fk-stat', [el('span', undefined, [label]), v]);
|
|
return { row, v };
|
|
}
|
|
|
|
export function createTopStrip(data: GameData): TopStrip {
|
|
const root = panel();
|
|
root.id = 'fk-top';
|
|
|
|
const head = el('div', 'fk-h', [COPY.bandwidth]);
|
|
|
|
const drawEl = el('span', 'fk-bw-draw');
|
|
const genEl = el('span');
|
|
const nums = el('div', 'fk-bw-nums', [drawEl, genEl]);
|
|
|
|
const fill = el('div', 'fk-bar-f');
|
|
const bar = el('div', 'fk-bar', [fill]);
|
|
|
|
const brownout = el('div');
|
|
brownout.id = 'fk-brownout';
|
|
brownout.textContent = COPY.brownout;
|
|
|
|
// Amber, not red: the factory is surviving on stored charge, not failing.
|
|
const reserve = el('div');
|
|
reserve.id = 'fk-reserve';
|
|
|
|
const buffer = stat('BUFFER', 'fk-buf');
|
|
const shipped = stat(COPY.shipped);
|
|
const run = stat(COPY.tick);
|
|
run.v.id = 'fk-run';
|
|
|
|
const items = new Map<string, ItemDef>(data.items.map((i) => [i.id, i]));
|
|
const chips: ChipRow = createChipRow(items);
|
|
|
|
root.append(head, nums, bar, brownout, reserve, el('div', 'fk-rule'),
|
|
buffer.row, shipped.row, run.row, chips.el);
|
|
|
|
return {
|
|
el: root,
|
|
update(snap) {
|
|
const { gen, draw, stored, capacity } = snap.bandwidth;
|
|
|
|
text(drawEl, `${draw.toFixed(0)} DRAW`);
|
|
text(genEl, `${gen.toFixed(0)} GEN`);
|
|
|
|
// BROWNOUT is the sim's flag and nothing else. A covered deficit is the tanks
|
|
// doing their job, and it gets its own amber word — see power.ts.
|
|
const p = powerState(snap.bandwidth);
|
|
style(fill, 'width', `${Math.min(1, p.ratio) * 100}%`);
|
|
cls(root, 'is-tight', p.state === 'tight');
|
|
cls(root, 'is-reserve', p.state === 'reserve');
|
|
cls(root, 'is-brownout', p.state === 'brownout');
|
|
|
|
if (p.state === 'reserve') {
|
|
text(reserve, `${COPY.onReserve} · ${formatReserve(p.reserveSeconds!)}`);
|
|
}
|
|
|
|
// Capacity (v4) turns the buffer readout into a fuel gauge rather than a number.
|
|
text(buffer.v, capacity ? `${stored.toFixed(0)}/${capacity.toFixed(0)}` : stored.toFixed(0));
|
|
|
|
let total = 0;
|
|
for (const n of Object.values(snap.shippedTotal)) total += n;
|
|
text(shipped.v, String(total));
|
|
|
|
text(run.v, snap.paused ? COPY.paused : `${snap.tick}`);
|
|
cls(run.v, 'is-paused', snap.paused);
|
|
|
|
chips.update(
|
|
Object.entries(snap.shippedTotal)
|
|
.filter(([, n]) => n > 0)
|
|
.map(([item, n]) => ({ item, count: String(n), state: 'met' as const })),
|
|
);
|
|
},
|
|
};
|
|
}
|