toastsim/src/sim/ingredients.ts
type-two cc39478a6e M15 bruschetta capstone: roast → assemble → the sog finger-press, day 14 end to end
The multi-station order, on the proven bones. Cut the sourdough (M10) →
toast (M1) → roast the tomatoes (toaster generalized) → assemble (garlic rub,
topping point-set, sog clock) → serve. Judge groups The Bread / The Topping /
The Bench. Planning is the mechanic: the temperature clock drifts the two
cheeses on real service time.

What I built on top of the partial work (sims + judge criteria + lines were
already in place and good — kept them):
- Temperature clock WIRED (sim/tempclock.ts): TempClock armed on bruschetta
  days, steps every tick while timing; a per-perishable fridge/bench toggle UI
  (game.showTempToggle) that reads the judge's own readiness word live; a
  toggle costs 8s service (reopening the fridge has teeth). Ticket hints:
  "get the brie out now", "leave the parmesan in".
- Roast tomatoes WIRED: OvenView routed after the toast lands on bruschetta
  orders (kitchen.onPopped branch → enterOven). Browning Field reused, blister
  past 0.6, collapse to sauce past 0.9. oven_tray.glb is absent → loadProp
  catch-fallback to the procedural tray (graceful, as designed).
- Assembly scene BUILT (scenes/assembly.ts, new): own toasted garlic-rubbed
  slice; G rubs the clove (one-swipe coverage), 1/2/3 drop tomato/cheese/basil
  as a point set; ENTER serves. Distribution scored by Clark-Evans (the rind
  code, fed the topping list); balance by mass-per-area band.
