sandoniette/OPUS-BUILD-BRIEF.md
type-two 820b559556 Build-out 6: the audience + global leaderboards
- The crowd: 11 seeded silhouette heads along the stage lip — idle sway,
  trembling past anger 0.5, a hard 110-frame duck at the string cut, happy
  bobbing on an S. Deterministic math in update(), harness-safe.
- Global boards: game/scores.ts client (silent-fail, 4s timeout) against the
  arcade server's new /api/sando/scores (via the new partly.party
  /arcade-api/ nginx proxy). Week score = sum of verdict totals across served
  days; KANSEI submits under a remembered chef name and shows rank + podium;
  the title shows top-5.
- E2E proven through https://partly.party/arcade-api/sando/scores (rank 1
  round-trip). Battery unchanged; crowd flinch + score accumulation verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 03:54:47 +10:00

425 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# サンドニエット SANDONIETTE — Build Brief 1: The Puppet, The Food, The Samurai
## STATUS (update this as you go — a cold session reads here first)
- **LIVE 🌐** https://partly.party/sandoniette/ — `bash deploy.sh` (build base
`/sandoniette/` → rsync dist/ → docker cp into `forum-nginx:/usr/share/nginx/html/sandoniette`).
Games VPS `humanjing@100.71.119.27`, Cloudflare-fronted (purge cache after big
changes). Verified live: index+JS 200, JS is application/javascript, PNGs image/png,
title renders. `vite.config.ts` bases dev at `/`, build at `/sandoniette/`; asset
loads use `import.meta.env.BASE_URL`. Local preview needs `vite preview --base=/sandoniette/`.
- **M0 ✅** vite+ts+phaser(3.90)+Matter skeleton, harness (`window.__s`), typecheck green.
Box drops → settles 68 frames, rests flush on floor.
- **M1 ✅** `sim/marionette.ts` — control bar + 6 strings (head, **back**, 2 hands,
2 knees) over a 9-segment silhouette. Bar snaps to mouse; lag comes from soft
worldConstraints (stiffness<1). Feet planted on floor + head/back strings = stable
upright stand, no topple. Grip is **sticky** (latches on contact via `puppet.update()`
each step). Exit-bar measured: **dangle** settles 87 frames upright (lean 4.6°);
**walk** 8.84 units upright (>6 req, lean 30° < 45 topple); **grip 10/10**. Verified
in-browser + walk screenshot.
- Stack: Phaser Matter, `autoUpdate:false` sim steps only via `stage.stepSim(dt)`
(which also calls `puppet.update()`). Render contract: Matter body = truth, flat
silhouette `Prop.view` follows. `Rng` = mulberry32. Tuning knobs: `K` at top of
marionette.ts. Harness `reset(homeX)` teleports to a known pose for back-to-back tests.
- Assets: all 20 2D props rendered on MODELBEAST `public/assets/img/` (samurai×5,
bg_washi, floor_boards, title, tickets, slash, 5 food faces, splat). Not wired in
yet procedural silhouettes carry gameplay; art is backdrop/faces/juice (M4/M6).
- **M2 ✅** cutting. `sim/geom.ts` (pure convex-split + pieceCV/cvWord), `sim/food.ts`
(convex polygon foods: bread/tomato/cheese/katsu/egg, material params
resistance/moisture/wobble), `sim/cutting.ts` (blade pass-through split food
into 2 real bodies along the chord, or squashsplat on a slow wet press).
Honest geometry, no canned slice anim. Traps hit + fixed: (a) cut line through a
vertex made lopsided halves split now treats on-line verts as shared (regression
guarded in `geom.check.ts`); (b) **Matter `setVelocity` is px/STEP not px/s**
piece launch was 60× too fast and flung halves offstage. Exit-bar measured:
**chop** 2 clean pieces CV 0 (<0.1 req); **push** 0 pieces + splat 3.28 (wet crime);
**wild** swing flings a piece offstage. Verified in-browser (two clean halves rest
on the board) + `npm run check` (headless geom asserts).
- **M3 ✅** stacking. `sim/stack.ts` the sando: ordered layers on a plate zone
(x=980), honest Matter stacking scored by lean°, COM drift, overhang. `standing`
is measured by **elevation** (a toppled tower loses height) not lean angle a
short 3-layer stack reads 30° from a mere 24px step, so lean alone is a bad
topple test. Trap fixed while here: **grip pickup was non-deterministic** (trials
shared puppet state) AND unreliable (yanking the bar sideways just stretches the
arms they hang from the shoulders, so the BODY must drift over the crate first;
`shiftBodyTo` waits for the hips to arrive, then reach). Now a true 10/10.
Exit-bar: centred 3-layer sando stands + survives quake (lean 02.4°); a leaning
stack stands marginally (lean 44°) then the nudge topples it the quake
discriminates good from bad. Verified in-browser (full M1+M2+M3 battery green).
- **M4 ✅** samurai + full loop. `sim/samurai.ts` (anger meter 0..1, events
drop/splat/crimeCut/servedWrong/servedLate/offstage/cleanServe, 5 unsheath
stages the generated portraits, vignette), `game/judging.ts` (4 groups
**THE CUT / SANDO / SERVICE / FLOOR**, banded scores, grade SD), `game/lines.ts`
(bilingual samurai barks, 8+/group, deterministic pick). **THE STRING CUT**:
`Marionette.cutStrings()` deletes all 6 strings in one frame ragdoll (pins
survive); stage flings loose food with the slash impulse + white slash FX +
synth gong. Backdrop = `bg_washi`, samurai portrait HUD + anger bar swap with
the meter. Assets now wired in the stage reads as the shadow-puppet theatre.
Exit-bar: clean run **grade S(10)** standing; bad run **grade D**; disaster run
**strings cut → ragdoll head falls ~180px, samurai fury**. Judge discriminates
SD. Verified in-browser (scorecard + string-cut screenshots) + full battery.
Trap fixed: field named `game` collided with `Phaser.Scene.game` renamed
`session`; walk faceplant test was per-frame lean>45 (a stride swings past 45
and recovers) → now "did the head hit the floor / end collapsed".
- Note: a string cut is game-over; real restart should rebuild the scene. Harness
`reset()` restrings in place — Matter's constraint solve-order makes the
restrung puppet marginally different but still walks fine (head stays ~400,
floor 640).
- **M5 ✅** days + escalation. `DAYS` in `game/game.ts` — 5 days, each ONE new
pressure: (1) basics, (2) +1 ingredient (4 layers), (3) wetter (2× tomato),
(4) a real cut (katsu), (5) patience ramp (maxSeconds 45→22, angerRamp 1.8).
`Game.setDay(n)` loads order + ramp; sins scale by `samurai.ramp` (sheathing
doesn't). Exit-bar: `__s.playAllDays()` — every day beatable by a scripted-
honest run (all S, standing). Ramp bites: day 1 needs 7 crime-cuts to strike,
day 5 only 4. Verified in-browser.
- **M6 ✅** juice. Title scene (`scenes/title.ts`) — sumi brush `title_art` +
「サンドニエット」+ pulsing **SHUBATTO!! SANDONIETTE!!!** + click-to-begin;
launched as an overlay over the live stage so the harness is up from frame one
(`__s.startGame()` dismisses it). `game/audio.ts` — 100% synth Web Audio SFX
(gong / slash whoosh / wet splat / shamisen), lazy AudioContext, no-op headless,
unblocked on first click. Wired: slash+gong on the string cut, splat on wet
squash, shamisen on title start. **Deferred (pre-authorized judgment):** food
faces — the generated faces are full-colour cartoons on paper, they clash with
the coherent black-silhouette look; would need regen as true cutouts. String
unlockables (rubber/chain/silk) — pre-authorized defer, M5 lands fine without.
Attract ragdoll → the pulsing title. Verified: title screenshot, click path,
production `npm run build` clean.
## BUILD-OUT PASS 6 ✅ — the audience + the global board
- **The crowd**: 11 seeded silhouette heads (shoulders/head/occasional topknot)
along the stage lip. Idle sway; trembling once anger>0.5; a hard duck for 110
frames at the string cut (`Stage.flinch`); happy bobbing on an S
(`cheerCrowd()`). Pure deterministic math in update() — harness-safe.
- **Global leaderboards**: express endpoints on the arcade server
(`/api/sando/scores` GET?week=omote|ura / POST {name,score,week}, json-file
storage `sando-scores.json`, top-100 kept) + a new nginx location
**`/arcade-api/` → arcade-landing:3001/api/** in the partly.party server
block. Game client `game/scores.ts` (4s timeout, silent-fail — the game never
notices a dead server). Week score = Σ verdict.total across served days
(registry `sando.score`, reset on week start/toggle); KANSEI submits under a
remembered chef name (window.prompt once — ponytail: that IS the entry UI)
and shows rank + podium; the title shows the top-5 board.
- **Infra learned the hard way** (documented in arcade-landing repo +
`forum-nginx.conf` canonical copy): partly.party is served by the FIRST
server block in `/home/humanjing/forum/nginx/nginx.conf` (a bind-mount into
forum-nginx; `conf.d/` is NOT included — edits there are inert; docker cp
fails busy — edit the host file + `nginx -s reload`).
- E2E proven: POST via https://partly.party/arcade-api/sando/scores → rank 1 →
GET returns it. Battery unchanged; crowd flinches on cut; day-1 serve banks
7.63 into the week score.
## BUILD-OUT PASS 5 ✅ — the judge speaks, the band worries, the jelly wobbles
- **Live barks**: every sin now earns a subtitled heckle at the foot of the stage
(`Stage.bark(group, seed)` → the existing line bank; seed varies the pick so
repeat sins don't repeat the insult). Wired: splat/crime→CUT-or-FLOOR lines,
late ticks→SERVICE, offstage/drop→FLOOR. Verified live: squash → "Mottainai. /
What a waste."
- **Anger-reactive music**: `music.setTension(anger)` (fed from updateHUD) —
tempo 620→380ms, heartbeat doubles past 0.6, plucks grow a sour minor-second
shadow past half-drawn. setTimeout-rescheduled so tempo shifts take hold.
- **Real wobble** (the last unused material param): jelly foods get two hidden
ghost masses (`mask:0`) on soft springs — pure slosh that honestly rocks the
body. `stage.addWobble` via makeFood when wobble>0.2; wobble parts cleaned up
in removeProp. New food **konnyaku** (wobble .85) replaces ura day 6 (Tamago
Ebi was the weakest concept): "it will not stop moving". Measured: same 2.2
nudge → cheese sando lean 1.1°, konnyaku sando 6.4° — 6× the rock, honest.
- **Ambient weather** per hour (5px generated dot): sun-dust motes at noon,
falling petals at dusk, blinking fireflies at night. (Particles animate on
rAF — invisible in the throttled preview pane, alive in real tabs.)
- Battery: everything identical + ura now AASSASSS with konnyaku day beatable.
## BUILD-OUT PASS 4 ✅ — 裏 THE URA WEEK (the Shogun)
- **Second week**: beat KANSEI and *the Shogun arrives*`URA_DAYS` (8 harder
orders), judged by a new character via the same-seed portrait trick
(`shogun_*` set, seed 620, oni mask + gold on blood-red; `Samurai.who`
prefixes the portrait, `shogun_bow` for an S). Ura is all moonlight, red-
bordered day cards, ramps 1.42.0, rush day at 18s.
- **New pantry** (the SHAPES are the difficulty): nori box(96,8) paper-thin,
ebi asymmetric convex 7-gon, daikon box(46,64) comically tall. Orders climax
in THE SHOGUN'S TOWER (7 layers, 80s).
- **Flow**: base KANSEI card → "the URA WEEK. the SHOGUN is hungry." → ura day 1;
ura KANSEI = **真・完成 SHIN-KANSEI** (true ending) → back to the quiet kitchen.
Ura unlock persists (localStorage `ura:true`); title gets a 表/裏 week toggle +
a second red-tinted stamp column (ura bests live at keys 100+).
- **Kabuki curtain**: day-advance restarts start covered by `curtain_red` (or a
plain red rect fallback) sweeping up. Death retries skip the curtain.
- Verified: base week SSSSSSSS; **ura week A A S S A S S S — all beatable,
honestly harder**; full arc served→kansei→ura(shogun judge)→shin-kansei→base
exercised in-browser; M1-M3 + disaster regression identical. Screenshot: the
moonlit ura kitchen with the Shogun is the best image the game has produced.
## BUILD-OUT PASS 3 ✅ — mobile, progression, music, night shift
- **Mobile/touch**: Phaser Scale FIT+CENTER (letterboxes on phones), 3 active
pointers, thumb **GRIP button** (touch devices only), drag-low = reach (no
button needed). Desktop input unchanged — battery numbers identical.
- **Progression** (`game/progress.ts`, localStorage `sandoniette.v1`): best grade
per day recorded on every live serve; **trophy shelf** of red hanko stamps
(day_stamp.png) on the title; **KANSEI** completion card after serving day 8
(all 8 stamps + grades, click → week restarts). Verified: serve→save→shelf→
kansei→day-0 all exercised in-browser.
- **Day-intro card** on each live day start (name + layers + pressure, fades).
- **Music** (`audio.ts music`): sparse seeded pentatonic plucks + taiko heartbeat
on setInterval (survives rAF throttling), starts at begin, **stops dead at the
string cut**, resumes next day. `sfx.chop()` thunk on every real cut.
- **Night shift**: days 1-3 noon washi, 4-6 `bg_washi_dusk`, 7-8 `bg_washi_night`
(moon + bonsai + lanterns — MODELBEAST batch 3, seeds 301 re-lit). Guarded
fallback to bg_washi if missing.
- Pushed to Gitea (`origin/main`). Battery unchanged-green after all of it:
M1 87f/8.41u/10-10 · M2 cv0-clean · M3 ✓✓ · days SSSSSSSS · live serve@31f ·
dead CUT. localStorage save verified ({0:"B"} after one live serve).
## BUILD-OUT PASS 2 ✅ — the playable loop, string unlockables, 8 days
- **Physical stacking** — `Sando` is now zone-DETECTION: the stack = whatever food
bodies rest in the plate zone (±82px, below-flight speed), sorted bottom→top.
A human stacking by marionette hand and the harness are judged identically.
`sando.kinds()` (strips piece `p` suffixes) is what the judge reads.
- **The full live loop** — `Game.state` (playing/served/dead) + `updateLive()`:
ticket clock with late-anger ramp (1 bump / 3s overdue), the **bow-dwell serve**
(hold the bar still 0.5s over a matching standing sando → scorecard), click →
next day (`registry sando.day` → `scene.restart`), string cut → ragdoll →
fail scorecard → click retries the SAME day with fresh strings. Day setup
(`Stage.setupDay`) stages bread slices + one whole food per non-bread kind on
the board. Live sins wired off `cutter.onCut` (squash→splat, botch→crimeCut) +
offstage sweep. (ponytail: no floor-drop sin yet — add when a day needs it.)
- **String unlockables** — title-screen picker: HEMP/RUBBER/CHAIN/SILK
(`STRING_STYLES` k/d multipliers on the same rig; registry `sando.style`).
Rubber visibly stretches — verified.
- **Days 68**: Tamago (egg rolls), Tofu (fragile), THE TOWER (5 layers). All 8
days S via harness. Live serve: dwell commits at **31 frames ≈ 0.5s**, grade B
(no cut performed — honest), day 8 same; death→retry + serve→advance both
close the loop (verified by simulated clicks + manual queue flush).
- **samurai_bow.png** (same seed 320) wired: an S-grade serve swaps the portrait
to the single approving nod. counter_wood/day_stamp/face_cut_bread banked in
`public/assets/img/`, unwired.
- **Embedded-pane trap (KNOWN, not a bug)**: the preview pane reports
`document.visibilityState='hidden'` → rAF never fires → live loop + queued
scene ops freeze; harness drives frames manually, and
`game.scene.processQueue()` flushes ops by hand. Real visible tabs unaffected.
- Full battery green: M1 87f/8.41u/10-10 · M2 cv0-clean/splat/wild · M3 ✓✓ ·
days SSSSSSSS · live served@31f / d8 served / dead CUT · cleaver CUTS.
## POLISH PASS (post-M6) ✅ — "make it even better"
- **Visible strings + control bar** — the biggest missed feel. `Marionette.stringSegments()`
/ `barSegment()`; `Stage.drawStrings()` redraws the 6 strings + the wooden bar each
frame (gone when cut). It now unmistakably reads as a marionette.
- **Cut-out faces** — procedural: two eyes + a worried "o" punched into each whole-food
silhouette in the bright washi tone (backlit-hole look). Pieces get no face.
(The generated face PNGs were full-colour cartoons that clashed — this fits.)
- **Order ticket HUD** (top-left) + **the cleaver**: a grabbable blade; held in a hand,
its edge drives the live cutter — swinging chops. Proven: `__s.swingCleaver()` cuts
clean (tip speed ~150, well over threshold). Full manual play (walk to blade, carry,
chop, stack, serve across the stage) is the intended-hard game; the wiring is there.
- All M1M6 harness numbers unchanged (strings/faces/cleaver don't perturb the sim):
dangle 87f · walk 8.83u up · grip 10/10 · chop cv0 clean · days SSSSS · disaster→cut.
## ALL SIX MILESTONES SHIP ✅ — full loop plays, three must-feels landed
(the dangle M1, the clean chop M2, the string cut M4). Final battery green:
M1 dangle 97f / walk 8.83u up / grip 10/10 · M2 chop cv0 clean / splat / offstage ·
M3 good survives + bad toppled · M4 disaster→string-cut ragdoll · M5 days 1-5 all
beatable. `npm run typecheck` + `npm run check` + `npm run build` all pass.
Possible next (Brief 2 territory): food faces as true silhouette cutouts, string
unlockables, more days/orders, real attract-mode, mobile/touch controls.
*"SHUBATTO! SANDO-NI-ETTE!!!" — read this file and execute it. It is written to
work cold: a fresh session with no other context should be able to build this
game from here. Sibling project: `~/Documents/toastsim` — do NOT touch it, but
DO steal its discipline (this brief tells you exactly what to steal).*
## 0. What this game is
A 2D shadow-puppet kitchen. You are a **silhouette marionette chef** operated
by strings — and only by strings. You cut comical food, stack it into
sandwiches, carry the dish across the stage, and serve it to your one
customer: a giant, permanently unimpressed **samurai**, glaring down at the
kitchen like a health inspector from hell. Do badly — drop things, mangle the
cut, stack a leaning tower, serve late — and his katana unsheathes a little
more. At maximum: **SHUBATTO** — one lightning slash, your strings are cut,
and the chef collapses into a floppy ragdoll amid flying ingredients. Restart.
The comedy IS the physics: puppeteer lag, pendulum swing, food with mass.
The difficulty is "cunty hard but fair" — Loot Cycle Inc. tension, not
Sekiro. Every failure must be readable as YOUR wobble, never RNG.
Fictional inspiration stack (for taste, not code): Loot Cycle Inc. (physics
triage under a ticking fail-state), Ratatouille (tiny chef, big kitchen),
1997 bargain-bin PS1 import energy (Pepsi Man voice, broken-Engrish UI).
## 1. The design law (inherited from TOASTSIM, adapted)
1. **One game, not minigames in a trench coat.** Cut, carry, stack, serve are
all the same verb — *move a pendulum with intent* — scored by the same
judge. A new mechanic must ride the string sim or improve it for everyone.
2. **The judge is the voice.** The samurai is toastsim's judge generalized:
named criteria with bands, a line bank with 8+ lines per criterion, the
scorecard as the emotional payoff. He speaks subtitled Japanese barks with
deadpan Engrish translations — *"SANDO... ga... yugande iru."* ("The
sandwich. It leans.")
3. **Physics is the comedian, the sim is honest.** No canned animations for
gameplay outcomes — if the tower falls, it falls because the center of
mass left the base. Comedy from real consequences, always.
4. **Nothing ships unverified.** The dev harness drives REAL input
deterministically and every milestone commits with measured numbers.
This is toastsim's single most valuable organ. Transplant it first.
5. **Failure must be readable.** The player must be able to say what they did
wrong from the wreckage alone. If a fail reads as random, it's a bug in
the DESIGN even when the code is correct.
## 2. Tech decisions (made — don't relitigate)
- **Vite + TypeScript + Phaser 3 (Matter physics build)**. One engine
dependency; Matter gives joints/constraints for the marionette and food.
Same repo shape as toastsim: `src/sim/` pure logic, `src/scenes/`
presentation, `src/dev.ts` harness, `npm run typecheck` must stay clean.
- **The strings are Verlet-ish chains of small Matter bodies + pin
constraints** (stiffness < 1 so they stretch visibly under abuse). If
Matter's constraint jitter fights back, fall back to pure-math Verlet
ropes rendered as polylines with only the ENDS as Matter bodies
pre-authorized judgment call.
- **Silhouettes that carry physics are procedural polygons** (flat black
vector shapes with visible pin-joints, like Balinese/Japanese shadow
puppets). Generated art is for backdrops, food faces, the samurai, title
never for collision shapes. (Toastsim law: silhouettes are gameplay.)
- **Determinism**: fixed-dt stepping (`app.step(dt)` equivalent Phaser's
`update` driven manually in harness mode), seeded RNG only. No `Date.now()`
in sim code.
## 3. The bones to build (in order; each is a pure module + a scene + harness verbs)
### 3.1 `sim/marionette.ts` — the puppet
Classic marionette rig: a **control bar** (the player's proxy) + strings to
head, two hands, two knees. Mouse position = bar position; mouse tilt comes
from horizontal velocity; hold LMB = lower the bar (crouch/reach), release =
lift; RMB or Space = the GRIP (both hands clamp). That's the whole control
surface depth comes from the pendulum, not the input map.
- Puppet body: 9 segments (head, torso ×2, upper/lower arms, thighs/shins as
single limbs to start), pin joints with angular limits.
- **The lag is the game**: strings transmit force with ~0.15s of felt delay
(stiffness/damping tuned, not a literal buffer). Walk = rhythmic bar sway
that gets the legs swinging. It must feel like Getting Over It's honesty
with Octodad's dignity.
- Harness: `__s.bar(x,y)`, `__s.sway(freq, amp, secs)`, `__s.grip(on)`,
`__s.pose()` joint angles, `__s.settle()` frames-to-rest.
- **Exit bar (M1)**: pendulum settle time measured; a scripted sway walks the
puppet 6 stage-units without face-planting; grip picks a box off the floor
10/10 times in harness. Feel: dangle a hand over a table edge it should
make you smile before any food exists.
### 3.2 `sim/food.ts` + cutting
Food = 2D convex-ish polygon bodies with mass, friction, a `face` sprite,
and toastsim-style material params: `skin.resistance`, `flesh.moisture`
(wet food SPLATS a 2D mess decal + slippery floor patch), `wobble`
(jelly-class food jiggles as soft constraint clusters).
- **The cut**: cleaver in gripped hand; a cut commits when blade-edge speed ×
angle-through-food crosses threshold (a slow push SQUASHES moisture
splats, no cut). Pieces are real bodies with the parent's face split
across them. Cut quality = blade angle + speed + follow-through, scored as
**clean / ragged / crime** (crime = squashed).
- Harness: `__s.spawnFood(id, x)`, `__s.swing(from, to, secs)`,
`__s.cutStats()` pieces, area CV (toastsim's pieceCV, straight port of
the idea), splat mass.
- **Exit bar (M2)**: a practiced harness swing produces 2 clean pieces with
area CV < 0.1; a slow push produces 0 pieces + splat > 0; a wild fast
swing sends a piece flying offstage (and that's CORRECT).
### 3.3 `sim/stack.ts` — the sando
Sandwich = ordered layers stacked by carrying pieces onto a plate zone.
Stability is honest Matter stacking, but SCORED: center-of-mass drift per
layer, overhang, lean angle. The order ticket names the target
(bread-filling-bread minimum; later: the katsu sando, the "tower" specials).
- Harness: `__s.stack()` → layers, lean°, COM drift; `__s.nudge(force)` for
the earthquake test.
- **Exit bar (M3)**: 3-layer sando stands; lean° reported; nudge test
topples a bad stack and not a good one; toppled food splats/rolls.
### 3.4 `sim/samurai.ts` — the judge + the fail state
The anger meter is toastsim's judge wearing armor. **Anger events**: drop
(+small), splat (+medium), crime-cut (+medium), served-wrong (+large),
served-late (+ramps), offstage food (+large). **Cooldown**: a clean serve
sheathes a little. Meter states = katana unsheath stages (0/25/50/75%:
eyebrow → hand-on-hilt → 10cm of blade → full glare, screen vignettes).
- **THE STRING CUT**: at 100% — freeze 300ms, slash line across the screen,
ALL string constraints deleted in one frame, ragdoll collapse, ingredients
launched with the impulse of the slash, gong. Score card over the wreckage.
It must be fast, unfair-FEELING but earned, and funny every single time.
- Serve = dish carried into the counter zone + a bow (hold still 0.5s — the
dwell commit, straight from toastsim).
- Scorecard groups: **THE CUT / THE SANDO / THE SERVICE / THE FLOOR** (the
floor = mess+drops; the bench criterion reborn). 8+ lines each, bilingual
bark + Engrish subtitle. Grade SD; S = "…umai." and a single approving nod.
- **Exit bar (M4)**: full loop playable — order in, cut, stack, carry, serve,
scored; harness plays a clean run (B or better) and a disaster run (strings
cut) deterministically; numbers in the commit message.
### 3.5 Orders, days, escalation (M5)
Ticket rail like toastsim's days: Day 1 = one open sando. Escalate
ingredient count, wetness (tomato before you've earned it is cruelty),
required cuts, and the samurai's patience ramp. **One new pressure per day,
never two.** Day N bar: each day introduces its thing and is beatable by the
harness with a scripted-but-honest run.
### 3.6 The juice (M6)
Title screen: giant brush-stroke 「サンドニエット」, the voice line (text
crawl + gong if no audio yet: **SHUBATTO!! SANDONIETTE!!!**), attract-mode
ragdoll. Shamisen sting + slash whoosh + wet splats (Web Audio, synthesized
or CC0 — no paid assets). Unlockable strings as difficulty modifiers —
**rubber** (bouncy, +lag +comedy), **chain** (heavy, slow, precise), **silk**
(tighter, for cowards) — pre-authorized to defer if M5 lands better without.
## 4. Assets — MODELBEAST, 2D ONLY (this game needs no meshes)
Steal `scripts/gen-assets.sh` from toastsim as the template, keep only the
flux path (`run_flux` + `run_cutout`). **Machine facts current as of
2026-07-17** (verified the hard way): client lives at `~/MODELBEAST` (NOT
~/Documents), token is `MB_TOKEN` in `~/Documents/backnforth/.env`, export
`MB_HOST=http://100.89.131.57:8777` (m3ultra queue). Node m4's venvs are
dead — every stage retries up to 10× with 60s pauses until a healthy node
takes the job; keep that loop until told m4pro is healed.
Style token (fix it, reuse it everywhere):
`"japanese shadow puppet theatre, flat black silhouette with cut-out
details, warm washi paper backdrop, edo period, high contrast, hand-crafted
paper texture"`.
Batch 1 (fixed seeds, start at 301): washi/shoji stage backdrop, wooden
stage floor boards, the samurai in 5 anger states (SAME seed, expression
phrase varies — the toastsim judge-portrait trick), title brushwork art,
paper order tickets, food FACES (bread, tomato, cheese, katsu, egg —
comical, slightly alarmed), splat decals, the katana slash frame.
Fire asset jobs FIRST, build procedural while they render, `loadProp`-style
catch-fallbacks so the game never blocks on art.
## 5. What to steal from toastsim, literally
Open these files and port the PATTERN (not the code — different engine):
- `src/dev.ts` — the harness: real input driven deterministically, helpers
returning stats objects, `window.__s` installer. THE most important file.
- `src/sim/cutting.ts` — pieceCV and the cvWord ladder; material params.
- `src/game/judging.ts` + `lines.ts` — criteria bands, weighted groups, the
line bank shape and its voice discipline.
- `OPUS-BUILD-BRIEF-2.md §0` — the verify discipline and "known traps" habit:
keep a STATUS section at the top of THIS file updated as you go, so any
session can pick up cold.
## 6. Milestone order and the bar
M0 bootstrap (vite+ts+phaser+harness skeleton, typecheck green, a Matter box
drops onto the stage floor) → M1 puppet → M2 cutting → M3 stacking → M4
samurai + full loop → M5 days → M6 juice. One commit per milestone, message
= what you measured. Screenshots when visuals change. If a feel is wrong,
fixing feel beats advancing — the three feels that must be RIGHT: **the
dangle** (M1), **the clean chop** (M2), **the string cut** (M4).
Pre-authorized judgment calls: all tuning constants; Verlet fallback (§2);
segment count on the puppet; deferring string unlockables; cutting a food
type that won't behave. Do NOT add: paid/cloud dependencies, accounts,
telemetry, or 3D. Do not touch `~/Documents/toastsim` or `~/MODELBEAST`
except through `mb`.
The bet: toastsim proved the bones (harness, honest sim, judge voice, free
assets) make a game feel handmade and alive. Sandoniette bets the same bones
survive transplant into slapstick — that "verified physics comedy" is a
genre, and we can build it in milestones without ever shipping an unfunny
fall or an unfair death.