[orchestrator] round 2 review + contracts v3 + round 3 orders

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>
This commit is contained in:
type-two 2026-07-17 18:05:53 +10:00
parent dc1591d0e6
commit 08761e8eeb
10 changed files with 293 additions and 151 deletions

View File

@ -59,7 +59,9 @@ codex wins.
## Milestones (orchestrator's map — lanes don't self-assign ahead)
- **M1 — FLOW** (rounds 1-2): the full canon melt chain — ore → demuxer →
- **M1 — FLOW** ✅ COMPLETE (round 2; verified live round-2 review: reference factory
ships melt, commission queue advances, SCREEN corrupts item-specifically): the full
canon melt chain — ore → demuxer →
{quantizer, subsampler, p-caster, dct-press} → gop-assembler → mosh reactor →
shipper. RULING (round 1): LANE-DATA's codex-faithful chain replaces the original
6-machine sketch; the ~50-entity build IS the fantasy. A deadlock-free reference

View File

@ -9,9 +9,8 @@ codex wins; when the codex is silent, invent IN ITS VOICE and note what you inve
validator that keeps five parallel lanes from drowning in dangling ids. Content should
light up in-game with zero code changes from other lanes.
**Owned paths:** `data/**`, `src/sim/data-validate.test.ts` — EXCEPTION: this one test
file in sim's directory is yours; it validates data, not sim. Nothing else in
`src/sim/`.
**Owned paths:** `data/**` (the validator now lives at `data/validate.test.ts`;
the old `src/sim/data-validate.test.ts` exception is closed — round 3 moves it).
**Never touch:** contracts, main.ts, other lanes' code. Schema changes are CONTRACT
REQUESTs.
@ -26,46 +25,47 @@ REQUESTs.
---
## CURRENT ORDERS — Round 2 (goal: contracts-v2 content + M2 "PRESSURE" economy)
## CURRENT ORDERS — Round 3 (goal: heat that matters, deeper economy)
Round 1 review: the validator's fault-injection and the FORWARD_CANON allowlist were
exactly right. Verdicts on your report: **M1 IS your canon chain** — MASTERPLAN
updated, no shortcut recipe, the depth is the game. Your four inventions
(`press-anchor-slab`, `color-plate` + `v-hold-roll`, `artifact-bottler`,
`lane-splitter`) are **CANONIZED**. Dials-as-recipes STAYS (right call; a real dial is
a v3+ contract discussion). CONTRACT REQUEST GRANTED: `MachineDef.heatPerTick?` exists
now — and your validator should now also accept `color?`, `flavor?`, `bufferCap?`,
`CommissionDef.repeat?`.
Round 2 review: the bloom-concentrate grep-before-delete and the fault-injection
discipline are exactly the standard. Both your cross-lane asks are RULED:
**(1) Units: per-second.** `gen`/`draw` are per second, `stored` is
bandwidth-seconds; SIM converts their integration this round — your `bufferCap: 240`
= 8 seconds of a 30 deficit, as you intended. **(2) Cooling:** `coolPerTick?` granted
in v3 (sim default 0.004); SIM wires it. Your SIM's-test-is-red flag was correct
mid-round and self-resolved at HEAD (their landing fixed it) — the stash-and-rerun
diagnostic was the right move. Ownership RULING: `data-validate.test.ts` moves to
`data/validate.test.ts` (git mv, yours — vitest picks it up there; `src/sim/**`
returns fully to SIM).
1. **`data/CANON-DELTA.md`** (new file, yours): ledger of everything invented beyond
the codex — round 1's four + whatever follows. The lore doc absorbs it in one
orchestrator pass later; until then this is the single place canon-drift lives.
2. **Art direction pass**: `color` on all 21 machines (renderer precedence:
your color > derived-from-outputs) and `flavor` on all 21 — codex one-liner each,
house voice. This kills UI's hardcoded lookup and RENDER's black-on-black problem.
3. **Heat economy**: `heatPerTick` on software-decoder, and scale its `powerGen` so
hot-but-mighty vs ASIC cool-and-steady is a real choice; a pinch of `heat` on
hex-splicer recipes. SIM implements throttle/scram this round — your numbers decide
whether anyone ever dares run two software decoders side by side.
4. **`bufferCap` on buffer-tank** — one tank should ride out roughly 10 seconds of
typical M1-chain deficit. Show your arithmetic in CANON-DELTA.
5. **Commission queue content** (SIM lands queue + `repeat` this round): reorder for
a difficulty ramp; add 2-3 `repeat:true` standing orders, including **THE
CORRECTION BUYBACK** — pays generous bandwidth for anchor-slabs, fax written in The
Correction's voice: polite, clean, quietly horrifying. This is the sellout loop
(ship your anchors for power now, watch your SCREEN go clean — SCREEN lane already
sanitizes on anchor shipments).
6. **Container line**: `moov-shard` / `offset-ribbon` / `sync-key` items + recipes
assembling `container-pack` (unblocks `stream-hex-splicing`); prune the
FORWARD_CANON allowlist as they gain recipes.
7. **Bloom line**: `bloom-grade-1..5` items + bloom-duplicator recipes
(grade n + delta-wafers → grade n+1); grade 5 is commission bait. Validator covers
all new content.
1. **The validator move** above, first, so the tree is clean.
2. **Heat retune, the headline**: today NOTHING can overheat — every `heatPerTick`
you authored is below the 0.004 ambient cooling, so heat is decorative. With
`coolPerTick` yours to set per machine: hit SIM's suggested feel — software
decoder throttles after ~10s of continuous load, scrams ~13-15s, full duty cycle
~25s; ASIC stays incapable of heat. Show the arithmetic in CANON-DELTA (per-tick
vs per-second — mind the units ruling). Give the hex-splicer a lesser fever.
Coordinate with SIM's reference-factory v3 via NOTES — your numbers decide
whether their layout survives.
3. **Buffer economy check** under the units ruling: is 240 (8s) still your intent
now that it's true? One tank should cover one decoder scram — verify against your
retuned cycle, adjust, show working.
4. **ASIC COOLER spec'd** in CANON-DELTA (the codex names it, §2 "vented by ASIC
coolers"): item/machine entry + intended adjacency mechanic, data authored but
recipes gated behind a `broadcast`-era tech node — the MECHANIC stays deferred
until SIM builds auras (M4); the data existing first is fine and the validator's
FORWARD_CANON pattern covers it.
5. **Reel + broadcast era tech nodes**: research costs in analog packs; unlocks
field-loom/telecine recipe families you already authored.
6. **Muxer + moov brain sub-lines** (§7): `stsz-ledger`, `stco-map`, `stts-metronome`,
`stss-keyring` items + assembly recipes converging on a `moov-brain`
`mux-container` endgame recipe on the muxer. This is M4's rocket — data lands
first, mechanics later. Muxer machine entry if you don't have one.
**Definition of done:** validator green over the expanded catalog; every machine has
color + flavor; the queue tells a story from first-taste to the first buyback; SIM's
reference-factory test still passes against your rebalanced numbers (coordinate via
NOTES if a ratio change breaks their layout).
**Definition of done:** validator green from its new home; a software decoder
demonstrably overheats in SIM's tests using YOUR numbers (coordinate — their DoD
depends on your retune); CANON-DELTA carries the arithmetic; FORWARD_CANON pruned of
anything that gained a recipe.
---

