glytch/fktry/src/ui/fax.ts
type-two c18309f464 [ui] build bar, top strip, inspector, commission fax, toasts
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>
2026-07-17 16:15:50 +10:00

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