Commit Graph

6 Commits

Author SHA1 Message Date
monster
5b09f23c7e M5+M6: content, generated assets, title, sound, perf
Content (verified — same dial, same 18s, four breads that disagree):

  bread        4s     9s     18s    char
  white        0.22   0.54   1.08   93%
  multigrain   0.20   0.52   1.06   92%
  raisin       0.30   0.76   1.14   99.8%
  sourdough    0.11   0.33   0.74   14%

Sourdough stalls at half the rate then accelerates hardest (x3.0 vs white's
x2.45) as its water boils off; raisin browns 40% faster and is essentially
carbon by 18s. Seven handwritten days then procedural, day persisted.

Assets, all generated on-device and free, all reproducible from fixed seeds:
- the judge's five expressions (one seed, one description, only the expression
  phrase varying — he stays the same man while his face falls)
- title art, wooden bench, blurred kitchen backdrop
- butter dish / peanut butter / MITEY jars as GLBs, normalised on load
  (recentre, scale to fit, sit on y=0) rather than hand-fixed, so regenerating
  them doesn't mean re-fixing them

The toaster stays procedural: the generated one is prettier but this one has a
lever we can drive and a dial that turns. Judgment call, per the brief.

Sound is synthesised, no samples: clatter is driven by real Rapier contact-force
events and scaled by force, which is the only way a drawer of cutlery can sound
like one. Plus lever clunk, knife scrape pitched by pressure, and the stamp.

Perf, measured (16.7ms is the 60fps budget):
  kitchen 0.99ms/step · drawer 0.12ms · drawer while dragging 0.21ms

Production build: 2.87MB / 1.0MB gzipped, mostly Rapier's inlined wasm + three.

Textures carry colorSpace = SRGBColorSpace explicitly — same class of bug as the
albedo gamma fix in M3, and it washes them out silently if you forget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:21:06 +10:00
monster
1ba66f0cdf M4: the cutlery drawer — and the link that makes it matter
toast -> drawer -> spread. Eight pieces settle into a real tangle; find the one
on the card, drag it up and over the rim, and whatever you're holding is what
you're spreading with.

The link, verified end to end (day 4, butter softness 0.3):

  4s in the drawer   warmth 0.78  yield 0.343  needs angle 0.36  comfortable
  50s in the drawer  warmth 0.17  yield 0.565  needs angle 0.60  cliff is 0.62

Dawdling walks you to within two hundredths of the scrape cliff. On day 7
(fridge-hard butter) it tips over into genuinely unspreadable. The drawer isn't
a timer bolted on the side — it's the thing that makes the butter hard.

Physics choices that earn their keep:
- Compound boxes, never trimeshes. A trimesh fork tangled in a trimesh fork is
  a solver nightmare and slow with it; the tine envelope is what matters anyway.
- Grab is a capped PD spring, not a fixed joint, so a snagged piece fights the
  ones lying on it and loses instead of tunnelling through them.
- The solver is fixed-stepped so a frame hitch can't detonate the pile, and the
  drawer settles offline so you open it on an existing mess.
- Clatter is synthesised from real contact-force events, velocity-scaled — a
  drawer of cutlery is a noise, and the noise has to come from the collisions.

Three fixes:
- Quaternion.toArray() returns [x,y,z,w]; Rapier wants {x,y,z,w}. Every axis
  read back undefined -> NaN in the solver -> wasm panic on the first step.
- The drag plane faced the camera, but the camera looks down into the drawer, so
  dragging to the top of the screen couldn't physically lift anything over the
  rim. It's a vertical plane now: up-screen means up.
- Cooling lived in kitchen.update(), which stops running the moment the drawer
  opens — so the toast stayed hot no matter how long you rummaged, quietly
  cancelling the entire mechanic. It's an app-level tick now.

The drawer was 4.6x3.4 and the pieces politely spread out flat. Tangling needs
crowding: 3.1x2.2, dropped down a narrow column so they land on each other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:13:08 +10:00
monster
b07d57e9e9 M3: judging — the scorecard, the judge, and a colour-space bug
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>
2026-07-16 22:04:22 +10:00
monster
eb74170011 M2: spreading — one control, three behaviours
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>
2026-07-16 21:51:34 +10:00
monster
67cfe56bb9 M1: toasting — browning sim, toaster, lever, and the pop
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>
2026-07-16 21:29:51 +10:00
monster
10adb268a0 M0: scaffold — Vite+TS+Three+Rapier, slice sim core, asset pipeline
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>
2026-07-16 21:16:18 +10:00