Supersedes unexecuted round-5 orders. Folds in the live first-eyes review findings (silence, void floor, onboarding cliff, quiet payoffs) + John's hidden-radio idea. Contracts v6: AudioFX (THE SPEAKER - all Web Audio synthesis, no asset files), RelicState + excavate + relicFound (the optical-fossil hidden pipe: find it and the game grows a tuner - FKTRY FM, Correction propaganda subtitled by THE SCREEN's chyron, a shortwave numbers station reading hidden-seam coordinates, dead air). main.ts wires audio + first-gesture unlock; src/audio stub awaits LANE-SCREEN. Lane orders: SIM = wildlife + seams-become-real + the relic + research time; RENDER = the floor (seam glints, strata whispers), relic shimmer (no signposting), juice, break-room cabinet -> match.html; UI = mercy toast, tuner dock, SCREEN click-to-enlarge, camera hints; SCREEN = era feed + THE SPEAKER (3 audio layers, 4 stations, spectral verification); DATA = the commission-ladder tutorial, stations.json broadcast schedule, seams.json final. Ship gate: after review, redeploy partly.party/glytch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
474 lines
32 KiB
Markdown
474 lines
32 KiB
Markdown
# 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):**
|
||
- `#ui` root has `pointer-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, `#d8ffd8` on dark, hot accents `#ff3fd4` /
|
||
`#3fffe0` (match GLYTCH's palette; see `../../index.html` for reference). No rounded
|
||
corners. This is machinery, not a startup.
|
||
|
||
---
|
||
|
||
## CURRENT ORDERS — ROUND 5 (MEGA) — supersedes the earlier round-5 text
|
||
|
||
Review verdicts you're executing: first-click punishment without guidance, camera
|
||
controls taught nowhere, THE SCREEN's payoff too quiet, and one suspect tab click.
|
||
Plus the radio — the game's sound is a *discovery*, and you build its face.
|
||
|
||
### PART A — carried from round 5
|
||
1. **Scrolling tab strip** (your design, approved).
|
||
2. **Publish inspect mode**: inspector open → `setSelection({mode:'inspect',
|
||
entity})`; clear on close; Esc order preserved (drop tool → close inspector).
|
||
3. **Wildlife surfacing**: infestation toasts in voice; MOSQUITO EXPOSURE line +
|
||
speed penalty in the inspector; one hatch-warning toast per pile, not a nag.
|
||
4. **Migrate research tests** to `grantResearch`; **verify RENDER's demolition tint
|
||
+ inspect highlight with screenshots** (third round owed — close it).
|
||
|
||
### PART B — onboarding + the radio face (new)
|
||
5. **First-brownout mercy toast** (once per session): "NOTHING IS GENERATING. THE
|
||
COMMITTEE SUGGESTS: POWER." — fires on the first brownout event only. The alarm
|
||
currently teaches nothing; this is the one hint the opening needs.
|
||
6. **Camera hint** in the idle line ("MMB/WASD PAN · WHEEL ZOOM") and a **tab
|
||
hitbox check**: in review, a direct click on the POWER tab did nothing once
|
||
(keyboard paging worked; may be harness — reproduce with a real mouse, fix or
|
||
file it as harness with evidence).
|
||
7. **New jam voice**: `'no seam'` lands from SIM — give it the line it deserves
|
||
("SEAM EXTRACTOR: THERE IS NOTHING DOWN THERE. MOVE.").
|
||
8. **THE SCREEN click-to-enlarge**: click toggles a centered ~2.5× overlay (CSS
|
||
transform + backdrop dim), Esc/click closes. The sky is the score; let people
|
||
look at it.
|
||
9. **THE TUNER DOCK (the radio's face).** Hidden until `relicFound`/`relics.found`.
|
||
Then: a dock slides in (bottom-right) — frequency dial (drag + arrow keys), band
|
||
AM/FM/SW, volume + mute, station readout in voice ("88.1 FKTRY FM — SIGNAL
|
||
LOCKED"). Controls talk to THE SPEAKER via its exported `radio` control (narrow
|
||
sanctioned import from `src/audio` — SCREEN documents the API; coordinate in
|
||
NOTES). Discovery ceremony: relicFound → "AUDIO BUS RESTORED. THE BITSTREAM HAS
|
||
A VOICE." toast + dock slide-in. Radio state persists per session only (the
|
||
fossil's `found` is the durable part, sim-side).
|
||
|
||
**Definition of done:** cold-start first-brownout shows the mercy toast; tuner dock
|
||
appears only after excavation and drives the real audio engine; enlarge verified;
|
||
tint/highlight screenshots in NOTES; tab click adjudicated.
|
||
|
||
---
|
||
|
||
## 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).
|
||
|
||
1. 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.
|
||
2. Top strip — bandwidth meter (cyan → amber ≥85% → red + flashing BROWNOUT over 100%),
|
||
buffer, shipped ticker + per-item chips, TICK / HALTED.
|
||
3. Inspector — name, codex flavor, INTAKE/OUTPUT chips w/ live buffers, process bar,
|
||
heat readout w/ THROTTLING, jam reason in amber. Esc or X closes.
|
||
4. Commission fax — fax-paper card under THE SCREEN, wants as chips w/ `1/1` progress,
|
||
reward line, and a FAX SENT rubber stamp on `commissionDone`.
|
||
5. 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.ts` so 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,
|
||
and `UIBus` has only `dispatch`/`selectedBuild` — so the UI cannot reach
|
||
`renderer.pickTile`, which is the only thing that knows the camera. LANE-RENDER has a
|
||
working `pickTile` this round; I just can't call it. `pick.ts` opportunistically probes
|
||
`window.__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`.** No `jsdom`/`happy-dom` installed 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.
|
||
- `?uifixture` is 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: space` delivers an empty
|
||
`event.key`, so pause could only be exercised via a dispatched `' '` KeyboardEvent
|
||
(works: HALTED renders) and via `dispatch({setPaused})` (works). Real spacebar sends
|
||
`' '` per spec; the handler matches that.
|
||
|
||
**CONTRACT REQUEST:**
|
||
1. **`UIBus.pickTile(clientX, clientY): Vec2 | null`** (or pass the `Renderer` to
|
||
`ui.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.
|
||
2. **`MachineDef.color: string`** — *why:* `ItemDef` has `color`, `MachineDef` doesn'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 now `palette.ts` and 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.
|
||
3. **`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.
|
||
4. **`jsdom` (or `happy-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.
|
||
|
||
1. **Click-to-inspect** via `bus.pickTile`. The `window.__fktryRenderer` probe and the
|
||
TAB fallback are gone.
|
||
2. **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.
|
||
3. **Inspector** — recipe picker (`setRecipe`), heat bar w/ THROTTLING/SCRAM states,
|
||
jam wording via `jamText()`.
|
||
4. **Fax** — NEXT IN TRAY peek, STANDING ORDER stamp on `repeat`, SANITISING chip on
|
||
shipped anchor-slabs.
|
||
5. **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.ts` pins it at 1/9/14/40 crafters.
|
||
- Logic was pulled out of the DOM modules into `pages.ts` / `chipspec.ts` / a pure
|
||
`createKeyRouter`, which is *why* there are 57 tests without happy-dom (below).
|
||
- Data-first everywhere: `MachineDef.flavor` and `.color` win, 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-dom` is NOT installed** — order 5 says "GRANTED — happy-dom is installed"; it
|
||
is not in `package.json` and not in `node_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 in `chipspec.test.ts`) and `attach()`'s window binding (the
|
||
routing is covered in `hotkeys.test.ts`). Install it and I'll add the render tests.
|
||
- **STANDING ORDER stamp is built but unobservable.** Data has 3 `repeat` commissions
|
||
(dust-allowance, correction-buyback, melt-retainer) but the sim pins the active
|
||
commission to `commissions[0]` (`first-taste`, not repeat) and never advances —
|
||
`commissionDoneEmitted` latches 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 `activeCommission` but 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-decoder` has `heatPerTick` (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/` and
|
||
`screen/` continuously; vite logged **16 page reloads** in ~10 minutes, wiping the world
|
||
mid-test every time. I verified on `vite build` + `vite preview` instead. Not a
|
||
complaint — just: if a round asks for hands-on verification, expect this.
|
||
- During the round I hit LANE-SCREEN's `glitchStack.ts` and LANE-RENDER's `index.ts`
|
||
mid-edit and broken (tsc errors, dead rAF loop). Both self-resolved. `src/ui` was clean
|
||
throughout; the one failing test at the time was LANE-SIM's own `sim.test.ts`.
|
||
- Harness note (unchanged): browser-automation `key: space` sends an empty `event.key`;
|
||
space verified via a dispatched `' '` event. Real spacebar sends `' '` per spec.
|
||
|
||
**CONTRACT REQUEST:**
|
||
1. **`SimSnapshot.commissionQueue: string[]`** (or `nextCommission: string | null`) —
|
||
*why:* NEXT IN TRAY currently guesses from data order. Low cost, removes a UI lie.
|
||
2. **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.
|
||
3. **`happy-dom` actually 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 `[]`).
|
||
|
||
### Round 3 — 2026-07-17 — Opus 4.8
|
||
|
||
**SHIPPED:** Orders 1-4. Order 5 (tech panel) skipped — see BLOCKED. New: `live.test.ts`
|
||
(real sim + real HUD), `chips.test.ts`, `hotkeys.dom.test.ts`, `fax.test.ts`. **105 UI
|
||
tests** (was 57); full suite 238/238; `npm run check` clean.
|
||
|
||
1. **REMOVE mode** — `X` or the REMOVE button beside the tabs arms it (button goes hazard
|
||
red, hint turns red, `[ ] PAGE · X REMOVE` in the idle hint). Click demolishes, Esc
|
||
stands down, and it *stays armed* between clicks because demolition is usually plural.
|
||
Toast: "UNIT RECLAIMED. THE FLOOR REMEMBERS." Clicking open ground does nothing, so a
|
||
stray click never reads as a broken tool.
|
||
2. **NEXT IN TRAY reads truth** — `snap.commissionQueue[1]`. The data-order inference is
|
||
**deleted**, not kept: SIM *is* publishing the field (see BLOCKED for the race that
|
||
nearly made me miss it). No queue → no peek. A fax that lies quietly is worse than one
|
||
that says nothing.
|
||
3. **DOM tests** — chip rendering, `attach()`/`detach()` window binding + typing guards,
|
||
fax stamp lifecycle (incl. back-to-back stamps not clearing early), all under
|
||
happy-dom.
|
||
4. **Live pressure verification** — see VERIFIED.
|
||
|
||
**VERIFIED — live, in the browser** (screenshots taken; `__fktryDemo()` = 118 commands):
|
||
- The reference factory builds and runs: DRAW 94 / GEN 180, buffer charging, belts moving,
|
||
starved units toasting in house voice.
|
||
- **REMOVE end-to-end:** armed the mode (button red, `bus._sel` = `{def:'__remove'}`),
|
||
clicked the mosh reactor → **gone from the world**, "UNIT RECLAIMED. THE FLOOR
|
||
REMEMBERS." toast, **DRAW dropped 94 → 86** (its draw left with it), inspector
|
||
auto-closed because its entity vanished, mode still armed. That's the round's DoD.
|
||
- Inspector on a real MOSH REACTOR: DATA's flavor, PROCESS SELECT reading
|
||
"1 GOP CRATE → 3 MELT + 1 I-FRAME (ANCHOR SLAB)", INTAKE `GOP CRATE 0/1` amber, OUTPUT
|
||
chips, "INTAKE STARVED. NOTHING IS ARRIVING."
|
||
- Paging tabs + NEXT IN TRAY (DUST ALLOWANCE) + REMOVE all legible in one bar.
|
||
|
||
**VERIFIED — headless, against the REAL sim** (`live.test.ts`; boots `createSim()` +
|
||
`createUI()` on the real `data/*.json`, builds `referenceFactory()`, pumps ticks through
|
||
the same `update(snap, events)` path main.ts uses, then reads the actual DOM):
|
||
- **Heat → THROTTLING → SCRAM** on a software decoder, inspected via the real
|
||
pickTile→entityAt→open click path. Also pinned: the unit **still reads SCRAM while
|
||
cooling back below 1.0** — the exact case `heat >= 1` got wrong, now read off
|
||
`EntityState.scrammed` per contracts v3.
|
||
- **SANITISING on real anchor-slab shipments** (21 of them) — see BLOCKED for why this
|
||
needed a machine swap.
|
||
- **STANDING ORDER on a real repeat completion**: the factory runs, SIM's queue advances,
|
||
`dust-allowance` (repeat:true) reaches the head, the fax stamps it, and NEXT IN TRAY
|
||
matches `commissionQueue[1]` exactly.
|
||
- **REMOVE against the real sim**: entity count drops by one, the id is gone, toast fires,
|
||
stays armed, Esc disarms, open ground is a no-op.
|
||
|
||
**DECISIONS:**
|
||
- **The `__remove` ghost sentinel (LANE-RENDER: this is the hook — please read).** The UI
|
||
has no Renderer reference; the only channel to the ghost is `bus.selectedBuild()`, which
|
||
main.ts feeds to `setGhost`. So while remove is armed, `_sel` reads
|
||
**`{ def: '__remove', dir: 0 }`** — key your demolition tint off that def id. It is safe
|
||
because sim's place handler is `const def = defs.get(cmd.def); if (def) ...`, so main.ts's
|
||
click-to-place fires a `place` for `__remove`, matches no machine, and does nothing; the
|
||
real removal is dispatched by the UI. Exported as `REMOVE_DEF` from `src/ui/selection.ts`
|
||
and pinned by a test. A magic string is not how I'd design this — CONTRACT REQUEST 1.
|
||
- **Build-bar icons now follow the two-material rule** (codex §8), prompted by the v3
|
||
ruling that `color` is chassis, not accent. DATA's chassis values are all desaturated
|
||
neutrals — correct for the world, but 21 icons in `#4a4e52`/`#3f4348` is an unreadable
|
||
grey wall. So an icon is body (data `color`) + one emissive element **derived from the
|
||
machine's first output item colour** — the same derivation LANE-RENDER uses, so bar and
|
||
world agree. My round-1 id/kind tables were never wrong, only mislabeled (the same
|
||
mistake the v2 contract made); they now serve as the accent fallback for machines that
|
||
output nothing (belts, buffers, uplinks).
|
||
- Inspector reads `EntityState.scrammed`, not `heat >= 1`. Re-deriving SIM's hysteresis
|
||
here would silently desync the moment they retune `HEAT_RESTART` — exactly what
|
||
LANE-RENDER flagged in their round-2 notes.
|
||
|
||
**BLOCKED/BROKEN (read this part):**
|
||
- **The reference factory can never ship anchor-slabs, so SANITISING can't fire from
|
||
`__fktryDemo()` as the order assumed.** It recycles them on purpose — reference.ts:
|
||
*"Recovered anchor slabs come back out of the reactor. Rather than sell them, feed the
|
||
i-only assembler."* Confirmed by probe: 12k ticks ships `hf-dust`, `chroma-slurry`,
|
||
`macroblock-bricks`, `melt` — zero slabs, with `dct-press` sitting on `output full`. So
|
||
I verified it the way a player would: demolish the i-only assembler at (-6,5), drop an
|
||
uplink on it → **21 slabs ship, SANITISING lights up**. That's what `live.test.ts` does.
|
||
Nobody's bug; the order's premise just didn't hold.
|
||
- **Live verification of heat/scram/SANITISING in the browser is not practically
|
||
possible, and it's the harness, not the code.** The browser pane keeps the tab
|
||
backgrounded (`document.hidden === true`), which pauses `requestAnimationFrame`, so the
|
||
page only advances **~10 ticks per screenshot**. SCRAM needs ~435 ticks of continuous
|
||
generation and slabs need thousands — that's hundreds of captures, while parallel lanes'
|
||
HMR reloaded the page **~16 times an hour**, wiping the world mid-test (it ate four of my
|
||
attempts). Chrome-MCP was no better: it couldn't reach the dev server
|
||
(`chrome-error://`) and reported `hidden: true` too. Hence `live.test.ts` — same sim,
|
||
same DOM, same code path, deterministic, milliseconds, and it stays as a regression test.
|
||
I'd rather hand you that than a screenshot I had to fight the harness for.
|
||
- **I nearly reported `commissionQueue` as unpublished.** My first grep for
|
||
`snap.commissionQueue` found nothing and I wrote the fallback on that basis; SIM landed
|
||
it minutes later (parallel-round race — same thing that happened with `flavor`/`color`
|
||
last round). A probe against the real sim caught it. Worth noting as a pattern: in this
|
||
operation, "not landed yet" has a short shelf life, and greps at round-start lie by
|
||
round-end.
|
||
- **Order 5 (tech panel) skipped.** `tech.json` is **still `[]`** — an era tree rendering
|
||
an empty array is a panel that says nothing, and the order marked it skippable. It's
|
||
~30 lines whenever DATA fills the file.
|
||
- The `?showroom` / demolition-tint coordination with LANE-RENDER is one-way so far: I've
|
||
defined the sentinel above and pinned it, but they hadn't landed remove-tint when I
|
||
finished, so **the hover tint is unverified from my side**. If they key off a different
|
||
string, remove mode still works — it just won't glow.
|
||
|
||
**CONTRACT REQUEST:**
|
||
1. **`UIBus.setGhostMode(mode: 'build' | 'remove' | null)`** (or let `setGhost` take a
|
||
mode) — *why:* remove-mode hover currently rides a magic def id through
|
||
`selectedBuild()`, and main.ts fires a junk `place` command on every demolition click
|
||
that only works because the sim happens to ignore unknown defs. It's two lanes agreeing
|
||
on a string in a NOTES file. One typed field kills it.
|
||
2. **`MachineDef.accent?: string`** — *why:* deriving the icon accent from the first
|
||
output item is a decent heuristic, but it's a heuristic, and LANE-RENDER is running the
|
||
same one independently. If art direction ever wants a machine's glow to differ from its
|
||
product, there's nowhere to say so. (LANE-RENDER asked for this in round 2 as well.)
|
||
|
||
**PROPOSAL:** `?uidemo` — a URL param that runs `__fktryDemo()` on boot. Trivial, in my
|
||
lane, and it would have saved most of an hour this round: every HMR reload wiped the world
|
||
and needed a manual re-dispatch. Say the word.
|
||
|
||
**NEXT (suggested):** tech panel the moment `tech.json` has entries; verify RENDER's
|
||
demolition tint once it lands; SANITISING's real fiction (The Correction's buyback) when
|
||
M3 arrives — the hardcoded `anchor-slab` id should become data by then.
|
||
|
||
### Round 4 — 2026-07-17 — Opus 4.8
|
||
|
||
**SHIPPED:** All five orders. New: `power.ts` + `power.test.ts`, `research.ts` +
|
||
`research.test.ts`, `techpanel.ts` + `techpanel.test.ts`. **145 UI tests** (was 105);
|
||
`npm run check` clean.
|
||
|
||
1. **The BROWNOUT bug is fixed.** BROWNOUT is now the sim's flag and nothing else. A
|
||
covered deficit gets its own amber, steady, non-flashing state: **"ON RESERVE · 34s"**,
|
||
from `stored/(draw−gen)` — the same arithmetic SCREEN drives dread with, and honest
|
||
because v3 ruled `stored` is bandwidth-SECONDS. Four states now: ok / tight / reserve /
|
||
brownout, all pure in `power.ts` and pinned by tests, including the exact case that was
|
||
lying (`gen 100, draw 134, stored 1000, brownout:false` → reserve, **not** brownout).
|
||
Bonus: the buffer readout is a fuel gauge now (`0/900`) using v4 `bandwidth.capacity`.
|
||
2. **Selection protocol migrated.** Writes go through `bus.setSelection()`, `REMOVE_DEF`
|
||
is deleted, and **my `remove` dispatch is gone** — main.ts owns it. Verified live: one
|
||
click, one removal, no double-fire. A legacy fallback remains for a host that doesn't
|
||
serve `setSelection` (it's optional in the contract) and is tested.
|
||
3. **Research UI.** `T` opens the STANDARDS COMMITTEE: every tech grouped by era in era
|
||
order, cost chips in item colours, era prefix stripped from node names, `UNLOCKS:`
|
||
tooltips in machine names. Pick → `setResearch`; picking the active one stands it down;
|
||
ratified nodes are inert. Locked machines in the bar are greyed + padlocked with
|
||
`REQUIRES: <TECH>`, can't be picked by click *or* hotkey, and the padlock falls off in
|
||
the same frame `researched` lands. `researched` → "STANDARD RATIFIED. NEW CRIMES
|
||
AVAILABLE."
|
||
4. **`?uidemo`** — boots the reference factory after init. It already paid for itself.
|
||
5. **Accent adopted**: `MachineDef.accent` → first-output derivation → codex kind table.
|
||
**LANE-RENDER: confirming we read this identically** — same three tiers, same order.
|
||
|
||
**VERIFIED — live, in the browser** (`?uidemo`, screenshots taken): the factory builds on
|
||
boot with no console poking; the STANDARDS COMMITTEE renders all 23 techs across REEL /
|
||
BROADCAST / DISC / STREAM / FORTRESS; clicking SOFTWARE DECODING dispatches `setResearch`
|
||
and the node goes hot magenta reading **IN COMMITTEE** with a progress bar (round-tripped
|
||
through the real sim); the POWER page shows DECODE ASIC bright and free next to SOFTWARE
|
||
DECODER and ASIC COOLER greyed with padlocks and `REQUIRES: STREAM SOFTWARE DECODING` /
|
||
`REQUIRES: BROADCAST ASIC COOLING`; `bus.selection()` reads `{mode:'remove'}` and a click
|
||
demolishes the mosh reactor once — DRAW 100→92, "UNIT RECLAIMED. THE FLOOR REMEMBERS."
|
||
|
||
**VERIFIED — headless** (`live.test.ts`, real sim + real DOM, 16 tests): the whole
|
||
research loop the DoD asked for — **lock visible → `place` silently dropped by the sim →
|
||
build a lab → pick the tech through the panel → packs delivered → `researched` → toast →
|
||
padlock falls off → the same `place` now succeeds.** Its harness is now a faithful main.ts
|
||
(typed selection; *it* dispatches place/remove), which is what caught the double-fire.
|
||
|
||
**DECISIONS:**
|
||
- Packs in the research run are teleported into the lab's intake via the sim's own
|
||
`save()`/`load()` (v4 hardened; `load()` re-runs `applyUnlocks`). Everything after that
|
||
is the sim's real `drainLabs → researched → applyUnlocks` path, unmocked. Reason in
|
||
BLOCKED: nothing in the game can currently *make* a science pack.
|
||
- With no `ResearchState` at all, the safe reading of the v4 rule is that gates **hold**
|
||
(padlocks on) rather than fall open, and the panel says RESEARCH OFFLINE instead of
|
||
rendering a tree that can't do anything. The sim always ships a ResearchState today, so
|
||
this is belt-and-braces.
|
||
- `live.test.ts` no longer hardcodes reference-factory coordinates — it finds the i-only
|
||
assembler by its recipe. reference.ts moved it from (-6,5) to (-6,6) *during this
|
||
round* and broke my own test; that's my brittleness, fixed.
|
||
|
||
**BLOCKED/BROKEN (read this part):**
|
||
- **SIM's research gating breaks SIM's own reference factory, and it's live right now.**
|
||
`stream-software-decoding` gates `software-decoder`; the sim silently drops placements
|
||
of gated machines (`if (!isAvailable(def.id)) return`); reference.ts places two software
|
||
decoders for its 200 of generation. Result at HEAD: `?uidemo` builds a factory with
|
||
**GEN 80 against DRAW 100, buffer 0/900, permanent BROWNOUT** — it can never run. My
|
||
screenshots show it. Either reference.ts should use decode-asics, or the demo should
|
||
pre-unlock, or that tech shouldn't gate the generator. (This is also why every
|
||
reference-factory test in `live.test.ts` now runs with research pre-granted.)
|
||
- **Nothing in the game can make a science pack yet, so no research can actually be
|
||
performed by playing.** Packs come only from `artifact-bottler`, whose recipes need
|
||
`field-splinters` / `v-hold-roll` / `static-canister` — a broadcast-era chain that
|
||
neither the reference factory nor any tech-free layout builds. The tree is *reachable*
|
||
on paper (18 techs have satisfiable costs, and DATA fixed the artifact-bottler deadlock
|
||
I was about to report), but unreachable in practice. Suggest reference.ts grow a
|
||
research rig — it would make this verifiable by playing rather than by save/load.
|
||
- **LANE-SIM's `reference.test.ts` is red at HEAD** ("keeps the demuxer alive by voiding
|
||
surplus rather than jamming"), and the factory layout shifted twice while I worked. Not
|
||
mine; flagging because the suite isn't green.
|
||
- **RENDER's demolition tint is still unverified from my side.** The typed `mode` reaches
|
||
`setGhost` via main.ts now, so the magic string is no longer my problem — but I never
|
||
saw the tint land.
|
||
- **The order's own numbers went stale mid-round, again — third round running.**
|
||
"24 nodes" is 23 at HEAD; `tech.json`'s era distribution changed under me; the
|
||
artifact-bottler deadlock I'd have reported as blocking was fixed by DATA while I
|
||
worked; SIM landed *all* of research after my round-start grep said it had none. I now
|
||
re-probe everything against a running sim before writing a word of NOTES, which is the
|
||
only reason this entry is accurate. Worth considering whether lanes should get a
|
||
"HEAD as of" line rather than counts baked into orders.
|
||
- Minor, mine: the tab row wraps to two lines now that RESEARCH joined (8 tabs + REMOVE).
|
||
Legible, not pretty. Real fix is a scrolling tab strip; say the word.
|
||
|
||
**CONTRACT REQUEST:** none this round. v4 gave me everything I asked for and the
|
||
`SelectionState` migration was clean.
|
||
|
||
**PROPOSAL:** `Sim.grantResearch(techIds)` or a `?unlocked` demo flag. Every research-
|
||
adjacent test I wrote needs research pre-granted, and my only route is
|
||
`JSON.parse(sim.save())` → mutate → `load()`. That works and uses public API, but it
|
||
couples my tests to SIM's save schema — if they rename `research` in the blob, my suite
|
||
breaks for no good reason.
|
||
|
||
**NEXT (suggested):** verify RENDER's tint; research rig in reference.ts so a pack economy
|
||
exists to test against; scrolling tab strip if the catalog grows again; era-skinned tech
|
||
panel once eras mean something visually (§8).
|