toastsim/OPUS-BUILD-BRIEF-2.md
type-two 003aede82f Bell pepper in the cast; the pantry backlog spec'd in the brief (§7.5)
- bell_pepper: waxy slick skin (2 slips pressed vs tomato's 2 at higher
  displacement), 60-seed core — cutting it scatters seeds, bench reads
  'a crime scene' with zero extra code. Hollowness ponytail-deferred.
- Brief §7.5: capers+wasabi as the shared dosing-danger class, the
  mustard family on the jar/drawer system, rib eye as its own milestone
  (cook/rest/cut-across-grain/mustard pairing), baked stuffed potatoes
  (lid cut, scoop-as-inverse-spread, restuff). Each rides existing bones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:22:29 +10:00

24 KiB
Raw Blame History

TOASTSIM — Build Brief 2: The Prep Bench

For: Claude Opus 4.8, executing in Claude Code on this Mac (M3 Ultra, cwd /Users/m3ultra/Documents/toastsim). Approved by the owner. Asset generation via MODELBEAST is free and pre-authorized. Commit + push per milestone to git@gitea.partly.party:monster/toastsim.git (SSH remote already configured; HTTPS has no credentials).


0. Where you are standing

M0M9 are shipped and verified: toasting (browning field), spreading (knife-angle rheology, butter/PB/MITEY/marmalade), the judged scorecard with a claymation judge, the tangled cutlery drawer, day loop with orders, oil-separation stirring, rind distribution scored by ClarkEvans, and a fresh-eyes pass (M9) that locked the cheat panel and added live readouts. Read README.md and the git log first — the commit messages are design documents.

M10 (the bakery) is code-complete but NOT fully verified — that is your first job (§2). It adds: bread brands with mechanical quality (bread.ts BRANDS), unsliced bakery loaves, a slicing minigame (sim/slicing.ts + scenes/slicer.ts), the bread knife and the legendary The Best Thing in the drawer, day 9, and cut quality feeding the toaster (wedge slices brown unevenly).

How to run and verify (non-negotiable discipline)

  • npx vite --port 5173 --strictPort in background; open via the Browser pane.
  • The embedded browser never fires rAF (document.hidden is true) and can report innerWidth 0. The game survives both, and the dev harness exists because of them: window.__t (dev builds) drives the real input path deterministically — t.run(frames), t.tap('Space'), t.point(x,y,z,down), t.toast(secs,power), t.dip(), t.spread(), t.swirl(), t.stats(), t.look(). Call t.app.resize() once after load. Extend the harness for every new minigame you build — if you can't drive it headless, you can't verify it.
  • Verify by playing and reading numbers the judge computes, then screenshot. Never trust "it compiles". Every milestone: typecheck, build, played-through evidence, console clean, commit with a message that records what you measured, push.

Known traps (all cost real debugging time once — don't pay twice)

  • THREE.Timer.connect(document) freezes dt when hidden — don't reintroduce it.
  • camera.aspect = 0/0 poisons projection forever; App.resize() guards it — keep the guard.
  • mesh.rotation.x = -PI/2 on extrusions lays profiles out in opposite directions — use extrudeFlat() (geometry-level) in cutlery.ts.
  • Rapier wants {x,y,z,w} quaternions, not arrays; array = NaN = wasm panic.
  • Lathe profiles touching the axis = starburst normals. Build from primitives.
  • Colours: palette is authored sRGB. new THREE.Color(...floats) reads as linear → pale. Use .setRGB(..., THREE.SRGBColorSpace).
  • ACES desaturates >1.0 — keep lighting products under 1.0 or yellow becomes cream.
  • Shared materials can't do per-piece emissive — clone if you need highlights.

1. The design law (what made M0M9 good — keep obeying it)

  1. Analog inputs, judged outputs. Every mechanic is a continuous gesture the player can be better at, and every score is a statistic computed from the same state the gesture mutated. The scorecard always points at something real.
  2. Mistakes travel. A bad cut toasts unevenly. Dawdling cools toast, which hardens butter. Never punish twice at the same station — punish downstream, where it's funny.
  3. Feedback before the verdict. Live readouts use the judge's own functions (see kitchen's amount readout). The verdict may be brutal; it may never be a surprise.
  4. Statistics with names. ClarkEvans for scatter, mass-weighted mean±stdev for consistency, CV for evenness. The judge quotes the number; the number is honest.
  5. Silhouettes are gameplay → procedural geometry for anything the player must identify or that carries physics. Generated GLBs for hero props and vessels.
  6. The judge is the reward. Every new criterion ships with ~5 bad lines + 3 good lines in his voice: dry, specific, never cruel except about MITEY.

