Review: 5/5 lanes pass. 160/160 tests, tsc clean, M1 COMPLETE — verified live: reference factory ships melt, commission queue advances with standing orders, SCREEN's new artifact passes respond item-specifically (block 0.46 from bricks). Contracts v3 (granted): EntityState.scrammed, SimSnapshot.commissionQueue, MachineDef.coolPerTick, setRecipeAt command, Sim.save/load, ScreenFX.frame(t, snap), BeltItem.id hardened required, MachineDef.color re-ruled as chassis/body. main.ts: passes snapshot to screen.frame; dev __fktryDemo() builds reference factory. happy-dom ACTUALLY installed this time (round-2 grant never landed; UI caught it). Rulings: bandwidth units are per-second (SIM converts); heat retune ordered (nothing can currently overheat - heatPerTick < ambient cooling); validator test moves to data/; RENDER's color deviation approved as the ruling; scram wipe 3 frames; REMOVE button approved. Round 3 orders in all lane docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
16 KiB
LANE-UI — HUD, build tools, and the voice of the game
You are an Opus 4.8 executor on the UI lane of FKTRY. Read ../MASTERPLAN.md and
../CONTRACTS.md first; the world bible is ../../docs/FKTRY_LORE.md. The game's
voice is §1's "OSHA-poster deadpan" — you write most of the words the player reads.
Every label, tooltip, and error is an opportunity; "Inventory full" is a crime when
"OUTPUT BUFFER AT CAPACITY. THIS IS A YOU PROBLEM." is available.
Mission: everything DOM. Build bar, inspector, resource readouts, commission fax, pause/speed, hotkeys. Plain TypeScript + DOM — no framework, no CSS libs.
Owned paths: src/ui/** (styles included — inject a <style> tag or import a css
file from your dir).
Never touch: contracts, main.ts, other lanes. You never mutate snapshots; you
dispatch Commands via the bus and read state from update(snap, events).
Standing rules (all rounds):
#uiroot haspointer-events:none; every interactive child you create re-enables it. Clicks must fall through to the game world everywhere else — test this.- Keyboard map lives in one file (
hotkeys.ts) — it will grow. - Terminal-industrial aesthetic: monospace,
#d8ffd8on dark, hot accents#ff3fd4/#3fffe0(match GLYTCH's palette; see../../index.htmlfor reference). No rounded corners. This is machinery, not a startup.
CURRENT ORDERS — Round 3 (goal: close the interaction loop, verify the pressure UI live)
Round 2 review: 57 tests against a 10 minimum, and factoring pure modules instead of
touching root config when my grant turned out to be vapor — model behavior. About
that: happy-dom genuinely is installed now (verified: package.json line 17, and
your 57 tests pass under --environment happy-dom). My round-2 "it's installed" was
wrong and your BLOCKED section caught it — good catch, keep doing that.
Verdicts: CONTRACT REQUEST 1 GRANTED — SimSnapshot.commissionQueue in v3, SIM wires
it this round. Request 2 was already landing as you wrote it (parallel-round race;
the queue advances at HEAD — I watched PATIO RESURFACING arrive live). REMOVE
PROPOSAL: APPROVED.
- REMOVE: your design — suggested shape: X hotkey / bar button arms remove mode,
hover shows RENDER's demolition tint (they're building it this round — coordinate
via NOTES), click dispatches
remove, Esc disarms. Toast in house voice ("UNIT RECLAIMED. THE FLOOR REMEMBERS."). - NEXT IN TRAY becomes truth: read
snap.commissionQueuewhen present, deletepeekNextinference the moment SIM lands it (keep fallback until then). - DOM render tests: the two you named as needing a real DOM (chip rendering,
attach()binding) plus fax stamp lifecycle —// @vitest-environment happy-dom. - Live pressure verification (now unblocked):
window.__fktryDemo()builds SIM's reference factory in one call — use it. Verify with your own eyes and screenshot in NOTES: heat bar + THROTTLING + SCRAM on the software decoders once DATA's retune lands, SANITISING chip on real anchor shipments, STANDING ORDER stamp on a real repeat completion (I verified the stamp renders — you verify the full lifecycle). - Stretch, skippable: read-only tech panel (
T) renderingtech.jsonas an era tree marked RESEARCH OFFLINE — research mechanics land in M3, the panel can exist first.
Definition of done: a misplaced machine is mouse-removable; queue peek reads truth; the three DOM tests exist; NOTES carry screenshots of heat/scram/SANITISING observed live.
NOTES (append-only log, newest at bottom)
### Round N — YYYY-MM-DD — [your model]
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
Round 1 — 2026-07-17 — Opus 4.8
SHIPPED: All five ordered surfaces, in src/ui/**, verified in a browser (screenshots
taken of each). Modules: index.ts (root/wiring), style.ts (injected CSS), voice.ts
(all player-facing copy), hotkeys.ts (the keymap, one file as ordered), buildbar.ts,
topstrip.ts, inspector.ts, fax.ts, toasts.ts, chips.ts (shared item chip),
selection.ts (writes bus._sel), palette.ts, pick.ts, dom.ts, fixture.ts (dev).
- Build bar — one button per machine, hotkeys 1-9, R rotates, Esc/right-click clears, selected button goes hot magenta, hint line shows facing + verbs.
- Top strip — bandwidth meter (cyan → amber ≥85% → red + flashing BROWNOUT over 100%), buffer, shipped ticker + per-item chips, TICK / HALTED.
- Inspector — name, codex flavor, INTAKE/OUTPUT chips w/ live buffers, process bar, heat readout w/ THROTTLING, jam reason in amber. Esc or X closes.
- Commission fax — fax-paper card under THE SCREEN, wants as chips w/
1/1progress, reward line, and a FAX SENT rubber stamp oncommissionDone. - Toasts — max 4, 6s TTL, colored per event kind, house voice.
VERIFIED: npm run check clean; npm test 70/70 (other lanes' suites). Dev server on
:8151 (8150 was another lane's session). Confirmed with own eyes: hotkey 1 → bus._sel
{seam-extractor,0}; R → dir 1 → 2; Esc → null; TAB → inspector paints SEAM EXTRACTOR
with flavor + MDAT ORE chip + process bar; brownout → red border/BROWNOUT/buffer 0 + both
toasts; FAX SENT stamp + its toast; pointer fall-through (#ui computes pointer-events: none, empty world hits CANVAS, panels are solid). No console errors.
DECISIONS:
- All player-facing strings live in
voice.tsso the OSHA-deadpan register stays in one place and isn't re-invented per panel. - The UI keeps its own flattened
roster(pick.ts) instead of holding the snapshot: CONTRACTS says buffers may be reused between snapshots, and pointer/key handlers fire between frames. The backing array is reused so 500 entities don't allocate per frame. - Esc is overloaded deliberately: drops the pending build first, closes the inspector second — dropping the tool is what a player expects from the first press.
BLOCKED/BROKEN (read this part):
- Click-to-inspect does not work.
ui.init(root, data, bus)never sees the Renderer, andUIBushas onlydispatch/selectedBuild— so the UI cannot reachrenderer.pickTile, which is the only thing that knows the camera. LANE-RENDER has a workingpickTilethis round; I just can't call it.pick.tsopportunistically probeswindow.__fktryRenderer.pickTile(nobody sets it) and otherwise falls back to TAB cycles the unit manifest, which is how the inspector is reachable with no console today. Tile→entity hit-testing (incl. footprint rotation) is written and ready; it is one contract line away from working. See CONTRACT REQUEST 1. - LANE-DATA shipped 21 machines; hotkeys only cover 9. Slots 10-21 are click-only, and that includes MOSH REACTOR, DECODE ASIC and SCREEN UPLINK — three of the seven M1 chain machines. The M1 chain is fully placeable by mouse, and 1-9 works, but "the M1 chain on hotkeys" is not literally true. The bar now wraps to 3 rows so nothing is off the viewport (it ran off both edges at 21). See PROPOSAL.
- No DOM tests for
src/ui. Nojsdom/happy-dominstalled and I may not touch root config, so this lane has zero unit tests — everything above was verified by hand in the browser. See CONTRACT REQUEST 4. ?uifixtureis dev scaffolding I wrote (fixture.ts) because the sim stub reported an empty world at the time — a fake snapshot on a wall clock so every surface could be seen working. It is inert without the query param and dispatches nothing. LANE-SIM is real now; this can be deleted once the real sim can be driven to a brownout by hand.- Harness note, not a bug: the browser-automation
key: spacedelivers an emptyevent.key, so pause could only be exercised via a dispatched' 'KeyboardEvent (works: HALTED renders) and viadispatch({setPaused})(works). Real spacebar sends' 'per spec; the handler matches that.
CONTRACT REQUEST:
UIBus.pickTile(clientX, clientY): Vec2 | null(or pass theRenderertoui.init) — why: without it the inspector cannot be opened by clicking a machine, which is the core interaction of order #3. Renderer already implements it; only the plumbing is missing. This is my only real blocker.MachineDef.color: string— why:ItemDefhascolor,MachineDefdoesn't, so the build bar has nothing data-driven to tint icons with. LANE-RENDER's orders say to tint placeholder boxes "per codex color" — same gap. Right nowpalette.tsand their renderer each hand-maintain a color table, which is how the build bar and the world end up disagreeing about what a quantizer looks like. Data should own it once.MachineDef.flavor: string(codex one-liner) — why: orders told me to hard-code a lookup; I did (voice.ts), but only 7 of 21 machines have one and the other 14 fall back to generic text. This is LANE-DATA's material, not mine.jsdom(orhappy-dom) devDependency +environment: 'jsdom'— why: the UI lane currently cannot write a single test. Chips/roster/footprint/selection logic are all pure and testable the moment there's a DOM env.
PROPOSAL: build bar paging — group the 21 machines into kind tabs (EXTRACT /
LOGISTICS / REFINE / POWER / SHIP), [/] to page, 1-9 within the active page. Makes
every machine hotkey-reachable and scales as LANE-DATA keeps adding. Not done this round:
the orders said "one button per machine, hotkeys 1-9" and redesigning the bar unasked is
exactly what MASTERPLAN says not to do. Say the word and it's a small round-2 change.
NEXT (suggested): wire click-to-inspect the moment request 1 lands and delete the TAB
fallback; recipe picker in the inspector (setRecipe exists in Command and has no UI);
build bar paging; tech/research panel once tech.json is populated (it's []).
Round 2 — 2026-07-17 — Opus 4.8
SHIPPED: All five orders. New: pages.ts (pagination), chipspec.ts (pure chip
arithmetic), six *.test.ts. Deleted: fixture.ts and the TAB-cycle fallback, as ordered.
- Click-to-inspect via
bus.pickTile. Thewindow.__fktryRendererprobe and the TAB fallback are gone. - Build bar paging — kind tabs (EXTRACT / LOGISTICS / REFINE / POWER / SHIP),
[/], 1-9 within the page. Selecting from anywhere pulls the bar to the owning page. - Inspector — recipe picker (
setRecipe), heat bar w/ THROTTLING/SCRAM states, jam wording viajamText(). - Fax — NEXT IN TRAY peek, STANDING ORDER stamp on
repeat, SANITISING chip on shipped anchor-slabs. - 57 tests (min was 10), all green.
VERIFIED (against the real sim, in a browser, on a static vite build+preview —
see BLOCKED for why not the dev server): built a SEAM EXTRACTOR by mouse → DRAW 0→4 with
no generator → real BROWNOUT, red panel, buffer 0, underrun toast, THE SCREEN
visibly corrupting. Clicked the unit → inspector opened on entity 1 (pickTile →
{18,-7} → entityAt). Placed a SOFTWARE DECODER → GEN 0→55, brownout lifted, "BUFFER
RESTORED. RESUME SINNING." Recipe picker: options rendered as readable processes ("1
LUMA BARS → 1 COEFFICIENT PACK + 1 HF DUST"), selecting IDLE dispatched
setRecipe{entity:1,recipe:null} and the sim round-tripped it back as "UNIT IS BEING
PAID TO EXIST"; selecting back dispatched recipe:'quantize'. Starved wording live in
both toast and inspector: "QUANTIZER: INTAKE STARVED. NOTHING IS ARRIVING." Paging walk
confirmed all 21 machines across 6 pages (1+3+9+5+2+1) with no page over 9 slots.
#ui still computes pointer-events:none. npm run check clean; 160/160 tests.
DECISIONS:
- Kind tabs alone could not satisfy "every machine hotkey-reachable at any catalog
size" — REFINE holds 14 crafters and 1-9 reaches nine. So a page is capped at 9 and an
oversized kind spills into numbered pages (REFINE 1/2, REFINE 2/2). The invariant the
order asked for now holds for any catalog;
pages.test.tspins it at 1/9/14/40 crafters. - Logic was pulled out of the DOM modules into
pages.ts/chipspec.ts/ a purecreateKeyRouter, which is why there are 57 tests without happy-dom (below). - Data-first everywhere:
MachineDef.flavorand.colorwin, round-1 hand-transcribed tables stay as fallback. LANE-DATA's flavor is what's on screen now. - Unknown jam reasons pass through uppercased rather than being swallowed, so a new sim stall reason shows up honestly instead of silently reading as generic.
BLOCKED/BROKEN (read this part):
happy-domis NOT installed — order 5 says "GRANTED — happy-dom is installed"; it is not inpackage.jsonand not innode_modules(npm ls happy-dom→ empty). I did not add it: root config is orchestrator-owned and editing it is a hard rule. Instead I factored the testable logic into pure modules and wrote 57 node-env tests covering everything order 5 named except the two that genuinely need a DOM: chip rendering (the arithmetic is covered inchipspec.test.ts) andattach()'s window binding (the routing is covered inhotkeys.test.ts). Install it and I'll add the render tests.- STANDING ORDER stamp is built but unobservable. Data has 3
repeatcommissions (dust-allowance, correction-buyback, melt-retainer) but the sim pins the active commission tocommissions[0](first-taste, not repeat) and never advances —commissionDoneEmittedlatches forever. So nothing can ever reach the stamp. Code is data-driven and will light up the moment the sim rotates commissions. - NEXT IN TRAY is an inference, not truth. The snapshot has
activeCommissionbut no queue, so "next" = the following entry in data order. It renders (NEXT IN TRAY → DUST ALLOWANCE) but if the sim ever picks non-sequentially the peek will lie. See CONTRACT REQUEST 1. - Heat bar unverified live. Only
software-decoderhasheatPerTick(0.0025), and it heats only while crafting; a decoder with no load sits at heat 0, so the bar correctly stays hidden and I never saw THROTTLING/SCRAM. Thresholds are built to CONTRACTS (>0.7 throttle, 1.0 scram). Needs a fed chain to confirm. - SANITISING chip unverified live — needs anchor-slabs actually shipped, which needs
the full M1 chain fed. Renders off
shippedTotal, so it is one shipment away. - The dev server is unusable for verification. Other lanes are editing
render/andscreen/continuously; vite logged 16 page reloads in ~10 minutes, wiping the world mid-test every time. I verified onvite build+vite previewinstead. Not a complaint — just: if a round asks for hands-on verification, expect this. - During the round I hit LANE-SCREEN's
glitchStack.tsand LANE-RENDER'sindex.tsmid-edit and broken (tsc errors, dead rAF loop). Both self-resolved.src/uiwas clean throughout; the one failing test at the time was LANE-SIM's ownsim.test.ts. - Harness note (unchanged): browser-automation
key: spacesends an emptyevent.key; space verified via a dispatched' 'event. Real spacebar sends' 'per spec.
CONTRACT REQUEST:
SimSnapshot.commissionQueue: string[](ornextCommission: string | null) — why: NEXT IN TRAY currently guesses from data order. Low cost, removes a UI lie.- Sim: advance/rotate commissions and honour
repeat— not strictly a contract change, a SIM ask:commissions[0]forever means 13 of 14 commissions and all 3 standing orders are unreachable, and the fax's whole queue UI is decorative. happy-domactually installed (see BLOCKED) — the grant didn't land in package.json.
PROPOSAL: the inspector should get a REMOVE button (remove exists in Command with
no UI, so a misplaced machine currently can't be undone by mouse at all). Small, and it
closes the last build-verb gap.
NEXT (suggested): remove-by-mouse; heat/scram + SANITISING confirmation once a chain
can be fed; commission queue UI becomes real once requests 1-2 land; tech panel when
tech.json fills (still []).