View File

@ -25,33 +25,35 @@ you never simulate. If motion looks wrong, interpolate better or note it — don
---
## CURRENT ORDERS — Round 2 (goal: finish M1 polish, open M2 "PRESSURE")
## CURRENT ORDERS — Round 3 (goal: truth-driven states + asset intake readiness)
Round 1 review: excellent — the hot-swap verification and the fog/CAM_DIST honesty
note were exactly right. Verdicts: req 1 GRANTED (`BeltItem.id?` in contracts v2 —
use when present, keep order-matching as fallback until v3 hardens it). Req 2 GRANTED
(`MachineDef.color?` — precedence: `def.color` > your derived-from-outputs >
kind accent; your derivation stays as the fallback, it was the right instinct).
Req 3 DONE (main.ts ignores non-left buttons; drop your camera.ts workaround or keep
it as belt-and-braces, your call).
Round 2 review: the corner-cargo catch and the scram-latch bug you found by READING
SIM'S CODE are the reasons this process works. Verdicts: your color deviation is
**APPROVED as the ruling** — contracts v3 re-comments `MachineDef.color` as
chassis/body, accent stays derived; DATA's values stand; deviating loudly with a
written rationale was exactly right. Req 2 GRANTED: `EntityState.scrammed` is in v3 —
SIM wires it this round. Req 3 (`BeltItem.side`) DEFERRED until merges matter.
`BeltItem.id` is now required, so your fallback path can retire whenever you like.
1. **Belt topology visuals**: corner/junction pieces chosen from neighbor analysis —
pure presentation, sim truth unchanged. Splitters (mechanics land this round in
SIM) get a visible output-cycling tell.
2. **Heat**: shimmer + dull-red emissive ramp driven by `EntityState.heat`; throttled
= idle animation visibly slowed; scram = dead-dark + blinking amber until restart.
3. **Buffer tank**: visible fill level (shader window or scale trick) proportional to
the tank's share of `snapshot.bandwidth.stored`.
4. **AnimationMixer**: play clip 0 on rigged GLBs with an idle loop — MODELBEAST
assets are imminent; verify with any animated GLB you can find or make.
5. **Exact cargo interpolation** via `BeltItem.id` — delete the one-tick approximation.
6. **Multi-mesh belt GLBs**: merge-or-group fix (your call, note it) so a fancy belt
asset can't half-render.
1. **Scram from truth**: read `EntityState.scrammed`, delete your `SCRAM_RELEASE`
duplication and the desync risk it carried.
2. **Remove-mode support**: UI ships a REMOVE interaction this round — give it a
hover treatment (target machine tinted demolition-red, footprint outline) driven
by a `setGhost`-style call. You own the visual language; coordinate the hook via
NOTES if `setGhost(null, ...)` isn't expressive enough (I'll widen the contract
next round if you two need it).
3. **Showroom formalized**: promote the devscene showroom row to `?showroom` — every
machine + every item on plinths, labeled, camera preset. This becomes the QA page
for MODELBEAST asset drops (which land as `public/assets/models/<asset>.glb`;
your hot-swap + normalization + AnimationMixer are already ready — this is the
eyeball step).
4. **Era-skin groundwork**: registry hook for per-era material variants (§8 era
skins) — plumbing only, stream-era default, so eras can land in data without a
renderer round.
**Definition of done:** reference factory (SIM ships it this round — coordinate via
their `src/sim/reference.ts` export in a dev page or test harness) looks ALIVE: corners
read as corners, heat reads as heat, tanks read as tanks; perf standing rule still met
under `?devscene=stress`.
**Definition of done:** scram visuals verified against SIM's real latch (no local
constants); showroom screenshotted in NOTES; remove-mode hover demonstrated with
UI's flow; perf standing rule still met.
---

