Routed the pan into a real order: day 16 'pan-sear the mushrooms on the
gas' — butter in, wait for the FOAM not the smoke, land the food, brown
both faces, baste in the noisette. judgePan scores THE SEAR / BOTH SIDES
/ THE BUTTER with a butter-snob line bank. New scorecard group THE PAN;
the cast-iron skillet GLB renders with a fuel-coloured flame.
Balance fix (the floor): food shields the butter once it lands, so a
clean cook keeps the butter at noisette instead of racing to burnt — a
full sear is now winnable. Emergent fuel character kept: induction sears
both sides dead-even (0.016), gas flares the first side so you turn it
early (0.085) — both under the 0.1 bar.
Verified live: day 16 routes to the pan, plays end to end, grades A;
judge nails it ('cooked in different postcodes... foamed not burnt, you
were watching'). Cast-iron pan + butter + searing mushroom on screen.
Heat brief M-H1..M-H6 + M-H4 all shipped and live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
199 lines
24 KiB
Markdown
199 lines
24 KiB
Markdown
# TOASTSIM — Build Brief: THE HEAT SOURCE & ENVIRONMENT DOCTRINE
|
||
|
||
*Read this and execute it. It works cold for an Opus 4.8 session with no other context. It is a CROSS-CUTTING doctrine, not a single phase — it is the layer the pan (P2/M17), the pot & fryer (P6), and the torch & grill (P9) all ride. Read MASTERPLAN §1–§3 (design law + bones), then KITCHEN-ATLAS §2/§5/§7.5, then OPUS-BUILD-BRIEF-3 §4 (the pan — committed, not yet built), then this file top to bottom. Build the doctrine's core (§2, §6 M-H1/H2) BEFORE or ALONGSIDE M17 so the pan is born fuel-aware; retrofit oven/toaster in the same pass; ship the rest as the phases that need it arrive.*
|
||
|
||
## 0. STATUS (update this section as you go — a cold session reads here first)
|
||
|
||
- **M-H1 + M-H2 DONE & LIVE (2026-07-19).** `src/sim/heatsource.ts` (the bone) + the golden-preserving oven retrofit + the gas-oven day are shipped. Verified in-browser: **golden** — no-source oven reads mean 0.698 / blister 1.0 / collapse 0, byte-identical to HEAD. **M-H1 chase 3→9** — gas 0.433s, electric 6.0s, induction 0.133s (distinct, no overlap). **Residual after 9→0** — gas 0 @1s, electric 6.6 @6s, charcoal 9 @30s. **M-H2** — gas oven stdev 0.1207 vs fan oven 0.0016 at equal mean 0.6001 (75× ratio; bar was 2×). Day 14 bruschetta now roasts on `oven_gas` and still grades **S (9.62)**, HUD shows "GAS OVEN · blue flame". Harness: `t.heatChase`, `t.residual`, `t.ovenStdev`, `t.ovenGolden`, `t.ovenDemo`.
|
||
- **M-H3 DONE (2026-07-19).** Environment sim (gust walk, flame perturbation, ferrous rejection, indoor smoke→ZAP). Verified: windy gas flame stdev 0.2635 (bar >0.05) and mean drops to 5.66 (wind steals heat); sheltered stdev 0.0065 (bar <0.02, the dodgeable floor); induction-in-wind stdev 0 (no flame); the ZAP fires exactly once indoors, never outdoors. `newOutdoorEnv`, `addSmoke`, `SMOKE_ZAP`. Harness: `t.envVar`, `t.zapTest`.
|
||
- **M-H5 (noise half) DONE.** cheap_electric hob stdev 0.1244 (bar >0.12) vs induction 0.0198 (bar <0.04) vs fan 0.0064. Harness: `t.hobNoise`. The "both reach 10/10" half needs the M17 pan to cook on — deferred with it.
|
||
- **M-H6 DONE & LIVE (2026-07-19) — the charcoal grill, a heat-bed you arrange.** `src/sim/charcoal.ts` (bankCoals paints a 2D heat bed, zoneDecayStep only cools it, food sears by the coals beneath it, a rendering piece over hot coals flares and chars until you move it) + `src/scenes/grill.ts` (top-down open grill, the bed rendered as an additive glow, food dragged between the sear zone and the cool lee, flames lick up on flare) + the routed **day 15 BBQ order** with `judgeGrill` (THE SEAR / THE CHAR / THE GRILL) and the char-snob line bank. Verified: two-zone bed stdev 0.303; bed ceiling monotone-decays 1.68→0.61/90s; flare dodge — left over coals = done 2.08/31 flares (carbon), pulled to the lee = 0/0 clean; **the full day plays and grades S (9.4/10)** with careful sear-then-pull, F if you let it flare. Retune: RENDER_AT 0.62 (above the good-sear window's floor) so a clean sear is reachable with no flare — the floor holds (active play wins, neglect chars). Harness: `t.zoneStdev`, `t.zoneDecay`, `t.grillTwoZones`, `t.grillFlareDodge`, `t.grillDemo`, `t.grillDay`.
|
||
- **M17 + M-H4 DONE & LIVE (2026-07-19) — the pan, born fuel-aware.** `src/sim/pan.ts` (butter state machine cold→melting→foaming→noisette→burnt keyed on pan temp × time; per-side sear Fields; flip; baste paints hot butter up the up-face; food shields the butter once it lands so a clean cook is winnable; rendered fat flares a flame, never induction) + `src/scenes/pan.ts` (the cast-iron skillet GLB, a fuel-coloured flame, butter that foams and browns, food you flip and baste) + the routed **day 16 pan-sear order** with `judgePan` (THE SEAR / BOTH SIDES / THE BUTTER) and the butter-snob line bank. Verified: foaming window shrinks with heat (10.4/6.0/4.6s @4/7/9); well-timed flip → sides within 0.016 on induction, 0.085 on gas (gas flares one side, so you turn it earlier — the fuel character); never flip → 1.2 burnt / 0.23 raw; baste noisette flags the bonus (up 0.07→0.43), burnt-butter baste writes bitterness 0.48; **M-H4**: noisette on gas 11.0s vs electric 14.4s, induction output pinned at knob 8 with fat searing (never flares). The day plays and grades A; judge lines land ("These two faces were cooked in different postcodes. Good butter, foamed not burnt. You were watching. I noticed."). Harness: `t.panButter/panFlip/panNoFlip/panBaste/panFuel/panDay`.
|
||
- **THE HEAT BRIEF IS COMPLETE: M-H1 through M-H6 + M-H4 all shipped and live.** Remaining heat ideas are content/polish: environment bolted onto the grill (wind/windbreak already in `heatsource.ts`), wok-hei on the pan, the induction "wrong pan" comedy day, charcoal ash-in-the-sear. The bone carries them all.
|
||
- Presupposes the M16 salt doctrine and the M17 pan skeleton from Brief 3; those aren't in yet, so §2 ships as a standalone `heatsource.ts` (zero dependency on the pan) and M17 will read it from birth.
|
||
- **LIVE 🌐** https://partly.party/toastsim/ — `bash scripts/deploy.sh` (build `--base=/toastsim/` → rsync staging → docker cp into `forum-nginx`). Curl cache-busted after every deploy.
|
||
- Seed lane for this brief's assets: **680–719** (open gap after P12's 630–679). A `do_heat()` lane (cooktops/pans/BBQ/utensils) was already fired at seeds 181–219 for the general cast; the 680–719 sprites (flames, glows, flare, ZAP) remain.
|
||
|
||
## 0b. THE PITCH (one paragraph)
|
||
|
||
Today every heat surface in TOASTSIM maps its dial *straight* into the browning integrator — `roastStep` reads `s.power`, `toastStep` reads `power`, and the number the knob commands is the number the food gets. That is a lie no real cook believes. This doctrine interposes ONE new bone, a `HeatSource`, between the knob and the integrator: the knob sets a **target**, an **output** chases it on a fuel-specific lag, and every station reads `output` where it read `power`. Gas is glued to your hand, an electric coil cooks its own future, induction is eerily exact and rejects the wrong pan, charcoal has no knob at all and is a heat-zone Field you *arrange*. Wrap that in an `Environment` — indoor is a promise, outdoor is weather that steals heat off the windward edge — and the four axes SOURCE ⟂ MEDIUM ⟂ VESSEL ⟂ ENVIRONMENT become pure data multiplied into the *same* `power → heat → dry → roast` pipeline. No new integrator. New primitives are two small structs and a first-order ease; everything else is content.
|
||
|
||
## 1. THE HEAT SOURCE & ENVIRONMENT DOCTRINE — the cross-cutting rule
|
||
|
||
State it as crisply as the salt doctrine ("salt is two objects and a timing question") and the butter doctrine ("cold → foaming → noisette → burnt"):
|
||
|
||
> **Every heat surface has a FUEL and lives in an ENVIRONMENT. The knob commands a target; the fuel decides how fast the heat obeys; the environment decides how much of it survives the trip to the food. You never cook the knob — you cook the lag.**
|
||
|
||
Four laws follow, and nothing in any phase may break them:
|
||
|
||
1. **The knob is not the heat.** No station reads its dial into the integrator anymore. It reads `hs.output`. `output` chases `target` on the fuel's `riseRate`/`fallRate`. The gap between what you asked for and what you're getting IS the skill (the pan's "the lag IS the skill", generalized to every burner).
|
||
2. **No meter. The state reads diegetically.** Blue gas cone, orange coil glow, silent induction, coal-bed glow — never a temperature bar. The butter's foam and the food's sear ARE the readout. The dial can lie (electric residual, induction rejection); the food never does.
|
||
3. **Floors, not lotteries (WA-4, MASTERPLAN).** Hot-spots, wind, flare-ups, empty bottles, altitude — all add NOISE and dodgeable EVENTS to the Field. None may randomly burn a correct cook. Every hazard has a diegetic warning and a physical dodge (turn the pan, raise the windbreak, move the food across the gradient). Careful, lag-aware play reaches 10/10 on the worst gear in the game.
|
||
4. **SOURCE ⟂ MEDIUM ⟂ VESSEL ⟂ ENVIRONMENT are orthogonal data axes.** A fuel is a profile. A pan is a profile. A wind is a profile. Combinations (gas + wok + windy beach) are *content the day table composes*, never new code. Adding the fifth fuel or the tenth environment must touch zero integrator lines.
|
||
|
||
## 2. THE DATA MODEL — `src/sim/heatsource.ts` (pure: Field + Rng only)
|
||
|
||
The SOURCE sits between the knob and the existing integrator. It owns a scalar that chases the setpoint on the SAME first-order shape `TempClock.step` already uses (climb toward target by rate·dt; if above, fall), retargeted from fridge/bench(0/1) to the knob's setpoint. This is the only new math, and it is not new — it is `TempClock`'s drift and `coolStep`'s decay, generalized.
|
||
|
||
```ts
|
||
export type Fuel = 'gas' | 'electric' | 'induction' | 'charcoal' | 'radiant';
|
||
|
||
export interface HeatSource {
|
||
fuel: Fuel;
|
||
target: number; // 0..10 — what the knob commands (charcoal: the bed level)
|
||
output: number; // 0..10 — what actually reaches the vessel; every station reads THIS
|
||
riseRate: number; // units/sec climbing toward target
|
||
fallRate: number; // units/sec falling toward target (LOW = residual heat that won't let go)
|
||
maxOut: number; // powerMax clamp (cheap hobs top out below 10)
|
||
evenNoise: number; // amplitude blended into the station's heat-map bias (hot-spots)
|
||
hotspot: number; // radial concentration term (a burner ring, a coal pile)
|
||
flame: boolean; // has a flame → wind steals from output, fat can flare it
|
||
needsFerrous: boolean; // induction rejects a non-ferrous vessel → output pinned at ambient
|
||
flareGain: number; // how hard rendered fat spikes output (gas high, induction 0)
|
||
zone?: Field; // charcoal only: a 2D heat-zone gradient you arrange (itself a Field)
|
||
}
|
||
```
|
||
|
||
`setKnob(hs, k)` → `hs.target = clamp(k, 0, hs.maxOut)`. Charcoal ignores it: `target` is the fuel-bed ceiling and `stoke(hs)` bumps it. The step:
|
||
|
||
```
|
||
export function heatStep(hs, env, dt, fatLoad = 0) {
|
||
const t = hs.flame ? hs.target * (1 - 0.35 * env.gust) : hs.target; // wind steals flame
|
||
if (hs.needsFerrous && !env.vesselFerrous) { hs.output = env.ambient; return; } // dead pan
|
||
if (hs.output < t) hs.output = Math.min(t, hs.output + hs.riseRate * dt);
|
||
else hs.output = Math.max(t, hs.output - hs.fallRate * dt); // residual coast
|
||
hs.output = Math.min(hs.maxOut, hs.output + hs.flareGain * fatLoad); // flare on fat
|
||
if (hs.zone) zoneDecayStep(hs.zone, dt); // coal-bed cools
|
||
}
|
||
```
|
||
|
||
**`FUELS` profiles** (tune headless against §6 bars; shipping targets, not gospel):
|
||
|
||
| Fuel | rise | fall | maxOut | evenNoise | flame | needsFerrous | flareGain | Feel |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| `gas` | 12 | 14 | 10 | 0.10 | ✓ | – | 3.5 | settles <1s, no memory |
|
||
| `electric` | 1.1 | 0.8 | 9 | 0.28 | – | – | 0 | 6–10s lag, long residual |
|
||
| `induction` | 20 | 20 | 10 | 0.03 | – | ✓ | 0 | instant, flattest, dead to wrong pan |
|
||
| `charcoal` | — | 0.02 | (zone) | 0.35 | ✓ | – | 4.0 | no knob; a 2D bed, decays |
|
||
| `radiant` | 0.9 | 0.5 | 10 | 0.14 | – | – | 0.5 | top-hot; what toaster & electric oven ARE |
|
||
|
||
`applyToHeatMap(heat, hs, rng)` is the only spatial hook: it scales the station's existing `Float32Array` and re-blends the bias amplitude to `hs.evenNoise` plus a `hotspot` radial term — the `amp = 0.12 + 0.42·(1 − quality)` slot in `buildHeatMap`, now driven by fuel instead of bread brand. Charcoal writes its `zone` Field in directly (§3).
|
||
|
||
**`Environment` — same file:**
|
||
|
||
```ts
|
||
export interface Environment {
|
||
place: 'indoor' | 'outdoor';
|
||
ambient: number; // the temp TempClock/coolStep pull toward
|
||
wind: number; // 0..1 base
|
||
gust: number; // 0..1 live, walks toward wind on value-noise
|
||
sheltered: boolean; // lid / windbreak / lee side → gust forced to 0 (dodgeable floor)
|
||
vesselFerrous: boolean; // does the mounted pan take induction
|
||
smoke: number; // indoor only: accumulates → fires the ZAP once
|
||
rng: Rng;
|
||
}
|
||
```
|
||
|
||
`envStep(env, dt)` walks `gust` via value-noise toward `wind` (0 if `sheltered`/`indoor`). Wind does exactly two things, both existing knobs: perturbs flame `output` in `heatStep`; accelerates surface cooling (`coolStep` `tau /= (1 + 1.4·gust)`). `sheltered` zeroes gust → wind is NEVER a lottery; it is a floor you dodge. Indoors `wind=0` but `smoke` climbs from burnt butter/food and fires the **ZAP** once (comedy, no damage).
|
||
|
||
**Exports (documented in-file, the `juiceResult()` pattern):** `Fuel, HeatSource, Environment, FUELS, HEATSOURCE_SKUS, newHeatSource(fuelOrSku, seed), setKnob, stoke, heatStep, envStep, applyToHeatMap, bankZone` (charcoal).
|
||
|
||
**Retrofit is opt-in and golden-preserving.** `roasting.ts`/`toasting.ts` gain an optional `src?: HeatSource`; the step reads `src?.output ?? power`. With no source passed, every existing oven and toaster judged number is byte-for-byte unchanged (§6 M-H2 golden test). Nothing already shipped moves.
|
||
|
||
## 3. THE FEEL per fuel — the must-feel moments
|
||
|
||
**GAS — the live wire.** `output` glued to the knob (`rise 12`); blue cone visible, `maxOut` to the clamp. **Must-feel — the gas scorch:** one nudge to chase a stalling first side and `output` is instantly there — butter clears foaming → noisette → burnt in one breath. The recovery is the lesson: yank the pan off, heat dies on contact (`fall 14`, no residual). Punishes and forgives at the same speed.
|
||
|
||
**ELECTRIC COIL — the ring remembers.** `rise 1.1 / fall 0.8`. Turn the dial *down* and `output` keeps coasting for ten-plus seconds while the sear climbs past target. No flame: the dial lies, the food is truth. **Must-feel — the coil that won't let go:** spin the dial to 3, nothing changes for ten seconds; your only save is lifting the pan and riding carryover. Make the turn-down BEFORE you need it.
|
||
|
||
**INDUCTION — eerie precision, dead pans.** `rise 20 / fall 20`, `evenNoise 0.03` — the flattest sear in the game. Gated by `needsFerrous`: wrong pan → `output` never leaves `ambient`, readout stone-cold, dial at 8, silence. **Must-feel — the induction ZAP:** wrong pan, flat sear, *beep beep beep*; swap and it snaps to setpoint in a heartbeat. Comedy by absence.
|
||
|
||
**CHARCOAL — you build it.** No knob. Banking coals paints the 2D `zone` Field — a screaming-hot side, a cool side to park on. The ceiling decays on `zoneDecayStep` ("sear early, hold late"). Fat drip spikes a local texel — a **flare** you dodge by moving the food. The fan (`stoke`) pumps it back up; the lid raises `ambient` and traps it.
|
||
|
||
**RADIANT — the element, honest and top-biased.** `rise 0.9 / fall 0.5`, top-hot bias, no flame. This is what the toaster and electric oven ALREADY are — the retrofit names them. Patience: climbs slow, holds.
|
||
|
||
## 4. THE ENVIRONMENTS — ambient modifiers + comedy
|
||
|
||
**INDOOR is a promise.** `ambient` constant, `wind=0`, sources behave as specced — the clean bench to learn each curve. The one hazard is `smoke`: burnt butter/food accumulates it, past threshold the **ZAP** fires once (a `Mess`-style event → `result.bench` + a judge line; no temp damage).
|
||
|
||
**OUTDOOR is weather.** `envStep` walks `gust` toward `wind`; wind steals from flame `output` on the windward half and accelerates `coolStep`. All dodgeable via `sheltered`. Three flagship stages, each ONE new pressure:
|
||
|
||
- **Backyard BBQ** (flagship). Gas bottle with a hidden `fuel` reserve that reads only diegetically — flame shrinks blue → sad yellow, then floors mid-service. **New pressure: fuel is finite.** Flare-ups when fat hits flame (local `output` spike, chars in ~3s, dodge by moving). Wind subtracts from the windward half. The neighbour, a char snob, leaning on the fence.
|
||
- **Camp stove.** Tiny tippy burner — narrow `heatDisc`. Altitude lowers `maxOut` (water never boils; `dry` gate stalls). One gust and the flame *goes out*. **New pressure: no dial you can trust.**
|
||
- **Beach / park.** Sand tilts the pan so fat pools (`heat × dry` asymmetry). Constant low breeze. A seagull. **New pressure: the tilt.**
|
||
|
||
Charcoal's outdoor gusts blow **ash** into the sear — a `Mess` spatter, not a temp hit (WA-4 kept).
|
||
|
||
## 5. UTENSIL EXPANSION that pairs with heat (procedural vs generated)
|
||
|
||
The VESSEL is the third orthogonal axis. Each vessel is a small data profile (`heatDisc` radius, `ferrous` flag, `tilt` susceptibility, `wallHeight` for boil-over) multiplied into the same map — a heavier pan literally smooths the `evenNoise` Field (P12), never adds a stat.
|
||
|
||
- **Pans** (cast-iron — M17 hero; carbon-steel; thin cheap). GENERATED props; `heatDisc`/`ferrous`/`tilt` PROCEDURAL.
|
||
- **Woks** — bowl `heatDisc`, hot centre / cool rim, the wok-hei window. GENERATED prop; toss + peak-window PROCEDURAL.
|
||
- **Spatulas / tongs / basting spoon** — GENERATED (tongs seed 217); flip/baste/grip PROCEDURAL (they write to the Field).
|
||
- **BBQ tools** — long tongs, grill fork, coal fan, chimney, lid. GENERATED; `stoke`/`sheltered`/light-time PROCEDURAL.
|
||
- **Pots / camp burner / trivet** — GENERATED; `wallHeight` boil-over + coin `heatDisc` PROCEDURAL (seed P6).
|
||
|
||
Rule, unchanged: **the prop is art, the Field is sim.** Judged numbers stay procedural; chrome/marbling/lids are generated.
|
||
|
||
## 6. MILESTONES with harness-measurable exit bars (these PROVE the fuels differ)
|
||
|
||
Add `src/dev.ts` helpers driving REAL input (`tap('Space')`, `run`, `point`; study `t.chop`/`t.juice`/`t.roast`): `t.heatChase(fuel, from, to) → sec`, `t.residual(fuel) → sec`, `t.hobNoise(sku) → stdev`, `t.zoneStdev()`, `t.envVar(fuel, wind)`. Verify each fuel first in a `dev-stove.ts` sandbox like grate. Typecheck + build stay clean; one commit per milestone, message = measured numbers.
|
||
|
||
- **M-H1 — the core ease.** `t.heatChase('gas',3,9) < 1.0s`; `('electric',3,9)` 5–9s; `('induction',3,9) < 0.15s`. Residual after 9→0: gas `output<1` within 1s, electric still `>6` at 6s, charcoal still `>5` at t=30s. **Bar:** three rise + three residual curves numerically distinct, no overlap.
|
||
- **M-H2 — retrofit, golden-preserving.** Gas-oven roast `stdev > 2×` electric-fan roast at equal `mean`. **Golden test:** running the existing oven and toaster with `src` omitted leaves every shipped judged number identical to HEAD — zero drift.
|
||
- **M-H3 — environment.** Outdoor gas at `wind=0.6`: `output` variance `>0.05`; `sheltered:true` drops it `<0.02` (dodgeable floor proven). Indoor high-`smoke` fires the ZAP exactly once. Charcoal ash registers as `result.bench`, not a `roast` delta.
|
||
- **M-H4 — pan / butter / flare.** Butter → noisette on gas ~3s faster than electric at the same knob. Electric's post-zero `∫output dt` tips butter cold→burnt on coast alone. Induction `flareGain·fatLoad == 0`. Gas flare spikes one zone, chars in ~3s, DODGEABLE.
|
||
- **M-H5 — economy is noise, never tax.** Cheap-electric SKU `t.hobNoise > 0.12` vs induction `< 0.04`; BOTH reach 10/10 under lag-aware play. The WA-4 / P12 promise, measured.
|
||
- **M-H6 — charcoal is a Field, not a dial.** `t.zoneStdev()` after banking a two-zone bed `> 0.25`; ceiling monotonically decays; food on the cool zone holds below sear while the hot zone chars — one bed, two outcomes by placement.
|
||
|
||
## 7. How it composes with the already-planned systems
|
||
|
||
- **The pan (P2 / M17).** M17 reads `hs.output` not its knob. Fuel-aware from birth: `PanSession` holds a `HeatSource` + the shared `Environment`. The butter state machine keys on `∫output dt`, so gas overshoots to noisette fast and electric's residual keeps butter cooking after the knob is off — the fuel drives the "moment" for free. The `~8s` committed lag IS the electric profile.
|
||
- **The oven (M14 `roasting.ts`).** Two profiles over the SAME `roastStep`: gas = `radiant` + top-weighted gradient (`v>0.7`) + `flame`; electric fan = flat, low `evenNoise`. Pure data on `heat[]`; golden test proves default untouched.
|
||
- **The toaster (`toasting.ts`).** Retrofit as `fuel:'radiant'`: feed `hs.evenNoise` into `buildHeatMap`'s `amp` slot, pass `hs.output` to `toastStep`. Stripes stay; a cheap toaster is higher `evenNoise`.
|
||
- **Salt & butter (M16).** Rendered fat is the `fatLoad` into `heatStep`: flame flares, induction never does. Salt's moisture-draw on `dry` is unchanged.
|
||
- **P12 career (buy better hobs).** `HEATSOURCE_SKUS` maps SKU → profile override: cheap thin electric = `{maxOut:8.2, evenNoise:0.42, hotspot:0.5, riseRate:0.9}` — hot-spot NOISE + slow chase, never a flat tax. Induction is aspirational (flat, silent) with the vessel-match catch. Every SKU: careful play still reaches 10/10.
|
||
|
||
## 8. JUDGE LINES (house voice, deadpan — bank 14, expand per day)
|
||
|
||
1. "You cooked this on an electric coil. I can taste the patience it required of you."
|
||
2. "There's no char on this. There's a *memory* of char. A rumour."
|
||
3. "Induction. Even. Correct. Joyless. Like a well-run dentist's office."
|
||
4. "The wind took the left three. You didn't. Notice."
|
||
5. "This has wok-hei. I did not expect wok-hei from a man with a domestic extractor fan."
|
||
6. "Gas. You panicked at the whoomp and never recovered. It's fine. We all hear the whoomp."
|
||
7. "One side is Sunday. The other side is a house fire. Commit to a Tuesday."
|
||
8. "The bottle ran out at minute four. I know because minute five tastes like giving up."
|
||
9. "You seared this over charcoal you arranged with genuine care. I'm almost moved."
|
||
10. "Blistered, not black. You waited through the boring part. That's the whole job."
|
||
11. "Flare-up. You let the fat conduct the orchestra. It chose *inferno*."
|
||
12. "This never got hot. Altitude, or nerve — either way, it steamed."
|
||
13. "You put cast iron on induction and stood there. It beeped at you. It was right."
|
||
14. "Perfectly even, corner to corner. On a *grill*. You've smuggled a laboratory outdoors."
|
||
|
||
(Wire through `Criterion{key,label,score,weight,group}`; ZAP/empty-bottle events read into `result.bench` like `Mess`. Each shipped fuel/environment day deserves 8+ lines in its own group.)
|
||
|
||
## 9. ASSET LANES — MODELBEAST (seeds 680–719; the 181–219 cast is already fired)
|
||
|
||
The general heat cast (gas/electric/induction hobs, camp stove, cast iron, wok, saucepan, kettle & gas BBQ, spatula, wooden spoon, tongs, ladle) was fired at **seeds 181–219**. The 680–719 lane is the remaining SPRITES + specialised props:
|
||
|
||
- 3D: kettle grill lid-closed pair (691), chimney starter (693), coal fan/bellows (695), gas bottle full+empty tell pair (697/698), windbreak screen (708), neighbour's fence rail (711).
|
||
- 2D: blue-cone flame frames (713), sad-yellow low-fuel flame (714), coil orange-glow ramp (715), flare-up flash (716), ash-gust spatter (717), smoke-alarm ZAP flash (718), wind streak overlay (719).
|
||
|
||
**Stays PROCEDURAL:** the charcoal `zone` Field, every sear/doneness Field, the flare's local spike, hot-spot noise. Prop is art, Field is sim.
|
||
|
||
## 10. RECOMMENDED FIRST BUILDABLE SLICE (being built now)
|
||
|
||
**Build `src/sim/heatsource.ts` + the OVEN retrofit first — NOT the pan-with-fuel from scratch. Ship a "gas oven vs electric fan oven" day.**
|
||
|
||
Justification: the pan (M17) isn't on disk and is P2's biggest integrative build; coupling a new bone to a new station doubles the bug surface. The oven ALREADY ships (`roasting.ts`, M14) with a `power`-scaled `heat` Field and a centre-hot gradient — interposing `HeatSource` there is a `src?.output ?? power` change with a golden test proving byte-identical default. "Gas hot-at-top vs electric fan even" is pure data on `heat[]`, proving the doctrine (two fuels, distinct `stdev` at equal `mean`) with zero new scene and one order flag. It de-risks M17: the pan then reads a measured, gate-verified `heatsource.ts`.
|
||
|
||
Concretely: (1) `heatsource.ts` with `FUELS` for `radiant`/gas-oven, `heatStep`, `applyToHeatMap`, no `Environment` yet. (2) `roasting.ts`: optional `src?`; `src?.output ?? power`; golden test green. (3) `dev.ts`: `t.heatChase`, `t.residual`, gas-vs-fan `stdev`; hit M-H1 (partial) + M-H2. (4) One day-table entry handing the same tray to gas vs fan oven; judge lines. (5) Deploy, curl, screenshot both scorecards.
|
||
|
||
Then M17 is born fuel-aware, `Environment` bolts on for BBQ/camp, and charcoal (M-H6) is the one genuinely new scene — the showpiece, built after the bone is proven.
|
||
|
||
---
|
||
|
||
**The bet:** the knob was always lying. This doctrine makes the lie a mechanic — gas obeys your hand, electric cooks its own future, induction is exact and unforgiving of the wrong pan, charcoal has no hand to obey. Four fuels, two environments, one first-order ease on a scalar the game already integrates. If the coil that won't let go, the gas scorch, and the induction ZAP all *feel* like the same lesson taught three ways — you never cook the knob, you cook the lag — then every burner TOASTSIM will ever add is already content.
|