- Sog clock: rate x wet-mass x moisture / thickness — the doorstop resists,
  the thin slice surrenders; the judge presses a finger ("It bends. Toast
  should not bend.").
- Day 14 order added; enterOven/enterAssembly/serveBruschetta follow the exact
  runNextPrep/enterPrep/enterJuice machinery. Scorecard grouped
  bread/topping/bench via judge()'s existing group path.
- Harness (src/dev.ts): t.roast(s,power), t.rub(passes), t.assemble({...}),
  t.sogWait(s), t.tempToggle(id), and t.bruschetta() happy path returning the
  full grouped stats.

Fixes to the partial work: judge.ts shadowed the `order` param with a local
`const order` (rename → groupRank; would not typecheck); oven.ts material
opacity access typed as Material|Material[] (cast once).

Tuning (measured headless — the pure sims run without three.js):
- assembly SOG_RATE 0.006 → 0.004: doorstop (0.31) under a normal wet load
  (5 tomato ~2.1) served promptly (8s) stays CRISP (sog 0.21); dawdled 30s goes
  soft (0.77); a thin slice (0.12) bends inside ~15s. The bakery loaf matters.
- assembly MASS_BAND [1.1,2.1] → [2.5,4.5]: the old band scored a normal build
  (5t+4c+3b = 3.82 total mass) as an off-band 0; the band now sits around a
  proper generous topping. Sparse ~1.3 reads bare, a heap ~6.6 slides off.
- roasting RATE 0.042 kept; comment corrected to the measured window (power 6:
  stalls wet to ~26s, blisters 28-36s, collapses past ~40s).
- brie softensInSec 80 (from partial work) kept: out at order start, spreadable
  by assembly.

VERIFY (dev build; run in the console):
  t.start(); t.bruschetta()
Expected (headless sim + live path):
  roast:        {mean ~0.70, blister ~1.0, collapse 0, score ~1.0}
  distribution: ~1.0   (Clark-Evans R ~1.25, evenly strewn)
  balance:      {mass ~3.82 in band [2.5,4.5], score ~1.0}
  sog:          {value ~0.21, word "crisp", score ~0.99}
  rub:          {coverage ~0.35-0.5, score ~1.0}
  temp:         {score 1.0 (brie taken out, parmesan left in)}
  bench:        ~1.0   (clean roast, no slump)
  grade A/S, total ~9

Sub-mechanic checks (after t.bruschetta up to the oven, or standalone):
  t.roast(26,6)  → mean ~0.56, blister ~0     ("under-roasted")
  t.roast(42,6)  → collapse ~0.69             ("collapsing to sauce")
  skip t.tempToggle('brie') → Temperature 0, worst "brie fridge-hard"
  thin slice + t.sogWait(15) → sog ~1.0, "it bends"

typecheck clean (tsc --noEmit), vite build clean. node_modules is a symlink to
the main checkout and is deliberately NOT committed (dir-only .gitignore rule
misses the symlink; staged the 12 source files explicitly).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 15:25:21 +10:00

195 lines
6.5 KiB
TypeScript

/**
* The prep-bench cast. Everything a knife can be taken to, described by the
* numbers the cutting sim actually consumes — shape for the silhouette and the
* piece math, skin for the slip check, flesh for juice and layering, and the
* station-specific extras (seeds, sting, temperature needs, gratability).
*
* 1 world unit ≈ 11cm, same convention as the loaf.
*/
export type IngredientShape = 'sphere' | 'ovoid' | 'block' | 'wedge' | 'bulb' | 'mushroom';
export interface Ingredient {
id: string;
name: string;
shape: IngredientShape;
/** Characteristic diameter/length in world units. */
size: number;
/** resistance: how hard the skin fights the first bite. slippery: how keenly
* it skates when pressed without sawing — the tomato number. */
skin: { resistance: number; slippery: number };
/** moisture > 0.4 juices when cut; layered flesh falls apart when mistreated. */
flesh: { resistance: number; moisture: number; layered?: boolean };
seeds?: { count: number; pocket: 'core' | 'distributed' };
/** Onions. 0..1, scales how fast cutting stings the eyes. */
sting?: number;
temp?: { state: 'fridge' | 'bench'; softensInSec?: number; needsSoft?: boolean; needsCold?: boolean };
gratable?: { yield: number; pithDepth?: number };
roastable?: boolean;
/** Authored sRGB, same convention as bread crumb/crust. */
colors: { skin: [number, number, number]; flesh: [number, number, number] };
}
export type IngredientId =
| 'tomato'
| 'roast_tomato'
| 'orange'
| 'onion'
| 'garlic'
| 'parmesan'
| 'brie'
| 'cucumber'
| 'avocado'
| 'button_mushroom'
| 'portobello'
| 'king_oyster'
| 'bell_pepper';
export const INGREDIENTS: Record<IngredientId, Ingredient> = {
tomato: {
id: 'tomato',
name: 'tomato',
shape: 'sphere',
size: 0.6,
skin: { resistance: 0.35, slippery: 0.9 },
flesh: { resistance: 0.2, moisture: 0.75 },
seeds: { count: 20, pocket: 'distributed' },
roastable: true,
colors: { skin: [0.82, 0.18, 0.12], flesh: [0.9, 0.35, 0.22] },
},
roast_tomato: {
id: 'roast_tomato',
name: 'roast tomato',
shape: 'sphere',
size: 0.52,
// The skin blisters off in the oven; what's left is a water balloon.
skin: { resistance: 0.1, slippery: 0.55 },
flesh: { resistance: 0.08, moisture: 0.95 },
colors: { skin: [0.62, 0.16, 0.1], flesh: [0.75, 0.28, 0.16] },
},
orange: {
id: 'orange',
name: 'orange',
shape: 'ovoid',
size: 0.68,
skin: { resistance: 0.55, slippery: 0.45 },
flesh: { resistance: 0.3, moisture: 0.85 },
seeds: { count: 5, pocket: 'core' },
gratable: { yield: 1.0, pithDepth: 0.35 },
colors: { skin: [0.95, 0.55, 0.1], flesh: [0.98, 0.68, 0.2] },
},
onion: {
id: 'onion',
name: 'brown onion',
shape: 'sphere',
size: 0.62,
skin: { resistance: 0.4, slippery: 0.6 },
flesh: { resistance: 0.45, moisture: 0.55, layered: true },
sting: 1.0,
colors: { skin: [0.72, 0.5, 0.28], flesh: [0.93, 0.9, 0.82] },
},
garlic: {
id: 'garlic',
name: 'garlic',
shape: 'bulb',
size: 0.28,
skin: { resistance: 0.25, slippery: 0.35 },
flesh: { resistance: 0.5, moisture: 0.3, layered: true },
sting: 0.15,
colors: { skin: [0.9, 0.86, 0.8], flesh: [0.95, 0.92, 0.85] },
},
parmesan: {
id: 'parmesan',
name: 'parmesan',
shape: 'block',
size: 0.5,
skin: { resistance: 0.8, slippery: 0.15 },
flesh: { resistance: 0.85, moisture: 0.1 },
temp: { state: 'fridge', softensInSec: 90, needsCold: true },
gratable: { yield: 0.9 },
colors: { skin: [0.85, 0.75, 0.5], flesh: [0.93, 0.87, 0.68] },
},
brie: {
id: 'brie',
name: 'brie',
shape: 'wedge',
size: 0.45,
skin: { resistance: 0.3, slippery: 0.2 },
flesh: { resistance: 0.15, moisture: 0.35 },
// softensInSec tuned for M15: out on the bench at order start, a brie is
// spreadable by the time the roast + toast are done (~45s of service).
temp: { state: 'fridge', softensInSec: 80, needsSoft: true },
colors: { skin: [0.95, 0.93, 0.88], flesh: [0.97, 0.92, 0.78] },
},
cucumber: {
id: 'cucumber',
name: 'cucumber',
shape: 'ovoid',
size: 1.4,
skin: { resistance: 0.5, slippery: 0.5 },
flesh: { resistance: 0.25, moisture: 0.8 },
seeds: { count: 14, pocket: 'distributed' },
colors: { skin: [0.2, 0.45, 0.16], flesh: [0.85, 0.94, 0.75] },
},
avocado: {
id: 'avocado',
name: 'avocado',
shape: 'ovoid',
size: 0.62,
skin: { resistance: 0.7, slippery: 0.3 },
// Ripeness is a lottery: roll flesh resistance per-avocado at spawn.
flesh: { resistance: 0.5, moisture: 0.45 },
seeds: { count: 1, pocket: 'core' },
colors: { skin: [0.16, 0.2, 0.1], flesh: [0.78, 0.85, 0.5] },
},
// The mushrooms: dry-slick skin that skates under a pressed blade, soft
// flesh once the edge bites. Three varieties, three different fights.
button_mushroom: {
id: 'button_mushroom',
name: 'button mushroom',
shape: 'mushroom',
size: 0.3,
// Tiny and rolls away from the blade — the slip is the whole challenge.
skin: { resistance: 0.15, slippery: 0.75 },
flesh: { resistance: 0.15, moisture: 0.35 },
roastable: true,
colors: { skin: [0.92, 0.88, 0.8], flesh: [0.97, 0.95, 0.9] },
},
portobello: {
id: 'portobello',
name: 'portobello',
shape: 'mushroom',
size: 0.85,
skin: { resistance: 0.2, slippery: 0.55 },
// Gills crumble when mistreated, and it weeps dark juice.
flesh: { resistance: 0.2, moisture: 0.45, layered: true },
roastable: true,
colors: { skin: [0.45, 0.32, 0.22], flesh: [0.82, 0.76, 0.66] },
},
king_oyster: {
id: 'king_oyster',
name: 'king oyster',
shape: 'ovoid',
size: 1.0,
// Mostly one dense smooth stem — cuts like a slippery little log.
skin: { resistance: 0.2, slippery: 0.8 },
flesh: { resistance: 0.35, moisture: 0.3 },
roastable: true,
colors: { skin: [0.9, 0.87, 0.78], flesh: [0.96, 0.94, 0.88] },
},
bell_pepper: {
id: 'bell_pepper',
name: 'bell pepper',
shape: 'sphere',
size: 0.75,
// Waxy gloss skin skates almost like the tomato; inside it's mostly air.
// ponytail: hollowness isn't modelled — pieces come out as if solid.
// Add a hollow flag to the piece math if rings/strips ever matter.
skin: { resistance: 0.45, slippery: 0.65 },
flesh: { resistance: 0.2, moisture: 0.5 },
seeds: { count: 60, pocket: 'core' },
roastable: true,
colors: { skin: [0.8, 0.12, 0.1], flesh: [0.88, 0.3, 0.2] },
},
};