The five M1 surfaces, replacing the stub: - buildbar.ts: one button per machine, hotkeys 1-9, R rotates, Esc or right-click clears. LANE-DATA shipped 21 machines, so the bar wraps rather than running off both edges of the viewport; slots 10-21 are click-only for now (paging proposed in NOTES). - topstrip.ts: bandwidth meter going amber at 85% and red + flashing BROWNOUT over 100%, buffer, shipped ticker + per-item chips, tick or HALTED. The brownout is legible from this panel alone, which is the one failure the player has to read across the room. - inspector.ts: name, codex flavor, intake/output chips with live buffers, process bar, heat, jam reason. Holds an entity id and re-looks-up each frame, so a removed unit closes itself. - fax.ts: the commission card with wants-as-chips and a FAX SENT stamp on commissionDone. - toasts.ts: max 4, 6s TTL, colored per event kind. Click-to-inspect is not wired: the UI cannot reach renderer.pickTile (UIBus exposes only dispatch/selectedBuild, and ui.init never sees the Renderer). TAB cycles the unit manifest as a stopgap so the inspector is reachable without a console. Contract request filed in NOTES. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
/**
|
|
* LANE-UI — the commission fax. The fax machine is sacred, do not ask why it works
|
|
* (lore §6). Sits under THE SCREEN, top right.
|
|
*
|
|
* Progress comes from `snap.commissionProgress` (itemId -> count delivered), measured
|
|
* against the active CommissionDef's `wants`.
|
|
*/
|
|
import type { CommissionDef, GameData, ItemDef, SimSnapshot } from '../contracts';
|
|
import { createChipRow, type ChipRow } from './chips';
|
|
import { cls, el, panel, text } from './dom';
|
|
import { COPY } from './voice';
|
|
|
|
export interface Fax {
|
|
el: HTMLElement;
|
|
update(snap: SimSnapshot): void;
|
|
/** Play the stamp. Driven by the `commissionDone` event, not by progress math. */
|
|
stamp(): void;
|
|
}
|
|
|
|
const STAMP_MS = 1800; // must match the fk-stamp keyframes in style.ts
|
|
|
|
export function createFax(data: GameData): Fax {
|
|
const commissions = new Map<string, CommissionDef>(data.commissions.map((c) => [c.id, c]));
|
|
const items = new Map<string, ItemDef>(data.items.map((i) => [i.id, i]));
|
|
|
|
const root = panel();
|
|
root.id = 'fk-fax';
|
|
|
|
const head = el('div', 'fk-h fk-fax-h', [COPY.faxHeader]);
|
|
const sub = el('div', 'fk-fax-sub', [COPY.faxSubheader]);
|
|
const flavor = el('div', 'fk-fax-flavor');
|
|
const chips: ChipRow = createChipRow(items);
|
|
const reward = el('div', 'fk-fax-sub');
|
|
|
|
const stampEl = el('div', 'fk-fax-stamp', [el('span', undefined, [COPY.faxStamp])]);
|
|
|
|
root.append(head, sub, flavor, el('div', 'fk-rule'), chips.el, reward, stampEl);
|
|
|
|
let stampTimer: number | undefined;
|
|
|
|
return {
|
|
el: root,
|
|
update(snap) {
|
|
const c = snap.activeCommission ? commissions.get(snap.activeCommission) : null;
|
|
cls(root, 'is-open', !!c);
|
|
if (!c) return;
|
|
|
|
text(flavor, c.flavor);
|
|
chips.update(
|
|
Object.entries(c.wants).map(([item, need]) => {
|
|
const have = snap.commissionProgress[item] ?? 0;
|
|
return {
|
|
item,
|
|
count: `${Math.min(have, need)}/${need}`,
|
|
state: have >= need ? ('met' as const) : ('plain' as const),
|
|
};
|
|
}),
|
|
);
|
|
text(reward, `ON RECEIPT: +${c.rewardBandwidth} BANDWIDTH`);
|
|
},
|
|
stamp() {
|
|
// Restart cleanly if two commissions complete back to back.
|
|
cls(stampEl, 'is-on', false);
|
|
void stampEl.offsetWidth; // force reflow so the animation re-runs
|
|
cls(stampEl, 'is-on', true);
|
|
clearTimeout(stampTimer);
|
|
stampTimer = setTimeout(() => cls(stampEl, 'is-on', false), STAMP_MS);
|
|
},
|
|
};
|
|
}
|