- vite.config.ts: dev base '/', build base '/sandoniette/' (subpath deploy). - Asset loads use import.meta.env.BASE_URL so paths resolve at the subpath regardless of trailing slash. vite-env.d.ts for the env types. - deploy.sh: build -> rsync dist/ -> docker cp into forum-nginx web root /usr/share/nginx/html/sandoniette (matches the partly.party games pattern). - Verified LIVE: index+JS 200, JS served as application/javascript (not the HTML-fallback failure), PNGs image/png, title screen renders in-browser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
20 KiB
サンドニエット 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 intoforum-nginx:/usr/share/nginx/html/sandoniette). Games VPShumanjing@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.tsbases dev at/, build at/sandoniette/; asset loads useimport.meta.env.BASE_URL. Local preview needsvite 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 viapuppet.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 viastage.stepSim(dt)(which also callspuppet.update()). Render contract: Matter body = truth, flat silhouetteProp.viewfollows.Rng= mulberry32. Tuning knobs:Kat top of marionette.ts. Harnessreset(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 squash→splat 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 ingeom.check.ts); (b) MattersetVelocityis 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.standingis 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;shiftBodyTowaits for the hips to arrive, then reach). Now a true 10/10. Exit-bar: centred 3-layer sando stands + survives quake (lean 0→2.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 S–D),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 S↔D. Verified in-browser (scorecard + string-cut screenshots) + full battery. Trap fixed: field namedgamecollided withPhaser.Scene.game→ renamedsession; 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.
DAYSingame/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 bysamurai.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 brushtitle_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, productionnpm run buildclean.
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 M1–M6 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)
- 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.
- 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.")
- 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.
- 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.
- 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.tsharness,npm run typecheckmust 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'supdatedriven manually in harness mode), seeded RNG only. NoDate.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 S–D; 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.__sinstaller. 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.