diff --git a/fktry/lanes/LANE-RENDER.md b/fktry/lanes/LANE-RENDER.md index 55fccc1..ec50d9d 100644 --- a/fktry/lanes/LANE-RENDER.md +++ b/fktry/lanes/LANE-RENDER.md @@ -211,9 +211,19 @@ literal reading, it's a two-line revert in `registry.ts` (`bodyFor`/`accentFor`) Dev scaffold still DEV-gated and absent from `dist/` (verified 0 hits). **DECISIONS** -- **Scram is derived from `heat >= 1`**, not from the `scram` event — the `Renderer` - interface only receives snapshots, never events, so the edge isn't reachable from - here. Works as long as SIM holds heat at 1.0 while latched off. See request 2. +- **Scram is a latch with hysteresis, mirroring SIM's — and this caught a real bug.** + SIM landed heat mid-round, so I read their code: `scrammed` lives on their internal + `Ent`, never on `EntityState`; they deliberately clear `jammed` on scram ("reported by + its own event, not as a flow jam"); and `Renderer` only ever receives snapshots, never + events. So the latched-off state is **not snapshot-visible at all**. A plain + `heat >= 1` test (what I shipped first) un-scrams the moment the machine starts + cooling — and with `HEAT_RESTART = 0.5` and `HEAT_DISSIPATION = 0.004/tick` the sim + keeps it dead for ~125 ticks (**~4 seconds**) while heat falls 1.0 → 0.5. The renderer + would have shown it alive, glowing and animating, for that whole window, every scram. + `entities.ts` now latches at 1.0 and releases below `SCRAM_RELEASE = 0.5`, mirroring + them. Verified: 1.0 → dead; **0.7 → stays dead** (the window that used to lie); + 0.3 → releases and recovers. This is correct today but duplicates their constant — + see request 2. - GLB instances get **cloned materials**; heat/scram is per-entity state and the template's materials are shared, so one hot machine would otherwise tint every copy. Placeholders own geo+mats; GLB clones own only mats. Disposal follows that split. @@ -228,22 +238,34 @@ Dev scaffold still DEV-gated and absent from `dist/` (verified 0 hits). - **Merge pieces don't route cargo per-inlet**: items on a merge belt still ride the straight path, because `BeltItem` doesn't say which side it entered from. Reads fine (a merge's main flow *is* straight) but it's not truthful for the side feed. -- **`reference.ts` doesn't exist** — SIM hadn't landed it when I built, so the DoD's - "reference factory looks ALIVE" was checked against my own devscene bend + showroom - row instead. Happy to re-verify against theirs next round. -- SIM doesn't emit heat/splitters/ids yet, so orders 2/4/5 are verified against the - devscene, not live sim data. All of it is contract-shaped and will light up as SIM - lands; nothing in my lane needs to change. - Corner arc assumes a 1×1 belt. Fine per contract (`beltSpeed` belts are 1×1). +**DoD — verified against SIM's actual `referenceFactory`** (it landed while I was +building; I loaded it in the dev page and dispatched all 114 commands): +- **85 belts classify as 80 straight / 3 cornerA / 1 cornerB / 1 merge**, 25 machines — + five pieces that round 1 would have drawn as flat straight plates. +- **Cargo flows with `exactIds: true`** — SIM stamps `BeltItem.id`, so the exact + interpolation path is live on real sim data, not just the scaffold. +- Ore renders `#4b4b62` (round 1's legibility floor lifting DATA's `#1a1a22`), so the + reference factory's first product is visible on its belts rather than a black smudge. +- **4.1ms/frame** with the whole reference factory up. +- Note for whoever plays it: a hand-placed extractor produces nothing until you also + `setRecipe` it — `place` alone leaves `recipe: null`. Not a bug, but it's why a + quick manual line looks dead. The reference factory sets all four itself. + **CONTRACT REQUEST** 1. **Settle `MachineDef.color`'s meaning** (see the top of this entry). My proposal: keep DATA's values as-authored and re-comment the field as chassis/body colour, then add `accent?: string` if art-directing the emissive is ever wanted. Right now the contract comment says "accent", DATA authored bodies, and I've followed the data. -2. **`EntityState.scrammed?: boolean`** — I derive scram from `heat >= 1`. If SIM ever - cools a machine while it's still latched off, the visual un-scrams early and lies. - A flag makes it unambiguous and costs one bool. +2. **`EntityState.scrammed?: boolean`** — now CONFIRMED necessary, not speculative (see + DECISIONS). SIM's scram latch is invisible to the snapshot, and the only way I can + render it correctly is by duplicating their `HEAT_RESTART = 0.5` as my own + `SCRAM_RELEASE`. That works, but it silently desyncs the moment SIM retunes their + constant — a rendering lie with no test to catch it. One bool on `EntityState` kills + the duplication outright. (Alternative if you'd rather not widen the state: give + `Renderer` the events, like `ScreenFX` gets — but the bool is smaller and snapshots + staying self-sufficient is a nice property.) 3. Not urgent: `BeltItem.side?: Dir` (which edge cargo entered from) would let merge pieces route their side feed honestly. Only worth it if merges get common. diff --git a/fktry/src/render/entities.ts b/fktry/src/render/entities.ts index 8479c55..1c64355 100644 --- a/fktry/src/render/entities.ts +++ b/fktry/src/render/entities.ts @@ -21,6 +21,19 @@ import { createHeatHaze, type HeatHaze } from './heat'; /** Heat is throttled above this (contracts: "throttles >0.7, scrams at 1"). */ const THROTTLE_AT = 0.7; const THROTTLE_FLOOR = 0.25; +/** + * Scram releases below this — mirroring LANE-SIM's `HEAT_RESTART`. + * + * Scram is a LATCH in the sim, but it lives on its internal `Ent`, never on + * `EntityState`; it clears `jammed` on scram ("reported by its own event, not as a flow + * jam"), and `Renderer` only ever receives snapshots, never events. So the latched-off + * state is not snapshot-visible, and a plain `heat >= 1` test un-scrams the moment the + * machine starts cooling — showing it alive and glowing for the ~125 ticks (~4s) it + * spends dead between 1.0 and HEAT_RESTART. Mirroring the sim's hysteresis here is + * correct today but couples this constant to theirs; `EntityState.scrammed` would kill + * the duplication. Filed as a CONTRACT REQUEST. + */ +const SCRAM_RELEASE = 0.5; /** Used when LANE-DATA hasn't set `bufferCap` yet — tanks still read as tanks. */ const DEFAULT_BUFFER_CAP = 100; const HEAT_RED = new THREE.Color(0xff3a12); @@ -52,6 +65,9 @@ interface Rec { idleClock: number; heat: HeatTarget[]; all: HeatTarget[]; + /** Latched scram state, mirroring the sim's (see SCRAM_RELEASE). */ + scramLatch: boolean; + /** Whether dead-dark is currently applied, so restart knows to restore. */ wasScrammed: boolean; } @@ -108,7 +124,10 @@ export class EntityLayer { } const heat = THREE.MathUtils.clamp(e.heat ?? 0, 0, 1); - const scrammed = heat >= 1; + // Latch with hysteresis, exactly as the sim does — see SCRAM_RELEASE. + if (heat >= 1) rec.scramLatch = true; + else if (heat < SCRAM_RELEASE) rec.scramLatch = false; + const scrammed = rec.scramLatch; // Throttle ramps in rather than snapping, so "labouring" reads before "dead". const speed = scrammed ? 0 @@ -239,6 +258,7 @@ export class EntityLayer { idleClock: 0, heat, all, + scramLatch: false, wasScrammed: false, }; this.live.set(id, rec);