The loop closes: ticket -> toast -> spread -> ENTER -> verdict -> next day.
- judging.ts: 9 weighted criteria, each reading the same fields the knife was
pushing around, so every line of the scorecard points at something real
("0.78 against 0.68 asked", "thick — thin asked", "28% burnt").
- lines.ts: ~45 lines keyed to whichever criterion actually decided the score,
so the verdict and the scorecard always agree — the verdict just has feelings
about it. MITEY has its own vocabulary.
- judge.ts: the toast turns on a pedestal under a spotlight, grade stamps in,
heatmap toggle for browning/spread. The generated inspector reacts by grade.
- orders.ts: seven handwritten days, then procedural. Day 1 is soft butter on
white; day 7 is a translucent film of MITEY on wet sourdough with fridge-hard
butter and a steak knife.
The find: every art colour was authored as sRGB and fed straight into a linear
lighting pipeline. Linear 0.14 encodes back out to sRGB ~0.4, so "near-black"
MITEY rendered as TAN and saturated butter washed to pale cream. This was the
root cause of the legibility fights in M0 and M2 — I'd been treating the symptom
by pushing the specular around. One line (albedo = pow(albedo, 2.2), mixing
stays in sRGB because that's the space the palette was picked in) and the whole
art direction landed: MITEY is genuinely black, butter is butter, the crust went
from cream to a rich golden brown.
Also: the specular lobe was far too broad. The slice is flat with the light and
camera both above it, so dot(N,H) ~0.98 everywhere and a wide lobe blankets the
whole slice in white. Tightened, so only ridges tilted into the light catch —
which is what a spread actually looks like.
The slice rolls its own lighting and ignores scene lights, so the judge's
spotlight did nothing to it; it now swaps its own rig for presentation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Knife angle (wheel) is the only input, and spread/scrape/gouge all fall out of it.
Verified by driving real input through the real code paths:
cold toast + fridge butter -> damage 0.015, spread 0.029 (tears, as designed)
fresh warm toast -> damage 0, spread 0.117 (flows cleanly)
burnt toast, knife on edge -> char 99.9% -> 82.7% -> 54.5%
evenness 0.029 -> 0.123 -> 0.226
pale toast, knife on edge -> gouges; steak knife gouges 3.2x harder
That evenness column is the mechanic: scraping rescues you from char and wrecks
uniformity doing it. The judge will have opinions.
The trap is calibrated, not hoped for. Pressure comes from steepness, but past
SCRAPE_ANGLE the knife stops spreading:
fridge butter / cold toast yield 0.72 needs angle 0.75 IMPOSSIBLE
bench butter / cold toast yield 0.54 needs angle 0.57 (cliff at 0.62)
fridge butter / fresh toast yield 0.36 needs angle 0.38 fine
soft butter / fresh toast yield 0.16 needs angle 0.12 dream mode
So cold toast + hard butter cannot be spread at any angle, and the way out isn't
technique — it's not dawdling. The pressure gauge draws both marks so you can
see the gold sitting past the red and understand why you're losing.
Emergent and kept: a steak knife's narrow blade concentrates pressure enough to
beat cold butter's yield. It's the right tool for cold butter and a menace
everywhere else.
- cutlery.ts: 9 hand-authored archetypes (silhouettes are gameplay — the drawer
has to be fair) + compound box colliders for M4.
- dev.ts: harness that drives real gestures deterministically. Earns its keep.
Two bugs: cutlery meshes used mesh.rotation.x = -PI/2, which sends a profile
drawn toward +y to -z and one drawn toward -y to +z — the handle and blade were
laid out in opposite directions, overlapping, nowhere near the cursor. Now
rotated at the geometry level. And resize() computed aspect = 0/0 = NaN when the
container reports zero, which poisons the projection matrix so every raycast
silently misses — i.e. the entire mechanic stops with no error.
Metals need something to reflect: added a RoomEnvironment IBL and rebalanced the
direct rig, which was tuned before it existed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playable: pick a bread, set the dial, drop the lever, watch it brown, pop it
onto the plate. No timer readout — you go by smell.
- toasting.ts: heat map from real toaster behaviour (element stripes, cool top
edge where the slice stands proud of the slot, edge falloff, per-run noise
bias). Moisture must boil off before the crust browns at full rate, so
sourdough stalls then catches up; sugar browns and burns early.
Rate is measured against the heat map's actual ~0.77 mean, not guessed:
power 6 -> golden 0.511 at 10s, verified in-browser.
- Props on a real scale (1 unit ~ 11cm, one slice wide) — the toaster is
meant to dwarf the bread.
- The pop solves a genuine ballistic arc from slot to plate.
Three bugs found by driving it rather than trusting the compile:
- valueNoise2D read one past its grid at x=w-1, so heatBias was NaN along an
edge, which poisoned mean browning to NaN — i.e. every judge score would
have been NaN. Clamped.
- erode() clamped at the field border, so border texels could never erode out
of the mask and kept those NaNs in scope. Off-grid now counts as outside.
- Timer.connect(document) zeroes dt whenever document.hidden is true, which
froze the sim solid in an embedded browser. Dropped; rAF already pauses for
hidden tabs. App.step(dt) is now exposed so the game can be driven
deterministically for verification.
Also: screen shake was mutating the camera permanently instead of offsetting
it per-render, and the plate's lathe profile touched the axis (degenerate
triangles -> starburst normals) — rebuilt from primitives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bread slice is the whole game's canvas, so it's built first and properly:
- Field: a 128x128 scalar field with bilinear sample, a mass-conserving brush,
self-levelling relax, and the masked statistics the judge will read.
- Slice: extruded loaf silhouette (domed top), planar-projected UVs so the sim
grid lines up with the bread, per-texel silhouette mask, and four fields
(browning/dryness/spread/damage) packed into one RGBA8 texture per frame.
- Custom shader: browning ramp with blotchy char, crumb/crust/inclusions,
spread wetting + opacity + thickness relief, gloss, gouge exposure, and
flat data views for the judge screen.
- bread.ts / spreads.ts: the archetypes and the rheology the difficulty
curve will live in.
- scripts/gen-assets.sh: reproducible fixed-seed asset generation through
MODELBEAST (flux_local -> bg_remove_local -> hunyuan3d_mlx). All on-device.
Legibility work that mattered: butter rendered invisible at first. Fixes were
a gamma-style wetting darkening (a multiply *brightens* pale crumb), a normal
perturbation taken in UV units rather than raw texel deltas (~30x too small),
and pulling the lighting back under 1.0 so ACES stops desaturating yellow to
cream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>