- sim/marionette.ts: control bar + 6 soft strings (head/back/2 hands/2 knees) over a 9-segment silhouette. Bar snaps to mouse; the felt lag is string stiffness<1 (worldConstraints), not an input buffer. - Feet planted on floor + head+back strings keep the torso upright (no topple); earlier pure-pendulum rig spun past 360deg — the back string fixed it. - Sticky grip: while gripped, any empty hand latches the nearest target on contact (puppet.update() each step). Reach = LMB drops the bar. - Harness verbs: bar/sway/grip/pose/settle + dangle/walk/pickupTest + reset. - Exit-bar measured: dangle settles 87 frames upright (lean 4.6); walk 8.84 units upright (>6 req, lean 30<45); grip 10/10. Verified in-browser + shot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
157 lines
5.4 KiB
TypeScript
157 lines
5.4 KiB
TypeScript
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()');
|
|
}
|