0.5 STATUS — updated 2026-07-17, read this before §2

M10 is DONE, verified and pushed (commit e5be266). §2 below is history; skip it. M11 is code-complete and verified, committed as WIP — see §3, and the notes here.

What M11 landed (all typechecked, all played headless via the harness):

  • sim/ingredients.ts — the nine-ingredient cast with the full profile from §3.1.
  • sim/cutting.tsCutSession, patterns halve/slices/wedges/dice, slip physics, juice ejection, pieceCV. Note: CV for slices/halve runs over piece WIDTHS, not volumes — on a round ingredient the end slices are thinner by geometry and a perfect cut still scored CV 0.40, which is not a skill test. Wedges/dice keep volumes.
  • sim/mess.ts — the judged mess field + solids list + mass-conserving wipe.
  • scenes/prep.ts — the bench: aim/bite/saw, seams, pieces fan out at the end, juice stain texture, instanced solids, live bench: clean/smeared/a crime scene. Session end does NOT auto-exit — ENTER hands off, so you can wipe first.
  • judging.tsPrepResult, cutEvennessCriterion (The Pieces, w 1.3), benchCriterion (The Bench, w 0.9), clarkEvans extracted for M15's toppings, criteria tagged with group and the scorecard grouped by station.
  • lines.ts — 8 lines each for cutcv and bench.
  • Harness: t.prep(ing, pattern, knife), t.chop({pos, dy, wobble, press}), t.wipe(passes), t.prepStats(), plus t.start(), t.day(n), t.pick(id), t.slice().

Measured on the bench (repeat these before changing tuning):

  • Even deliberate slices (dinner knife, 0.045 u/frame) → CV ~0, bench mass 0.51.
  • Sloppy positions + frantic speed (0.3 u/frame) → CV 0.72, mass 18.8, 16 solids, "a crime scene".
  • Press a dinner knife on a tomato without sawing → it skates: 1 slip, aim jumped 0 → 0.12.
  • Same press with The Best Thing → bites in 7 frames, 0 slips (sharp edges sink on downforce).
  • Wipe two passes → mass 0.51 → 0.09.

M11 is NOT finished — remaining before it can be called done:

  1. No order actually routes through the bench yet. Game.startPrep() exists and the harness drives it, but Order.prep?: PrepStep[] is declared and unused — no day sends you there. Wire a day-10 prep order and play it end to end.
  2. dice two-phase and wedges are implemented in the sim but never driven — no rotate-the-board input, no verification. M14 needs the dice; do it there or here.
  3. The pieces fan-out is placeholder-grade (cylinders). Pieces aren't physics bodies yet.
  4. The wipe is the same drag as cutting, disambiguated only by distance from the ingredient — it needs a real cloth tool/mode before it reads to a stranger.
  5. No screenshots taken of the bench yet. Do that when the visuals are past placeholder.

BLOCKER — the asset farm is down for 3D. Every bg_remove_local job fails: m4pro's disk is 100% full (118MB free; ~3.4TB is ~/Music — hands off, that's John's). Flux 2D still works — bruschetta_hero.png and wonderslice_bag.png generated fine and are committed. The whole 3D cast (bakery loaves, cutting board, prep-bench props) is stuck at the cutout stage. mb settings is owner-only (403), so re-routing the bg_remove lane needs John. Once there's space: bash scripts/gen-assets.sh bakery then bash scripts/gen-assets.sh prep — retry-once is now built into gen_3d, seeds are bumped, and the prep cast + 2D are already queued in the script. Everything degrades gracefully: loadProp catches, so the procedural stand-ins just stay.


