Round 7 phase 1, contracts v9. Five surfaces for The Correction's rungs 2-4: - PARITY DEBT gauge in the top strip — five clinic-white bands, never red, never flashing, hidden until the debt first moves and visible forever after. - Notices are faxes: a white-letterhead tray above the commission fax. No unit name, no grid reference (pinned by a test that forbids digits outright), and deliberately not also a toast. Compliance quotas get a COMPLIANCE stamp. - Compliance seal panel + PRY, with the bar driven by SIM's own pry progress and the PROCESS bar standing down so that number can't be misread as a batch. - Stasis: hash-named countdown chips derived from the auditor's phase progress, and the abstention teach in the one window where it is still actionable. - MIRRORED joins the CORRECTED block as its own row, not the same counter. Verified live on :8153 by driving the real sim + real UI in the browser: a full warden cycle (seal -> PRY -> "SEAL REMOVED. THIS WILL BE NOTED." -> debt up) and a full auditor cycle (teach -> lock 744510A8 -> countdown -> auto-release). 263 UI tests, tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
6.5 KiB
TypeScript
163 lines
6.5 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 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<string, ItemDef>(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)])),
|
|
);
|
|
}
|
|
},
|
|
};
|
|
}
|