View File

@ -27,36 +27,33 @@ seed stock — port, don't move).
---
## CURRENT ORDERS — Round 2 (goal: artifact fidelity, M2 "PRESSURE" hooks)
## CURRENT ORDERS — Round 3 (goal: continuous dread + spatial artifacts)
Round 1 review: the pixel-reading verification set the bar for the fleet. Verdicts:
your anchor-slab PROPOSAL — option (b) ADOPTED as M3 fiction (The Correction buyback;
DATA writes the standing order this round); LANE-UI surfaces a SANITISING chip
meanwhile; keep potency 2.0 as shipped. Debug hook: KEEP, gated behind
`import.meta.env.DEV`. The 2-red you flagged was mid-round churn — HEAD is 70/70.
The launch.json affair: resolved, per-lane ports are in MASTERPLAN now; you're on 8154.
Round 2 review: the ringing self-rejection ("sterile, but sharper — it flunked your
bar and I nearly shipped it") is the exact quality instinct these orders exist to
buy. Verdicts: CONTRACT REQUEST GRANTED — `frame(timeMs, snap?)` is in v3 and main.ts
already passes the snapshot; the un-retained invariant you promised is now a contract
comment. Scram-wipe PROPOSAL: APPROVED at 3 frames (~100ms) — you were right, one
frame is a flinch, not a legend. Regional-moiré PROPOSAL: APPROVED as this round's
centerpiece.
1. **Artifact passes the 8 params can't express** (your list, adopted wholesale):
- legible 8×8 **macroblock quantization** pass — bricks/coeff overdrive must READ
as a blocky grid, not generic noise;
- edge-detected **ringing halos** (Gibbs oscillation hugging contrast edges);
- **moiré interference lattice**.
Wire signatures into corruptionMap: `macroblock-bricks`, `ringing-halos`,
`moire-crystal`. Keep the per-item legibility bar from round 1: shipping 40 bricks
should look UNMISTAKABLY different from 40 slurry.
2. **Shipment lands**: a ~0.4s stamp/silhouette flash in the item's chip color
composited on the feed per `shipped` event (batch bursts: one stamp per event, not
per unit — a 40-unit shipment is one big stamp, not 40 flickers).
3. **Scram wipe**: `scram` events (new in contracts v2) get a one-frame white
"TRACKING LOST" bar wipe. Cheap, from the event stream you already have.
4. **OPTIONAL — signal strain**: buffer tanks land this round; if you want THE SCREEN
to tremble as `bandwidth.stored` drains BEFORE brownout, file the snapshot-access
CONTRACT REQUEST you deferred (`frame(timeMs, snap?)`) and build it next round —
or skip it. Your call; the seam is noted and I'll grant it in v3 if asked.
1. **Signal strain** (your design, now unblocked): continuous dread from the
snapshot — `bandwidth.stored` draining toward zero = rising tremor/desync judder
BEFORE the brownout seizure; aggregate entity `heat` = a slow feverish shimmer.
Keep it subordinate to the corruption ledger: strain is weather, shipments are
climate.
2. **Regional moiré**: grow the lattice from a region instead of full-frame (the
codex has it spreading as maze walls) — spatial mask, seeded deterministically,
creeping outward with tally. This is the template for future spatial artifacts;
note the mask approach for reuse.
3. **Scram wipe at 3 frames**, as approved.
4. **Idle strain baseline**: with zero corruption AND healthy bandwidth, the feed
stays perfectly sterile — verify strain can't leak into the NOTHING IS WRONG era
(a test pinning params at zero under a healthy snapshot would do it).
**Definition of done:** three new artifact classes verified the way you did round 1
(drive, read pixels, screenshot); stamps and scram wipe visible on the dev build;
frame budget still <2ms; tests updated for new signatures.
**Definition of done:** strain visible as stored drains (drive it by pausing the
reference factory's generators or just feeding a fixture snapshot), regional moiré
grows rather than switches, wipe readable, budget still <2ms, tests updated.
---

View File

@ -21,39 +21,39 @@ playable through unit tests. You are the lane the other four trust with their li
---
## CURRENT ORDERS — Round 2 (goal: finish M1, open M2 "PRESSURE")
## CURRENT ORDERS — Round 3 (goal: M2 "PRESSURE" becomes real, M3 groundwork)
Round 1 review: outstanding. Verdicts on your report — **rank-routing STAYS** (it made
M1 flow; your proposal 2, auto-fallback, is REJECTED for exactly the reason you gave:
it deletes the byproduct tension). Proposal 3 (starved) APPROVED. Contracts v2 landed
for you: `BeltItem.id?`, `CommissionDef.repeat?`, `MachineDef.heatPerTick?/bufferCap?`,
`SimEvent 'scram'`. Extractor seam tiles: deferred to M3 worldgen, stop worrying.
Round 2 review: the reference factory turning round 1's deadlock into 1,136 shipped
bricks is exactly what "surplus becomes product" was supposed to mean — outstanding.
Verdicts: `coolPerTick` GRANTED (contracts v3, default 0.004 — your constant becomes
the fallback). `setRecipeAt` GRANTED (your tripwire, defused). `BeltItem.id` HARDENED
to required (you asked for it). Throttle-hits-powerGen: KEPT, your reading was right.
The validator-file ownership wart: RULED — it moves to `data/validate.test.ts`, DATA
executes the move, `src/sim/**` is purely yours again.
1. **Reference factory.** `src/sim/reference.ts` exporting
`referenceFactory(data: GameData): Command[]` — your deadlock-free M1 layout
(second quantizer on crime, second assembler on i-only). Test: ships melt within
2,000 ticks AND is still shipping at 20,000 (the deadlock regression net). This
becomes the official demo; I wire it to a dev hook in main.ts next round.
2. **Splitters.** `kind:'splitter'` becomes real: 1 input, round-robin across outward-
facing adjacent belts; a blocked output is skipped, not waited on; your rank cache
traces THROUGH splitters (rank = best reachable downstream). This is the sanctioned
surplus-voiding tool — the rank rule elsewhere is unchanged.
3. **Buffer tanks.** `kind:'buffer'` + `bufferCap`: charge when gen>draw, discharge to
cover deficit, brownout only when tanks run dry; `snapshot.bandwidth.stored` = pool.
4. **Heat v1.** Accumulate `heatPerTick` while active + `RecipeDef.heat` per craft;
ambient dissipation; heat>0.7 → per-machine throttle (curve is your call, note it);
heat=1 → scram: machine stops, emits `{kind:'scram',on:true}`, restarts below 0.5
with `on:false`. `EntityState.heat` is already in the contract — make it real.
5. **Commission queue.** Data-order activation; `commissionDone` advances; `repeat:true`
re-enters at the back of the queue. Progress counts only while active (keep).
6. **`jammed:'starved'`** — recipe idle with empty inputs, edge-triggered like
'output full'.
7. **`BeltItem.id`** — stable per-item ids in every snapshot (optional field now;
hardens to required in v3, so just always set it).
**UNITS RULING (DATA's cross-lane ask):** `gen`/`draw` are bandwidth per SECOND;
`stored` is bandwidth-seconds. Your integration currently charges/drains per tick —
divide by `TICKS_PER_SECOND` so a 30-deficit drains 30 stored per second, not per
tick. DATA's `bufferCap: 240` then covers 8s as they intended. Update tests; confirm
the conversion in NOTES so DATA can retune against truth.
**Definition of done:** all of the above tested (determinism suite still green);
reference factory test proves melt at scale with zero deadlock; heat demonstrably
throttles and scrams the software decoder in a test.
1. **Contracts v3 wiring**: `EntityState.scrammed` (RENDER is currently duplicating
your 0.5 constant to avoid rendering a lie — kill that), `commissionQueue` on the
snapshot (active first, then upcoming — UI's NEXT IN TRAY is guessing today),
`coolPerTick` per machine, `setRecipeAt` command, the units conversion above.
2. **Save/load**: implement `save()`/`load()` — JSON, deterministic round-trip
(test: save at tick N, load into a fresh sim, run both 1,000 ticks, deep-equal).
3. **Reference factory v3**: heat is about to become real (DATA retunes this round —
coordinate via NOTES). Restructure power as ASIC base + software-decoder burst +
buffer tank so the layout *survives scram duty-cycles* — DATA warned your three
naked decoders will black out in lockstep. Keep the 20k regression green.
4. **Bloom loop proof**: test that a closed belt circle through a bloom-duplicator
accumulates grades (your rank-0 loop acceptance + DATA's grade recipes should
compose). This is M3's signature topology — prove it early.
**Definition of done:** all tests green including save/load round-trip and the bloom
loop; NOTES confirm the units conversion + the retuned reference factory's scram
survival (ticks of first scram, recovery, melt rate before/after).
---

View File

@ -24,32 +24,39 @@ dispatch Commands via the bus and read state from `update(snap, events)`.
---
## CURRENT ORDERS — Round 2 (goal: finish M1 interaction, open M2 "PRESSURE")
## CURRENT ORDERS — Round 3 (goal: close the interaction loop, verify the pressure UI live)
Round 1 review: strong, and the honesty section was the best in the fleet. Verdicts:
CONTRACT REQUEST 1 GRANTED — `UIBus.pickTile` is live, wired to the renderer in
main.ts. Req 2 GRANTED (`MachineDef.color?`). Req 3 GRANTED as `MachineDef.flavor?`
LANE-DATA fills it this round; read data first, keep voice.ts as fallback. Req 4
GRANTED — `happy-dom` is installed; use `// @vitest-environment happy-dom` per test
file (no root config change needed). Build-bar paging PROPOSAL: APPROVED.
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.
1. **Click-to-inspect** via `bus.pickTile`. Delete the TAB-cycle fallback and
`?uifixture` — the sim is real now; drive a brownout by building.
2. **Build bar paging** (your design): kind tabs, `[`/`]` to page, 1-9 within the
active page — every machine hotkey-reachable at any catalog size.
3. **Inspector upgrades**: recipe picker (`setRecipe` finally gets its UI), heat bar
with THROTTLING/SCRAM states, and starved-vs-backed-up wording (sim distinguishes
`'starved'` from `'output full'` this round).
4. **Commission queue UI**: current fax + a "NEXT IN TRAY" peek; `repeat:true`
commissions get a STANDING ORDER stamp. When anchor-slabs ship, show a SANITISING
chip (hardcoding the id is fine this round — the fiction becomes The Correction's
buyback in M3).
5. **Tests, minimum 10**: chips, roster/footprint hit-testing, selection, hotkey map —
under happy-dom.
1. **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.").
2. **NEXT IN TRAY becomes truth**: read `snap.commissionQueue` when present, delete
`peekNext` inference the moment SIM lands it (keep fallback until then).
3. **DOM render tests**: the two you named as needing a real DOM (chip rendering,
`attach()` binding) plus fax stamp lifecycle — `// @vitest-environment happy-dom`.
4. **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).
5. **Stretch, skippable**: read-only tech panel (`T`) rendering `tech.json` as an era
tree marked RESEARCH OFFLINE — research mechanics land in M3, the panel can exist
first.
**Definition of done:** the reference factory (SIM ships `src/sim/reference.ts` this
round) is inspectable end to end by mouse alone; every machine reachable by hotkey;
tests green; fixture deleted.
**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.
---

112
fktry/package-lock.json generated
View File

@ -12,6 +12,7 @@
},
"devDependencies": {
"@types/three": "^0.170.0",
"happy-dom": "^20.10.6",
"typescript": "^5.6.0",
"vite": "^6.0.0",
"vitest": "^2.1.0"
@ -869,6 +870,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
@ -898,6 +909,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/whatwg-mimetype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vitest/expect": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
@ -1001,6 +1029,19 @@
"node": ">=12"
}
},
"node_modules/buffer-image-size": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz",
"integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@ -1066,6 +1107,19 @@
"node": ">=6"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@ -1175,6 +1229,25 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/happy-dom": {
"version": "20.10.6",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.10.6.tgz",
"integrity": "sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": ">=20.0.0",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.18.1",
"buffer-image-size": "^0.6.4",
"entities": "^7.0.1",
"whatwg-mimetype": "^3.0.0",
"ws": "^8.21.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
@ -1448,6 +1521,13 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
@ -2619,6 +2699,16 @@
}
}
},
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@ -2635,6 +2725,28 @@
"engines": {
"node": ">=8"
}
},
"node_modules/ws": {
"version": "8.21.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@ -14,6 +14,7 @@
},
"devDependencies": {
"@types/three": "^0.170.0",
"happy-dom": "^20.10.6",
"typescript": "^5.6.0",
"vite": "^6.0.0",
"vitest": "^2.1.0"

View File

@ -51,10 +51,12 @@ export interface MachineDef {
powerGen?: number;
beltSpeed?: number; // tiles/sec, kind==='belt'
asset: string; // codex asset key for renderer mesh registry, e.g. "quantizer"
color?: string; // v2: art-direction accent; renderer falls back to derived
color?: string; // v3 RULING: CHASSIS/BODY color (codex §8 grime); the emissive
// accent stays derived from outputs. (Was mislabeled "accent".)
flavor?: string; // v2: codex one-liner for the inspector (DATA's material)
heatPerTick?: number; // v2: heat while active (the software decoder's whole bit)
bufferCap?: number; // v2: bandwidth storage, kind==='buffer'
bufferCap?: number; // v2: bandwidth storage in bandwidth-SECONDS (see SimSnapshot)
coolPerTick?: number; // v3: per-machine cooling rate; sim default 0.004
}
export interface TechDef {
@ -93,14 +95,15 @@ export interface EntityState {
outputBuf: Record<string, number>;
jammed: string | null; // human-readable reason or null
heat: number; // 0..1, throttles >0.7, scrams at 1
scrammed?: boolean; // v3: heat-scram latch (heat alone can't show the
// hysteresis window; renderer/UI read this, not 0.5)
}
export interface BeltItem {
item: string;
entity: number; // belt entity id
t: number; // 0..1 progress along the belt tile
id?: number; // v2: stable per-item id for exact render interpolation
// (optional this round; hardens to required in v3)
id: number; // v3: stable per-item id (required — sim always stamps)
}
export interface SimSnapshot {
@ -108,9 +111,13 @@ export interface SimSnapshot {
paused: boolean;
entities: ReadonlyArray<EntityState>;
beltItems: ReadonlyArray<BeltItem>;
// v3 RULING on units: gen/draw are bandwidth per SECOND; stored is bandwidth-seconds
// (sim integrates surplus/deficit divided by TICKS_PER_SECOND each tick).
bandwidth: { gen: number; draw: number; stored: number; brownout: boolean };
shippedTotal: Record<string, number>;
activeCommission: string | null;
commissionQueue?: string[]; // v3: truth for "next in tray" — active first, then
// upcoming in activation order (hardens in v4)
commissionProgress: Record<string, number>;
}
@ -119,6 +126,7 @@ export type Command =
| { kind: 'remove'; pos: Vec2 }
| { kind: 'rotate'; pos: Vec2 }
| { kind: 'setRecipe'; entity: number; recipe: string | null }
| { kind: 'setRecipeAt'; pos: Vec2; recipe: string | null } // v3: id-free variant
| { kind: 'setPaused'; paused: boolean };
export type SimEvent =
@ -139,6 +147,8 @@ export interface Sim {
tick(): void; // advance exactly one tick
snapshot(): SimSnapshot; // read-only view; renderer/ui must not mutate
drainEvents(): SimEvent[]; // events since last drain (main loop fans out)
save?(): string; // v3: full state as JSON, deterministic round-trip
load?(json: string): void; // v3: restore a save() blob (hardens in v4)
}
export interface Renderer {
@ -164,5 +174,6 @@ export interface UI {
export interface ScreenFX {
init(canvas: HTMLCanvasElement, data: GameData): void;
onEvents(events: SimEvent[]): void; // shipped items feed the corruption
frame(timeMs: number): void;
frame(timeMs: number, snap?: SimSnapshot): void; // v3: read-only peek for continuous
// strain (stored, heat); never retain
}

View File

@ -59,9 +59,19 @@ async function boot() {
renderer.render(snap, acc / MS_PER_TICK);
ui.update(snap, events);
screen.onEvents(events);
screen.frame(now);
screen.frame(now, snap);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// dev-only: paste-free demo — builds SIM's reference factory
if (import.meta.env.DEV) {
(window as any).__fktryDemo = async () => {
const { referenceFactory } = await import('./sim/reference');
const cmds = referenceFactory(data);
cmds.forEach((c) => sim.enqueue(c));
return `${cmds.length} commands dispatched`;
};
}
}
boot();