2. M10 finish line — verify the bakery (DONE — kept for the record)

Everything compiles and day 9 reaches the drawer with the right ticket (verified: chips show the bakery loaf / doorstop — slice it yourself, timer reads "they are waiting", and the drawer contains bread_knife AND the_best_thing). Remaining:

  1. Play the slicer end-to-end via harness: pick the bread knife from the drawer (drag it above the rim, hold 0.45s), aim ~0.31 world units (doorstop = 2636mm at 110mm/unit), saw with vertical strokes (~0.05 units/frame stays under the squash threshold), confirm onSliced fires and the kitchen receives the cut.
  2. Assert the couplings: a deliberate wobble cut (add dx while sawing) must show browning-evenness degradation after toasting (stats().browning.stdev visibly higher) and "a bit wedged" in The Cut row. A slam cut (fast dy) must arrive pre-damaged (Integrity).
  3. Wrong-tool slicing: take the soup spoon to the loaf — confirm it's miserable but possible (saw eff 0.3) and the judge's Cut row + Utensil tell the story.
  4. Tune feel: STROKE_GAIN, WOBBLE_GAIN, SPEED_CAP in slicing.ts are first guesses. A clean bread-knife cut should take ~46 deliberate strokes; The Best Thing ~3 and nearly wobble-proof. Cheap-brand WONDERSLICE toast (day 1) should visibly brown patchier than Crustworthy (day 8) — screenshot both.
  5. The slicer scene has placeholder-quality visuals (flat crust box loaf, no cut seam). Minimum bar: a visible cut line that deepens with progress, the cut slice separating and falling flat at the end. Nice-to-have: crumb texture on the cut face (reuse bread.crumb colour).
  6. verify → commit → push as M10.

3. M11 — The prep bench core (build once, everything else rides it)

A second bench (like the slicer scene) where ingredients get cut, and the machinery every later milestone uses.

3.1 Ingredients

src/sim/ingredients.ts:

interface Ingredient {
  id: string; name: string;
  shape: 'sphere' | 'ovoid' | 'block' | 'wedge' | 'bulb';
  size: number;                    // world units, 1 ≈ 11cm
  skin:  { resistance: number; slippery: number };  // tomato: low res, HIGH slip
  flesh: { resistance: number; moisture: number; layered?: boolean }; // onion: layered
  seeds?: { count: number; pocket: 'core' | 'distributed' };
  sting?: number;                  // onions
  temp?:  { state: 'fridge' | 'bench'; softensInSec?: number; needsSoft?: boolean; needsCold?: boolean };
  gratable?: { yield: number; pithDepth?: number };  // citrus zest / hard cheese
  roastable?: boolean;
}

Starting cast: tomato (slippery, wet), roast_tomato (wetter, softer), orange (ovoid, seeds, zestable), onion (layered, sting 1.0), garlic (bulb, small), parmesan (block, needsCold, gratable), brie (wedge, needsSoft), cucumber, avocado (seed = one giant pit, flesh softness varies by ripeness roll — a free lottery joke).

3.2 Generalized cutting (src/sim/cutting.ts)

Extract and extend the M10 slicing sim. A CutSession = ingredient + knife + a pattern:

  • halve — one cut; scored on evenness of the two halves (offset from centre + wedge).
  • slices(n) — repeated cuts; pieces recorded as widths → evenness = coefficient of variation of piece volumes. CV < 0.08 "even", > 0.25 "a lottery".
  • dice — two-phase (see onions, §6).
  • wedges(n) — radial; for oranges/tomato wedges.

