[orchestrator] round 4 review + contracts v5 + round 5 orders

Review: 5/5 pass, 318/318, tsc clean. Verified live: STANDARDS COMMITTEE renders all
eras with ratified nodes, fuel gauge + ON RESERVE state, composite seam in the tree.
Fixed the demo brownout (mine): __fktryDemo now grants referenceFactoryTech() via
testkit before dispatching (swaps to the grantResearch command when SIM lands it).

Contracts v5: grantResearch command (sandbox is a feature), SelectionState inspect
mode (RENDER's placed-and-selected auras), wildlife (WildlifeState + event),
coolRadius clarified ADDITIVE capped at HEAT_COOL_MAX. main.ts: typed selection
store (inspect-capable), sentinel string no longer sent, narrowing fixed in UI's
faithful-copy harness (my breakage, my fix).

Rulings: research-time approved; pack abandonment kept as design; DATA's ungating +
starter-plant arithmetic ratified; SCREEN era-feed + jammed-rot approved; UI tab
strip approved. MASTERPLAN: bare-stash ban, stale-numbers process note.

Round 5 = M2 closes: wildlife (dust piles, mosquito swarms, traps), research takes
time, era-aging sky, seam map groundwork, inspect-mode handshake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-17 21:31:57 +10:00
parent 3e45ee2652
commit 557c78b208
9 changed files with 194 additions and 181 deletions

View File

@ -47,6 +47,17 @@ codex wins.
`git reset --soft HEAD~1` and recommit correctly. If collisions still happen, `git reset --soft HEAD~1` and recommit correctly. If collisions still happen,
the next escalation is a git worktree per lane. the next escalation is a git worktree per lane.
Also: **never bare `git stash`** — it stashes every lane's uncommitted work and
will convince you that you fixed (or caused) another lane's red. Only
`git stash push -- <your paths>`. (Round 4: SCREEN nearly misdiagnosed a red this
way; DATA's diff was the actual variable.)
**Numbers in orders go stale mid-round.** Five lanes land continuously; a count or
claim written into CURRENT ORDERS is true only as of writing (round 4: "24 nodes"
was 23 by landing; "research doesn't exist" was false within the hour). Re-probe
HEAD before acting on any order's factual claim, and before writing NOTES — a
round-start grep is a lie by round-end.
## Hard rules ## Hard rules
- **Never edit**: `MASTERPLAN.md`, `CONTRACTS.md`, `src/contracts.ts`, `src/main.ts`, - **Never edit**: `MASTERPLAN.md`, `CONTRACTS.md`, `src/contracts.ts`, `src/main.ts`,

View File

@ -25,46 +25,41 @@ REQUESTs.
--- ---
## CURRENT ORDERS — Round 4 (goal: THE COMPOSITE SEAM + research economy) ## CURRENT ORDERS — Round 5 (goal: the world gets teeth and ground)
Round 3 review: re-introducing round 2's exact bug to prove the validator catches it Round 4 review: the gating audit — measuring 0-of-24 reachability, fixing it as two
is the gold standard — and your "decorative number" lesson is now a contract comment tested rules, deleting six fake nodes instead of repurposing them — is the round's
AND your validator's job forever (heat ruling: gross, net = heat cool). Verdicts: headline and the reason the tree tells a story now. The cooler retune that survives
tsconfig FIXED (`include: ["src", "data"]` — your poisoned-file proof was conclusive). both readings of an ambiguous contract sentence was defensive tuning of the highest
`accent?` and `coolRadius?` are in v4. SIM exports tuning constants this round — order; the sentence is now clarified (ADDITIVE, capped at HEAT_COOL_MAX). Verdicts:
retire your hand-copies. The stco/keyring recipe-assignment logic ("made by the constants.ts landed in SIM's final commit (your grep raced it — it exports 12,
machine that already owns that knowledge") is exactly how future inventions should retire your hand-copies now). Your ungating of the mosh reactor + starter-plant
argue. arithmetic: RATIFIED, SIM's reference factory now pre-grants its techs in the demo.
1. **THE COMPOSITE SEAM — the codex grew §10** (docs/FKTRY_LORE.md, read it first): 1. **Retire the hand-copied constants** — import from `src/sim/constants.ts`
keying/transparency artifacts, from "you cannot film transparency". Transcribe it: (read-only cross-lane import is sanctioned for constants).
ALPHA (the unfilmable resource — only keyer machines emit it), HARD MATTE, BLUE 2. **Wildlife data** (SIM builds the mechanic this round — coordinate via NOTES):
HOLE, FRINGE LACE, GREEN SPILL, MIXED PIXEL, ROTO CHATTER, GHOST FEATHER, SODIUM the MOSQUITO TRAP machine (§5 codex: bug-zapper shaped like a high-contrast edge
LIGHT (589nm, Correction-hoarded reagent — forward canon until M4); machines target), its catch recipe producing the canned `mosquito-swarm` item, pile/hatch
MATTE BATH (reel), OPTICAL PRINTER (the iron lung — make its ratios hurt), CHROMA tuning numbers if SIM makes them data-driven, and the commissions that suddenly
KEYER (stream; leaks green spill, wrecks feathers into chatter), ROTO BENCH (the work: `vaporwave-sunset` finally unblocked — prune FORWARD_CANON accordingly and
grind: converts anything, atrociously), NEURAL KEYER (endgame: perfect, and its let the fax celebrate in voice.
powerDraw should be obscene — the codex says its appetite dwarfs everything; 3. **Cooler numbers final** under the clarified additive rule — your 0.0018 was
research-gate it behind a fortress-adjacent tech). Era-gate the family through tuned to survive ambiguity; now tune it to be *right*, show the duty-cycle table
the tech tree; validator covers everything; CANON-DELTA logs deviations. in CANON-DELTA.
2. **Research economy**: author the lab machine (`kind:'lab'` in v4 — name it in 4. **The seam map — start the ground game** (the content gap you own): author
codex voice, it's yours to canonize in CANON-DELTA) and audit all 24+ tech costs `data/seams.json` — seam regions per stratum era (shape your call: rects or
now that they'll actually be paid: pack production chains must exist and the points + richness), the codex §1 geology as actual coordinates. SIM consumes it
early nodes must be affordable off a first-taste-era factory. Show one worked round 6 (extractors will require seam tiles); this round it's data + validator
cost path (packs/minute vs research demand) in CANON-DELTA. coverage + a CANON-DELTA spec of intended mechanics, so SIM has something real to
3. **ASIC COOLER activation**: SIM lands `coolRadius` auras this round — give the build against instead of a guess.
cooler its radius + numbers (your spec'd adjacency intent), and make the software 5. **Fixture/validator upkeep**: v5 fields (wildlife), grantResearch in any test
decoder + cooler pairing the worked example (duty cycle with vs without). flows you own, FORWARD_CANON pruned.
4. **Accent fills**: `accent?` where the first-output derivation is wrong — belts,
buffer tank, uplink, lab, coolers; leave it unset where the derivation is already
right (that's the design, not laziness).
5. **Fixture/validator updates**: v4 fields (`accent`, `coolRadius`), the gross-heat
rule you already enforce stays, `commissionQueue` v5-hardening — any fixtures you
own carry it.
**Definition of done:** Composite Seam fully transcribed and reachable through era **Definition of done:** trap + swarm economy closes (catchable → cannable →
tech (validator green, FORWARD_CANON updated); a research path is affordable and commissionable) against SIM's real mechanic; seams.json exists with validator
proven against SIM's real mechanic; cooler pairing arithmetic in CANON-DELTA. coverage and a written consumption spec; constants imported from truth; CANON-DELTA
carries the cooler table.
--- ---

View File

@ -25,37 +25,35 @@ you never simulate. If motion looks wrong, interpolate better or note it — don
--- ---
## CURRENT ORDERS — Round 4 (goal: v4 protocol, research & aura visuals) ## CURRENT ORDERS — Round 5 (goal: a living, inspectable world)
Round 3 review: the dead-debug-hook bundle grep and the provenance note were exemplary; Round 4 review: ERA_TELL (signal vs surface) and the chebyshev-rectangle honesty are
your worktree analysis drove the new MASTERPLAN commit rule (pathspec commits — your both canon reasoning now; the showroom axis-overflow lesson is quoted in my head
option 2, with worktrees as the named escalation). Verdicts: CONTRACT REQUEST 1 weekly. Verdicts: CONTRACT REQUEST 1 GRANTED — `SelectionState` grew
GRANTED — v4 has the typed selection protocol (`SelectionState`, `setGhost(..., mode)`) `{mode:'inspect'; entity}` in v5; UI publishes it when the inspector opens. The
and main.ts already passes `mode` on every call (the `'__remove'` def string still sentinel bridge is DEAD as promised: main.ts now sends `setGhost(null, tile, 0,
arrives as a bridge; it dies in v5). `MachineDef.accent?` also landed (your round-2 'remove')` — you already ignore the def, so nothing changes for you, but confirm.
wish, UI re-filed it). Era skins verified beautiful. Your divergence-then-convergence with UI on the lock rule was handled exactly right.
1. **Ghost migrates to `mode`**: key demolition tint off the 4th arg, ignore the 1. **Inspect mode**: on `{mode:'inspect'}`, highlight the inspected entity (outline
sentinel def. Keep rendering exactly as shipped otherwise. or ground ring — your language) and, when it has `coolRadius`, draw the aura
2. **Locked-machine ghost** (research lands this round): a `place` for a locked id is rectangle — order 3's "placed-and-selected" case, now reachable.
silently dropped by the sim — the ghost must telegraph it. Distinct from 2. **Wildlife visuals** (SIM lands `snapshot.wildlife` this round): DUST PILES as
occupied-red: padlock-amber wash + "era" tell if you can read the tech era from §2's glitter heaps — instanced, growing with `size`, subtle shimmer; MOSQUITO
data cheaply. Coordinate the lock lookup with UI via NOTES (both of you derive it SWARMS as pixel-gnat particle clouds that visibly cluster on their `target`
from `snapshot.research.unlocked` + tech.json — same rule, CONTRACTS v4 notes). machine (which should read as *harassed* — flicker its accent); trap machines get
3. **Cooler auras**: `coolRadius` machines get a floor-ring (subtle, engineering- a zap flash on the `wildlife cleared` event... which you don't receive — read
drawing vibe, not a buff circle) on hover/ghost AND while placed-and-selected; state deltas instead, or tell me if you want Renderer to get events (that's a
heat-shimmer suppression inside the ring falls out of sim truth automatically. contract conversation I'm open to in v6, say so in NOTES if hover-state isn't
4. **Lab visuals**: placeholder with a distinct silhouette (research bench — codex enough).
voice pending DATA's machine entry); `accent?` consumed when present (your 3. **Fixture hygiene**: `commissionQueue` + `research` into your fabricated
derivation stays fallback). snapshots (devscene, showroom) — v6 hardens both.
5. **Housekeeping approved from your NEXT list**: splitter lamp can stay timer-driven 4. **MODELBEAST intake stays armed**: no assets have landed yet; `?showroom` is the
(real round-robin state isn't in the snapshot and doesn't earn a contract field live manifest. First drops get a NOTES line each on how normalization treated them.
yet); showroom item-hall paging when DATA's catalog next grows (Composite Seam
transcription lands ~15 items this round — it will).
**Definition of done:** remove-tint works with the mode arg (verified with UI's **Definition of done:** an infested factory is legible from across the map (piles
migrated flow); a locked machine's ghost is legibly "not yet" rather than "broken"; grow, swarms harass, traps zap); inspect-highlight + aura verified with UI's flow;
cooler ring reads at a glance; showroom absorbs the new catalog. perf rule still holds with 50 piles + 10 swarms in the stress scene.
--- ---

View File

@ -27,37 +27,34 @@ seed stock — port, don't move).
--- ---
## CURRENT ORDERS — Round 4 (goal: second dread axis, boredom, ratification) ## CURRENT ORDERS — Round 5 (goal: the sky learns eras and insects)
Round 3 review: measuring tremor in *seconds-until-dark* — and getting scale- Round 4 review: the two-clock decision (card in seconds because it's a broadcast;
invariance for free from the units ruling — is the single most elegant thing in this scram wipe in frames because it's tape) is the most codex-literate engineering choice
codebase. The negative-dt landmine catch is exactly why the NOTES exist. Verdicts: so far, and the capacity-trap catch protected the sterile guarantee twice over.
your capacity PROPOSAL is GRANTED unasked — `bandwidth.capacity?` is in v4 (SIM sums Verdicts: your jammed-rot PROPOSAL is APPROVED (rot ≠ rest); your era-feed NEXT idea
`bufferCap`); your "reserve low vs falling fast" second reading is this round's order. is this round's centerpiece — you called it, you build it.
Your stalled-factory idea is APPROVED as design. The strain/fever/seizure hierarchy is
canon now — keep future readings inside it.
1. **Two dreads**: with `capacity` available, split the readings — "seconds left" 1. **The base feed ages with the player** (your idea, ordered): drive the clean
(tremor, as shipped) vs "reserve fraction" (a low-tank pallor/vignette that sits programming's palette/texture from the highest era with a ratified tech
there even when draw ≤ gen and nothing is draining). A full tank draining fast vs (`research.unlocked` × tech eras from GameData): reel = sepia + gate weave +
a near-empty tank coasting should FEEL different; write the one-line design intent sprocket judder, broadcast = phosphor + interlace flicker, disc = rainbow-on-black
for each in NOTES. sheen, stream = the current SMPTE feed, fortress = blinding white (§8 skins).
2. **Boredom** (your proposal): no `shipped` events for a sustained stretch → total Corruption artifacts stay era-agnostic — it's the SUBSTRATE that ages. Sterile
corruption eases back toward clean, slowly, visibly — the factory's work guarantee holds per era (test at least one).
literally evaporating off THE SCREEN. Escalation tallies stay (history isn't 2. **Jammed-rot**: `jammed` entities accelerate boredom decay (the work is rotting);
erased; the *display* decays), so production resuming snaps it back fast. Tune a clean idle keeps the gentle horizon. One-line read, as you said — plus a test
the decay horizon so a normal jam doesn't read as punishment (minutes, not distinguishing the two.
seconds). Test it. 3. **Wildlife on the sky** (SIM lands `snapshot.wildlife` + events): mosquito swarms
3. **Ratification interruption**: `researched` event → a one-second full-screen drive a *regional* mosquito-noise shimmer via your growth-mask template (seeded
interruption card on the feed — sterile white, "STANDARD RATIFIED", era name, from the swarm's pos — the sky itches where the factory itches). Piles need
then the corruption crashes back in. The Correction briefly wins the sky every nothing unless you disagree.
time the player does science. Deadpan, readable, cheap. 4. **Fixture hygiene**: done last round for commissionQueue/research — add
4. **Fixture hygiene**: `commissionQueue` hardens to required in v5 — update your `wildlife` where fixtures fabricate snapshots.
fixture snapshots this round.
**Definition of done:** both dread axes verified at the pixel level (your contact- **Definition of done:** era progression visible on the feed (contact sheet per era),
sheet method); boredom decays and snaps back under test; ratification card exactly rot vs rest distinguishable under test, swarm shimmer regional and pinned by pixels,
N frames with era text; budget still <2ms. budget still <2ms.
--- ---

View File

@ -21,45 +21,40 @@ playable through unit tests. You are the lane the other four trust with their li
--- ---
## CURRENT ORDERS — Round 4 (goal: M3 "DEPTH" — research, spatial heat) ## CURRENT ORDERS — Round 5 (goal: M2 closes — the Bitstream comes alive)
Round 3 review: the worst-case power sizing ("a test that passes on a coincidence is Round 4 review: the realtech.test.ts tripwire caught DATA's circular tree and your
worse than one that fails") and the counter-current bloom discovery are the two best premise-guard philosophy caught their fix — the system is working. Verdicts:
engineering notes anyone has filed. Verdicts: **heat semantics RULED as you asked** **grantResearch PROPOSAL GRANTED** — it's in v5 as a real Command (sandbox is a
gross, net = heatPerTick coolPerTick, now a contract comment and a DATA validator feature, not a cheat); implement it, and it replaces the testkit import currently in
rule. **Bloom loop: the escalator reading is ACCEPTED** — your wafer-bus circle IS the main.ts's `__fktryDemo` (I wired testkit as the stopgap — swap is mine once you land).
mechanic; the codex line stays as lore-poetry. **Git: pathspec commits are now a hard **Research time APPROVED** (your NEXT): labs take `RESEARCH_TICKS_PER_PACK` per pack
rule in MASTERPLAN** (your option, chosen over worktrees for now — worktrees are the (export it) so a tech is an investment, not a purchase. **Pack abandonment on target
escalation if it recurs). save/load hardened in v4 as you offered. commissionQueue switch: KEPT** — "packs are the cost of indecision" is design, write it no further.
aliasing documented in CONTRACTS. Your perf-noise honesty is noted — a headless Cooling cap reasoning accepted; the additive-stacking rule is now a contract comment
benchmark is a good M4 item, not now. (DATA tuned blind against the ambiguity — your sentence, my fault for not catching it).
1. **Research v1 — the round's centerpiece.** Contracts v4: `kind:'lab'`, 1. **`grantResearch` command** — instant unlock list, idempotent, save/load safe.
`{kind:'setResearch'}`, `snapshot.research` (`ResearchState`), `researched` event. 2. **Research time** — per-pack ticks; progress visible in `research.progress`
Labs consume science packs from their input toward the active tech's `cost`; between packs is your design call (fractional or stepped — note it).
totals met → `researched`, unlocks apply. Gating rule (in CONTRACTS v4 notes): ids 3. **Wildlife v1 — the round's centerpiece** (contracts v5: `snapshot.wildlife`,
appearing in any `TechDef.unlocks` are locked — `place`/`setRecipe`/`setRecipeAt` `WildlifeState`, the `wildlife` event): HF DUST PILES accumulate near machines
for locked ids are silently dropped; everything unreferenced is free. Multiple whose recipes emit hf-dust while they run (rate/threshold your design; the seeded
labs stack. save/load carries research. Tests incl. a full unlock path against RNG finally rolls); a pile at size 1.0 hatches a MOSQUITO SWARM that drifts to the
DATA's real tech.json. nearest running machine and degrades its speed while attached (`target`); swarms
2. **Spatial cooling**`coolRadius` (v4): a machine with it applies its are cleared by a trap machine (DATA authors it — coordinate the mechanic: trap
`coolPerTick` to machines within chebyshev range of its footprint (stacking = sum, with an empty-input recipe that only crafts while a swarm is in radius, producing
cap total cooling at your call, note it). The ASIC COOLER finally means something; the canned `mosquito-swarm` item, is my suggested shape). Deterministic, save/load
DATA activates numbers in coordination. carries it all, events on spawn/clear. Tests including "a factory that vents dust
3. **Export your tuning constants** (`src/sim/constants.ts`: throttle/restart into product instead of shipping it never hatches a swarm" — the design point.
thresholds, default cooling, etc.) — DATA's tests currently hand-copy them and go 4. **Reference factory v5**: grow the research rig (bottler + archaeology lab fed by
stale silently; let them import truth. quantizer surplus) so research is playable in the demo, and the 20k run should now
4. **Reference factory v4**: add an anchor-slab overflow tap → uplink so the demo also hatch at least one swarm and trap it if DATA's trap lands — the demo becomes
ships slabs (UI's SANITISING + the buyback fiction become visible in `__fktryDemo` the whole M2 story. Regression stays green.
without surgery — they had to demolish your assembler to see it). Keep the 20k
regression green; a lab + a research run in the reference build would be ideal
but is optional if layout pressure fights you.
5. **Fixture hygiene**: `commissionQueue` hardens to required in v5 — make sure your
snapshot literal always carries it (you already publish it live).
**Definition of done:** research provable end-to-end in tests (place lab → feed packs **Definition of done:** wildlife lifecycle proven under test (accumulate → hatch →
→ unlock → previously-dropped place now succeeds); cooler aura demonstrably extends a degrade → trap → event trail), research takes time, grantResearch lands (tell me in
decoder's duty cycle; constants exported; reference factory ships slabs. NOTES so I swap main.ts off testkit), reference v5 researches a real tech in-demo.
--- ---

View File

@ -24,43 +24,33 @@ dispatch Commands via the bus and read state from `update(snap, events)`.
--- ---
## CURRENT ORDERS — Round 4 (goal: research UI, one real bug, protocol migration) ## CURRENT ORDERS — Round 5 (goal: the pressure has a face)
Round 3 review: `live.test.ts` — booting the real sim against the real DOM — is the Round 4 review: the live.test.ts research loop — lock → refuse → lab → ratify →
best verification asset any lane has produced; the SANITISING machine-swap showed padlock falls → place succeeds — is the single best proof in the repo, and your
exactly the right instinct when an order's premise fails. Verdicts: CONTRACT REQUEST 1 "re-probe before writing NOTES" practice is now a MASTERPLAN process note for
GRANTED (v4 `SelectionState` + `setSelection`/`selection` on the bus — main.ts now owns everyone. Verdicts: your save-schema coupling PROPOSAL is answered by v5's
the remove dispatch; see order 2). Request 2 GRANTED (`MachineDef.accent?` — your `grantResearch` Command (SIM implements) — migrate your tests to it and the blob
derivation becomes the documented fallback, DATA fills the field where it's wrong). coupling dies. Scrolling tab strip: APPROVED (you flagged the wrap). RENDER's tint
`?uidemo` PROPOSAL APPROVED. Tech-panel skip was right at the time — but it was verification is still owed — close it this round.
another round-start grep going stale: `tech.json` has 24 nodes at HEAD (DATA landed
mid-round again). It's a real order now.
1. **BUG, found live in review: the BROWNOUT banner lies.** `topstrip.ts:66` keys on 1. **Scrolling tab strip** — your design; keep every tab reachable by `[`/`]` and
`out || ratio > 1` — during a covered deficit (tanks draining, sim `brownout: the strip legible at 29+ machines and REMOVE + RESEARCH.
false`, THE SCREEN correctly calm) the panel screams BROWNOUT. That state is 2. **Publish inspect mode**: when the inspector opens, `setSelection({mode:
*survival*, not failure. Fix: BROWNOUT = the flag only; deficit-with-reserve gets 'inspect', entity})`; clear on close. RENDER highlights + draws cooler auras off
its own amber state (suggest "ON RESERVE · 34s" — `stored/(draw-gen)` is seconds it — coordinate the timing so Esc clears both cleanly.
left, same math SCREEN uses for dread). Add a test pinning covered-deficit ≠ 3. **Wildlife surfacing** (SIM lands it this round): infestation toasts in house
brownout. voice ("QUANTIZER HOSTS A SWARM. OSHA HAS BEEN NOTIFIED. OSHA IS THE SWARM."),
2. **Selection protocol migration**: write via `bus.setSelection(...)`, drop the inspector shows a MOSQUITO EXPOSURE line on a targeted machine + the speed
`REMOVE_DEF` sentinel export and — important — **delete your own `remove` penalty; a pile nearing hatch (size > 0.8) earns one warning toast, not a nag.
dispatch**: main.ts dispatches it on click now (double-fire today is harmless 4. **Migrate research-granting tests** to the `grantResearch` command once SIM
only because the second one no-ops). lands it (drop the save-blob mutation).
3. **Research UI, the centerpiece**: `T` opens the tech tree (24 nodes, 5 eras), 5. **Verify RENDER's demolition tint + inspect highlight** with your own eyes and
pick → `setResearch`; progress from `snapshot.research` (packs delivered vs screenshot it in NOTES — third round this handshake has floated unverified.
cost); locked machines in the build bar get greyed + padlock + "REQUIRES:
<tech>" tooltip (lock rule in CONTRACTS v4 notes); `researched` event → toast in
house voice ("STANDARD RATIFIED. NEW CRIMES AVAILABLE.") and the padlocks fall
off live.
4. **`?uidemo`** — as approved: boots `__fktryDemo()` after init.
5. **Accent adoption**: read `accent?` first, your derivation second, kind table
third (same precedence as RENDER — one sentence in NOTES confirming you both
read it identically).
**Definition of done:** banner distinguishes RESERVE from BROWNOUT under test; a **Definition of done:** tab strip scrolls; inspect mode round-trips with RENDER
full research run is verifiable in `live.test.ts` (lock visible → research → unlock (screenshot); a swarm's lifecycle is narratable from UI alone (warning → infested →
→ place succeeds); remove mode works through the typed protocol with RENDER's tint. trapped); research tests no longer parse save blobs.
--- ---

View File

@ -62,8 +62,10 @@ export interface MachineDef {
coolPerTick?: number; // v3: per-machine cooling rate; sim default 0.004 coolPerTick?: number; // v3: per-machine cooling rate; sim default 0.004
accent?: string; // v4: art-directed emissive accent; fallback stays derived accent?: string; // v4: art-directed emissive accent; fallback stays derived
// from first output item (renderer + UI share the derivation) // from first output item (renderer + UI share the derivation)
coolRadius?: number; // v4: spatial cooling aura in tiles (chebyshev from footprint); coolRadius?: number; // v4: spatial cooling aura in tiles (chebyshev from footprint).
// this machine's coolPerTick also applies to machines in range // v5 CLARIFIED: auras are ADDITIVE — a machine in range cools at
// its own coolPerTick + sum of in-range auras, capped at the
// exported HEAT_COOL_MAX (see src/sim/constants.ts)
} }
export interface TechDef { export interface TechDef {
@ -128,7 +130,16 @@ export interface SimSnapshot {
// order. May alias a live array — read-only, never // order. May alias a live array — read-only, never
// retain. HARDENS IN v5: update fixtures this round. // retain. HARDENS IN v5: update fixtures this round.
commissionProgress: Record<string, number>; commissionProgress: Record<string, number>;
research?: ResearchState; // v4: research progress (hardens in v5) research?: ResearchState; // v4: research progress (hardens in v6)
wildlife?: ReadonlyArray<WildlifeState>; // v5: piles & swarms (M2's living hazards)
}
export interface WildlifeState {
id: number;
kind: 'dust-pile' | 'mosquito-swarm';
pos: Vec2;
size: number; // pile: accumulation 0..1; swarm: severity 0..1
target?: number; // swarm: entity id it is currently degrading
} }
export interface ResearchState { export interface ResearchState {
@ -146,6 +157,9 @@ export type Command =
| { kind: 'setRecipe'; entity: number; recipe: string | null } | { kind: 'setRecipe'; entity: number; recipe: string | null }
| { kind: 'setRecipeAt'; pos: Vec2; recipe: string | null } // v3: id-free variant | { kind: 'setRecipeAt'; pos: Vec2; recipe: string | null } // v3: id-free variant
| { kind: 'setResearch'; tech: string | null } // v4: pick the lab target | { kind: 'setResearch'; tech: string | null } // v4: pick the lab target
| { kind: 'grantResearch'; tech: string[] } // v5: instant unlock — the
// sandbox/demo verb, also a
// legitimate creative mode
| { kind: 'setPaused'; paused: boolean }; | { kind: 'setPaused'; paused: boolean };
export type SimEvent = export type SimEvent =
@ -157,7 +171,9 @@ export type SimEvent =
| { kind: 'removed'; entity: number; def: string; tick: number } | { kind: 'removed'; entity: number; def: string; tick: number }
| { kind: 'commissionDone'; commission: string; tick: number } | { kind: 'commissionDone'; commission: string; tick: number }
| { kind: 'scram'; entity: number; on: boolean; tick: number } // v2: heat shutdown edge | { kind: 'scram'; entity: number; on: boolean; tick: number } // v2: heat shutdown edge
| { kind: 'researched'; tech: string; tick: number }; // v4: research complete | { kind: 'researched'; tech: string; tick: number } // v4: research complete
| { kind: 'wildlife'; on: 'spawned' | 'cleared'; wildlife: 'dust-pile' | 'mosquito-swarm';
id: number; pos: Vec2; tick: number }; // v5: hazard lifecycle
// ---------------------------------------------------------------- module interfaces // ---------------------------------------------------------------- module interfaces
@ -175,6 +191,7 @@ export interface Sim {
export type SelectionState = export type SelectionState =
| { mode: 'build'; def: string; dir: Dir } | { mode: 'build'; def: string; dir: Dir }
| { mode: 'remove' } | { mode: 'remove' }
| { mode: 'inspect'; entity: number } // v5: open inspector — renderer highlights it
| null; | null;
export interface Renderer { export interface Renderer {

View File

@ -24,20 +24,24 @@ const screen = createScreenFX();
// v4 bridge: _sel keeps the legacy {def,dir} shape (including the deprecated // v4 bridge: _sel keeps the legacy {def,dir} shape (including the deprecated
// '__remove' sentinel) so unmigrated lanes keep working; selection()/setSelection() // '__remove' sentinel) so unmigrated lanes keep working; selection()/setSelection()
// are the typed protocol. The sentinel path is deleted in v5. // are the typed protocol. The sentinel path is deleted in v5.
const bus: UIBus & { _sel: { def: string; dir: Dir } | null } = { const bus: UIBus & { _sel: { def: string; dir: Dir } | null; _state: SelectionState } = {
_sel: null, _sel: null, // legacy mirror (build/remove only) — served for selectedBuild() compat
_state: null, // v5: typed truth, carries inspect mode
dispatch: (cmd: Command) => sim.enqueue(cmd), dispatch: (cmd: Command) => sim.enqueue(cmd),
selectedBuild: () => bus._sel, selectedBuild: () => bus._sel,
pickTile: (x, y) => renderer.pickTile(x, y), pickTile: (x, y) => renderer.pickTile(x, y),
selection: (): SelectionState => { selection: (): SelectionState => {
if (!bus._sel) return null; if (bus._state) return bus._state;
if (!bus._sel) return null; // legacy writer fallback
if (bus._sel.def === '__remove') return { mode: 'remove' }; if (bus._sel.def === '__remove') return { mode: 'remove' };
return { mode: 'build', def: bus._sel.def, dir: bus._sel.dir }; return { mode: 'build', def: bus._sel.def, dir: bus._sel.dir };
}, },
setSelection: (sel: SelectionState) => { setSelection: (sel: SelectionState) => {
bus._sel = sel === null ? null bus._state = sel;
: sel.mode === 'remove' ? { def: '__remove', dir: 0 as Dir } bus._sel =
: { def: sel.def, dir: sel.dir }; sel?.mode === 'build' ? { def: sel.def, dir: sel.dir } :
sel?.mode === 'remove' ? { def: '__remove', dir: 0 as Dir } :
null;
}, },
}; };
// LANE-UI sets the build selection through this hook: // LANE-UI sets the build selection through this hook:
@ -55,10 +59,11 @@ async function boot() {
const sel = bus.selection!(); const sel = bus.selection!();
const tile = renderer.pickTile(e.clientX, e.clientY); const tile = renderer.pickTile(e.clientX, e.clientY);
if (sel?.mode === 'remove') { if (sel?.mode === 'remove') {
// legacy def string kept alongside the mode until renderers migrate (v5 removes it) renderer.setGhost(null, tile, 0, 'remove');
renderer.setGhost('__remove', tile, 0, 'remove'); } else if (sel?.mode === 'build') {
renderer.setGhost(sel.def, tile, sel.dir, 'build');
} else { } else {
renderer.setGhost(sel?.def ?? null, tile, sel?.dir ?? 0, 'build'); renderer.setGhost(null, tile, 0, 'build'); // inspect / no selection: no ghost
} }
}); });
game.addEventListener('pointerdown', (e) => { game.addEventListener('pointerdown', (e) => {
@ -67,7 +72,7 @@ async function boot() {
const tile = renderer.pickTile(e.clientX, e.clientY); const tile = renderer.pickTile(e.clientX, e.clientY);
if (!sel || !tile) return; if (!sel || !tile) return;
if (sel.mode === 'remove') sim.enqueue({ kind: 'remove', pos: tile }); if (sel.mode === 'remove') sim.enqueue({ kind: 'remove', pos: tile });
else sim.enqueue({ kind: 'place', def: sel.def, pos: tile, dir: sel.dir }); else if (sel.mode === 'build') sim.enqueue({ kind: 'place', def: sel.def, pos: tile, dir: sel.dir });
}); });
const MS_PER_TICK = 1000 / TICKS_PER_SECOND; const MS_PER_TICK = 1000 / TICKS_PER_SECOND;
@ -85,13 +90,18 @@ async function boot() {
} }
requestAnimationFrame(loop); requestAnimationFrame(loop);
// dev-only: paste-free demo — builds SIM's reference factory // dev-only: paste-free demo — builds SIM's reference factory. The layout uses
// research-gated ids, so it is a post-research save by definition: grant the techs
// it needs first (testkit rides the public save/load API), then dispatch.
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
(window as any).__fktryDemo = async () => { (window as any).__fktryDemo = async () => {
const { referenceFactory } = await import('./sim/reference'); const { referenceFactory, referenceFactoryTech } = await import('./sim/reference');
const { grantResearch } = await import('./sim/testkit');
const techs = referenceFactoryTech(data);
grantResearch(sim, techs);
const cmds = referenceFactory(data); const cmds = referenceFactory(data);
cmds.forEach((c) => sim.enqueue(c)); cmds.forEach((c) => sim.enqueue(c));
return `${cmds.length} commands dispatched`; return `${techs.length} techs granted, ${cmds.length} commands dispatched`;
}; };
} }
} }

View File

@ -64,7 +64,7 @@ function harness(opts: { unlocked?: boolean; reference?: boolean } = {}) {
const sel = bus.selection!(); const sel = bus.selection!();
if (!sel || !pick) return; if (!sel || !pick) return;
if (sel.mode === 'remove') sim.enqueue({ kind: 'remove', pos: pick }); if (sel.mode === 'remove') sim.enqueue({ kind: 'remove', pos: pick });
else sim.enqueue({ kind: 'place', def: sel.def, pos: pick, dir: sel.dir }); else if (sel.mode === 'build') sim.enqueue({ kind: 'place', def: sel.def, pos: pick, dir: sel.dir });
}); });
if (opts.reference !== false) for (const c of referenceFactory(DATA)) sim.enqueue(c); if (opts.reference !== false) for (const c of referenceFactory(DATA)) sim.enqueue(c);