import type { Stage } from './scenes/stage'; import { FLOOR_Y } from './scenes/stage'; /** * Dev harness — toastsim's single most valuable organ, transplanted. * * Drives the REAL sim deterministically at a fixed dt so "does it actually work" * is answered by measured numbers, not by squinting. Every helper returns a * stats object. Installed on window.__s in dev builds. Milestones add verbs; * nothing ships without a harness number behind it. */ const STAGE_UNIT = 40; // px per "stage-unit" — the walk bar is measured in these const HOME_X = 400; export function installHarness(stage: Stage): void { const p = () => stage.puppet; const M = () => stage.matter.body; const step = (frames: number, dt = 1000 / 60): void => { stage.auto = false; for (let i = 0; i < frames; i++) stage.stepSim(dt); }; /** Hold the bar at (x,y) for `frames`, stepping each frame (no tilt). */ const hold = (x: number, y: number, frames: number, grip = false): void => { stage.auto = false; for (let i = 0; i < frames; i++) { p().setBar(x, y, 0); p().setGrip(grip); stage.stepSim(); } }; /** Frames until the swing steadies. Soft strings keep a marionette gently * alive forever, so "settled" means the energy has plateaued (stopped * falling), not literal zero. Returns frames-to-steady. */ const settle = (maxFrames = 600): number => { stage.auto = false; let f = 0, low = Infinity, plateau = 0; for (; f < maxFrames; f++) { stage.stepSim(); const v = p().restSpeed(); if (v < low - 0.02) { low = v; plateau = 0; } else if (++plateau > 45) break; if (v < 0.15) break; } return f; }; const placeCrate = (x: number): void => { M().setPosition(stage.crate, { x, y: FLOOR_Y - 16 }, false); M().setAngle(stage.crate, 0, false); M().setVelocity(stage.crate, { x: 0, y: 0 }); M().setAngularVelocity(stage.crate, 0); }; const s = { stage, step, settle, pose: () => p().pose(), /** Set the control bar to an absolute position and step one frame. */ bar(x: number, y = p().barHomeY) { stage.auto = false; p().setBar(x, y, 0); stage.stepSim(); return p().pose(); }, grip(on: boolean) { stage.auto = false; p().setGrip(on); stage.stepSim(); return { gripping: p().gripping, hands: p().handWorld() }; }, /** Oscillate the bar around home: freq Hz, amp px, for secs seconds. */ sway(freq = 1.2, amp = 90, secs = 3) { stage.auto = false; const frames = Math.round(secs * 60); for (let i = 0; i < frames; i++) { const x = HOME_X + amp * Math.sin((2 * Math.PI * freq * i) / 60); p().applyInput(x, p().barHomeY, false, false); stage.stepSim(); } return p().pose(); }, /** THE DANGLE (M1 feel gate): yank the bar aside, let go, measure the swing * home. Long settle + a clean decay = a puppet that has real pendulum. */ dangle() { hold(HOME_X, p().barHomeY, 30); // start at rest hold(HOME_X + 220, p().barHomeY, 12); // yank right p().setBar(HOME_X, p().barHomeY, 0); // release to home const frames = settle(); return { settleFrames: frames, ...p().pose() }; }, /** THE WALK (M1 exit-bar): a rhythmic sway whose centre drifts `dir`, so the * legs swing and the puppet is dragged along. Reports units travelled and * whether it stayed upright the whole way. */ walk(dir = 1, secs = 8, freq = 1.2, amp = 45, advance = 460) { hold(HOME_X, p().barHomeY, 20); const x0 = p().torsoX; const frames = Math.round(secs * 60); let upright = true; for (let i = 0; i < frames; i++) { const center = HOME_X + dir * advance * (i / frames); const x = center + amp * Math.sin((2 * Math.PI * freq * i) / 60); p().applyInput(x, p().barHomeY, false, false); stage.stepSim(); if (!p().pose().upright) upright = false; } const dx = p().torsoX - x0; return { units: +(dx / STAGE_UNIT).toFixed(2), dx: +dx.toFixed(1), upright, faceplant: !upright, lean: p().pose().lean, }; }, /** Reach down over the crate, grip, lift — did the crate leave the floor? */ pickup(crateX = 470) { placeCrate(crateX); // hover the hands over the crate, then reach + grip hold(crateX, p().barHomeY, 25); hold(crateX, p().barHomeY + 130, 30, true); // reach + clamp const grabbedY = stage.crate.position.y; hold(crateX, p().barHomeY - 40, 40, true); // lift const lift = grabbedY - stage.crate.position.y; const held = stage.crate.position.y < FLOOR_Y - 40; p().setGrip(false); return { lift: +lift.toFixed(1), crateY: +stage.crate.position.y.toFixed(1), held }; }, /** Grip must lift the crate 10/10. Vary the crate x each trial. */ pickupTest(n = 10) { let ok = 0; const lifts: number[] = []; for (let i = 0; i < n; i++) { stage.auto = false; hold(HOME_X, p().barHomeY, 25); // re-dangle to home between trials const r = this.pickup(440 + (i % 5) * 15); if (r.held) ok++; lifts.push(r.lift); } return { ok, n, rate: `${ok}/${n}`, lifts }; }, }; (window as { __s?: typeof s }).__s = s; // eslint-disable-next-line no-console console.log('[dev] __s ready — __s.dangle() __s.walk() __s.pickupTest()'); }