[render] scram latches with hysteresis, mirroring sim (fixes a ~4s visual lie)
SIM landed heat this round, so I read their implementation: `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 earlier today) therefore un-scrams the
moment a machine starts cooling. With HEAT_RESTART=0.5 and dissipation
0.004/tick, the sim holds it dead for ~125 ticks (~4s) while heat falls 1.0 ->
0.5 — the renderer showed it alive, glowing and animating, that whole window.
Now latches at 1.0 and releases below SCRAM_RELEASE=0.5, mirroring them.
Verified: 1.0 dead -> 0.7 STAYS dead -> 0.3 releases and recovers.
This duplicates SIM's constant, which is the wrong place for it to live —
EntityState.scrammed is filed as a contract request with the evidence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
30deed10dc
commit
dc1591d0e6
@ -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.
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user