/** * 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 type { Corrections } from './corrections'; import { createDebtGauge } from './debtgauge'; import { cls, el, panel, style, text } from './dom'; import { formatReserve, powerState } from './power'; import { lockChipText, type StasisLocks } from './stasis'; import { COPY } from './voice'; export interface TopStrip { el: HTMLElement; /** `corrections` is the session tally of The Correction's edits — surfaced only once * the first mite has struck, so it never signposts an enemy that hasn't arrived. * `locks` (v9) are the hash auditor's open sector locks, shown as countdown chips. */ update(snap: SimSnapshot, corrections?: Corrections, locks?: StasisLocks): 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(data.items.map((i) => [i.id, i])); const chips: ChipRow = createChipRow(items); // CORRECTED ticker (round 6): a count + per-item chips of what the mites have "fixed". // Hidden until the first edit, so pre-Correction play never sees the enemy named. // Round 7 splits it into two rows under one heading — see the MIRRORED row below. const corrected = stat(COPY.correctedLabel, 'fk-corrected-v'); cls(corrected.row, 'fk-corrected', true); const correctedChips: ChipRow = createChipRow(items); const correctedRow = el('div', 'fk-corrected-row', [corrected.row, correctedChips.el]); // MIRRORED row (v9): shipments a redundancy gunship put on the sky ledger before you // did. DECISION (orders left it to me): a separate row, not the same counter — a mite // editing your melt and a gunship out-shipping you are different crimes, and one // number that adds them up would be a worse readout than two that don't. const mirrored = stat(COPY.mirroredLabel, 'fk-mirrored-v'); cls(mirrored.row, 'fk-mirrored', true); const mirroredChips: ChipRow = createChipRow(items); const mirroredRow = el('div', 'fk-mirrored-row', [mirrored.row, mirroredChips.el]); style(mirroredRow, 'display', 'none'); const correctedWrap = el('div', 'fk-corrected-wrap', [correctedRow, mirroredRow]); style(correctedWrap, 'display', 'none'); // PARITY DEBT (v9) + the auditor's open sector locks. const gauge = createDebtGauge(); const lockChips = el('div', 'fk-locks'); style(lockChips, 'display', 'none'); root.append(head, nums, bar, brownout, reserve, el('div', 'fk-rule'), buffer.row, shipped.row, run.row, chips.el, correctedWrap, gauge.el, lockChips); return { el: root, update(snap, corrections, locks) { 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 })), ); const edits = corrections?.total() ?? 0; const mirrors = corrections?.mirroredTotal() ?? 0; style(correctedWrap, 'display', edits > 0 || mirrors > 0 ? '' : 'none'); style(correctedRow, 'display', edits > 0 ? '' : 'none'); if (edits > 0) { text(corrected.v, String(edits)); correctedChips.update( corrections!.byItem().map(({ item, count }) => ({ item, count: String(count), state: 'short' as const, })), ); } style(mirroredRow, 'display', mirrors > 0 ? '' : 'none'); if (mirrors > 0) { text(mirrored.v, String(mirrors)); mirroredChips.update( corrections!.mirroredByItem().map(({ item, count }) => ({ item, count: String(count), state: 'short' as const, })), ); } // PARITY DEBT — hidden until the debt first moves; the gauge owns that latch. gauge.update(snap.correction); // Sector locks: one countdown chip per open lock, named by hash. The contract // guarantees release, so this is a wait, not a task. const open = locks?.active() ?? []; style(lockChips, 'display', open.length ? '' : 'none'); if (open.length) { lockChips.replaceChildren( el('div', 'fk-h', [COPY.stasisChip]), ...open.map((l) => el('div', 'fk-lock', [lockChipText(l, snap.correction, snap.tick, COPY.stasisHeld)])), ); } }, }; }