New physics on top of M10's progress/wobble/squash:

  • Slip: on the first bite, if skin.slippery * downforce > grip, the ingredient skates — the cut position jumps, juice sprays (→ mess field), and you re-aim. Grip comes from saw motion (a moving serrated blade bites; pressing a chef's knife straight down on a tomato is the canonical failure). The Best Thing and sharp knives bite at low downforce.
  • Juice: every cut through flesh.moisture > 0.4 ejects juice proportional to moisture × speed, sprayed along the stroke direction (→ mess field §3.3). Slow deliberate cuts leak less. Roasted tomatoes are basically water balloons — the joke is intended.
  • Pieces are physics bodies on the board when it matters (dice results tumble; reuse drawer's compound-collider discipline — primitives, never trimesh).

3.3 The mess field (src/sim/mess.ts) — the bench is judged now

A Field over the cutting-board UV (reuse core/field.ts wholesale) + a particle list for solids (seeds, skins, zest, crumbs — reuse the rind instancing pattern from slice.ts).

  • Writers: juice spray, seed ejections, onion skins, grated overflow, squashed pulp.
  • Wipe tool: hold-and-drag a cloth (same brush machinery as spreading, in reverse). Wiping costs service time. Wiping juice spreads it thinner first (mass-conserving brush: same trick as butter) before absorbing — one pass smears, two passes clean. That's the feel.
  • Judge criterion The Bench (weight ~0.9) on any prep order: mess mass + spread fraction, worded by the worst contributor: "There is juice on everything. Everything." / "Somebody's seeds are on my floor." Good: "A clean board. You'd be surprised how rare."
  • Live readout in the prep HUD: bench: clean / smeared / a crime scene — judge's own thresholds.

3.4 Scoring plumbing

Orders grow a prep?: PrepStep[] list; the judge gains a per-step criterion builder (cut evenness, yield, pith, sting-time, bench). Group the scorecard visually by station (Prep / Toast / Spread) — the card is getting long, and grouping keeps it legible.

4. M12 — Oranges and the ribbed pyramid

Flow: (zest first, if ordered §5) → halve → juice → serve the glass.

  • Halving uses halve pattern. Cut-quality → theoretical yield: yield = base × (1 2|offset|) × (1 0.5·wedge). A perfect halve leaves 100% reachable; a 60/40 butchering caps you at ~70% and the judge will say why: "You have cut a lid and a bucket."
  • The juicer (hero prop: old-school ribbed glass pyramid — generate it). Minigame reuses the jar-stir swirl detection (kitchen.ts) verbatim: place the half (drag onto cone), then press + twist. Twist sweep extracts (extracted += sweep × pressGrip); pressing hard without twisting tears pulp (mess + pulp-in-juice) and sprays. Yield bar fills toward the theoretical max — the live readout says 62% of what this orange had. Rotate rhythm matters: reversals of twist direction give a small bonus bite (real reamers work this way; it also just feels right).
  • Seeds. Each orange carries seeds.count (37). The Pyramid Classic (starter) lets seeds through into the glass: visible, clickable — fish each out at a time cost, or serve pips and eat the criterion: "There are three pips in this. I counted. I shouldn't have been able to."
  • Juicer tiers (progression, introduced by day like brands): Pyramid Classic (day 12) → Ribbed Deluxe (seed moat catches ~80%) → The Juicernaut (legendary, day ~20 procedural rare: catches everything, +10% yield, and the judge notices: "Is that a Juicernaut. Sit down.").
  • Judge criteria: Yield (vs theoretical, weight 1.3), Pips (1.1), The Bench, plus the halve's Cut row. Serve = the glass on the pedestal instead of toast — JudgeView.show needs a subject abstraction (toast mesh | glass mesh). Keep it small: a presented Object3D + criteria list.

5. M13 — The grater

One prop (box grater generated; microplane optional later), two customers:

  • Zest: before juicing, some orders want zest of one orange. Hold the orange against the grater, rhythmic vertical drags = zest particles (distance-based shedding — the marmalade-rind pattern, reused exactly). The pith line: each surface region has zestDepth; over-grating a region past gratable.pithDepth sheds white pith particles into the pile — visible (white among orange), and the judge's Pith criterion counts the ratio: "This zest is half pith. It will taste like a headache." Scroll wheel rotates the orange to expose fresh skin — the skill is rotation discipline, same muscle as rind-scatter.
  • Cheese: parmesan (needsCold) grates clean IF cold — it softens on the bench in real time, and soft parmesan (or brie, ever) smears: output drops, the grater clogs (a clog meter; clearing it costs time), mess accrues. The inversion of butter: butter wants warm, cheese wants cold, and both run on the same temperature clock (§7).
  • Knuckles: grating speed over a threshold rolls a knuckle graze — small Integrity-style penalty on the player (brief red flash, mess spot, and a line: "That's your knuckle in there. I'm not scoring the protein.").
  • Judge criteria: Zest/Gratings amount (band, like spread amount), Pith, Clog/mess.

6. M14 — Onions (the technique test)

  • The sting. While an onion is cut open, sting pressure rises (rate × sting × cut aggression — crushing cuts sting more, so blunt knives and squash-heavy strokes are literally tear gas). Rendered as a growing edge-blur + welling vignette (CSS overlay, cheap). Vision cost is REAL: it blurs the cut line and the stop-line marker. Options with costs: wipe eyes (button: clears it, 4s, and if you've been handling onion it doubles it first — once, for the joke and the lesson), push through, or work fast and clean. Sharp knife + low squash = slow sting. The Best Thing is, again, the best thing.
  • The proper dice — exactly the technique the owner described:
    1. Halve stem-to-root.
    2. Lay flat. Radial/parallel cuts toward the stem that must STOP short of it — a visible stop-line at ~85%; a cut that goes through the stem releases the layers early (pieces fall apart into slivers — layered flesh means the CV of your final dice explodes). Undercutting leaves connected chunks (also CV chaos, other direction).
    3. Rotate 90° (scroll wheel), criss-cross cuts → dice falls in cubes.
  • Scored on Dice CV (the piece-evenness statistic from §3.2 — this is where it shines), stem discipline (through-cuts counted), sting time endured, bench state (onion skins are mess solids).
  • Judge lines: "You went through the stem. The onion became confetti. You served confetti." / good: "Square. Actually square. I'm keeping one."

7. M15 — Bruschetta (the capstone that uses everything)

The multi-component order. The ticket becomes a small recipe card; service time weight rises; planning is the mechanic:

  • Temperature clock (generalizes butter softness): ingredients have bench-vs-fridge states drifting on real time. Brie must come out EARLY (soft enough to spread by assembly time); parmesan must stay in UNTIL needed. The ticket hints ("get the brie out now, love") and a small fridge/bench toggle UI per perishable — decided at order start and revisitable at a time cost.
  • Roast tomatoes: the oven is the toaster generalized — reuse the browning Field on tomato halves (they blister rather than brown: same field, different colour ramp + a blister speckle past 0.6). Overdone = collapse into sauce (moisture → mess when handled).
  • Assemble: your OWN sliced sourdough (M10), toasted (M1), rubbed with garlic (one swipe mechanic — a light 'spread' with a garlic clove, coverage judged lightly), topped: tomato pieces + cheese + basil. Topping distribution = ClarkEvans again (the rind code takes a point list — feed it topping placements). Balance: total topping mass per slice area band, "thicker but even and balanced = more score" — mass band × distribution, the user's exact ask.
  • The sog clock: wet toppings on toast start sog rising (rate × topping moisture ÷ slice thickness — thick doorstop slices resist sog: ANOTHER reason the bakery loaf matters). Serve before soggy; the judge presses a finger on it, on camera: "It bends. Toast should not bend."
  • Judge screen groups: The Bread (cut+toast) / The Topping (distribution, balance, sog) / The Bench. This is the order type where an S feels like a diploma.

7.5 The pantry backlog (John's asks, 2026-07-17 — spec'd, not scheduled)

Each of these rides existing bones. None block M12M15; pull them in when their host system lands. Already in the cast as of today: three mushrooms (button/portobello/king oyster — the slip system's showcase) and bell pepper (waxy skin, seed core; hollowness deferred).

  • Capers + wasabi — the dosing danger class. Tiny and SUPER salty / hot; the mechanic is restraint. One "pinch/dab" gesture on the spread system with a brutal dose curve: under = judge doesn't notice, right = flavour bonus, a hair over = order ruined ("There is a WAR CRIME on this crostini"). Same curve, two condiments — build the dose scalar once. Rides: spread mass tracking + judging bands.
  • The mustard family. Heaps of types (hot english, dijon, wholegrain, american...) = the jar/drawer system generalized, like the PB crunchy/smooth split but wider. Each order names or implies a type; wrong-mustard is a pairing miss, not a technique miss. Rides: drawer + jar + ticket hints.
  • Rib eye — the crown jewel, its own milestone. Fresh cooked, and it BLEEDS: (1) cook right — the browning Field on a steak, two sides, doneness = interior ramp not surface; (2) REST it — cut too early and juice mass floods the board (moisture starts ~0.95 and decays toward 0.6 over a real-time rest clock; the mess field already knows what to do with the flood); (3) cut right — across the grain; give the ingredient a grain axis and score cut-angle against it, wobble tears rather than slices; (4) serve with the RIGHT mustard (pairing above). Judge groups: The Cook / The Rest / The Cut / The Pairing.
  • Baked stuffed potatoes. The oven (M15's toaster-generalized) plus one new gesture: cut the lid (cutting sim, one careful shallow cut — depth matters for once), scoop (inverse of spreading — remove mass evenly without breaching the skin, coverage judged), mix scoopings with cheese/toppings, restuff, rebake. Skin breach = structural fail the judge pokes. Rides: oven Field, spread-coverage math run backwards, mess.

8. Assets — MODELBEAST (all free, all local; fire jobs FIRST, code while they run)

scripts/gen-assets.sh has the pipeline (gen_3d name seed "prompt"; flux → bg_remove → hunyuan3d, ~6 min each, GPU lane serial). The M10 bakery batch partially failedsourdough_loaf meshed but produced no GLB, and batard_loaf, cutting_board, tomato_vine, roast_tomatoes failed at flux. First: check ./mb jobs (source ~/Documents/MODELBEAST/data/agent.env) for the error text, re-run with bumped seeds (+1) and slightly reworded prompts; add a retry-once into gen_3d. Never spam retries on queued — the lane is serial and shared.

Then queue the prep-bench cast (fixed seeds, style token as in the script): oranges ×2 (whole / halved showing segments), ribbed pyramid juicer (hero — prompt the ribs explicitly), juice glass, box grater, brown onion, garlic bulb, basil sprig, olive-oil bottle, oven tray, Ribbed Deluxe + Juicernaut juicer variants, plus 2D: bruschetta title-hero, "WONDERSLICE" bag art for the ticket. Anything the player must identify by shape or that carries physics (onion halves, dice pieces, orange halves on the reamer) stays procedural — silhouettes are gameplay, and meshers hate thin/segmented geometry.

9. Milestone order, and the bar for each

M10-verify → M11 → M12 → M13 → M14 → M15, one commit+push each, message = what you measured. For each: harness helpers first, then the sim (pure, in src/sim/), then the scene, then judge criteria + 8 lines, then live readouts, then played-through verification with numbers in the commit message, then screenshots. If a milestone's feel is wrong, fixing feel beats advancing — the loaf, the reamer twist and the onion stop-line are the three feels that must be RIGHT.

Pre-authorized judgment calls: all tuning constants; cutting scope inside a milestone to protect feel; procedural-vs-generated per asset; deferring the microplane, avocado, cucumber if the core five (orange, onion, tomato, parmesan, brie) land better without them. Do NOT add paid/cloud dependencies, accounts, or telemetry. Do not touch ~/Documents/MODELBEAST except through mb.

The bet is the same as last time: the bones are good, so build each new station on the bones — Fields, point-statistics, distance-shedding, swirl detection, dwell commits, the harness — and the game stays one game instead of six minigames in a trench coat.