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>
195 lines
6.5 KiB
TypeScript
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] },
|
|
},
|
|
};
|