diff --git a/src/course/seesaw-lab.ts b/src/course/seesaw-lab.ts index 1ee7eae..3dd0b31 100644 --- a/src/course/seesaw-lab.ts +++ b/src/course/seesaw-lab.ts @@ -1,33 +1,42 @@ /** - * SEE-SAW LAB — a throwaway test course (?course=seesaw) for tuning the see-saw - * obstacle in isolation, away from the noise of a real level. + * SEE-SAW LAB — a test course (?course=seesaw) for the see-saw obstacle. * - * Design that dodges the wedge problem: you roll along a RAISED DECK straight - * onto the plank's near end (both at y1.8 — a flush seam, no ramp to misalign), - * teeter across, and roll off the FAR end down onto the floor (a drop is never a - * wedge). A fan gives the momentum to carry over the pivot. + * Best-shot TEETERY design (John's call, fine-tune later): the plank rests + * PRE-TILTED — high near end flush with a raised deck, low far end near the + * floor. You roll off the deck onto the high end and DOWN the plank; your weight + * crossing to the far half pitches it steeper still, then dumps you off the low + * end onto the floor. Pre-tilt guarantees the teeter reads; the downhill line + * guarantees it never wedges. createSeeSaw snaps the plank back to rest each run + * (race:respawn) so it's boardable + teetery every time. * - * Knobs to tune for feel (John's call): the see-saw's `maxTilt` (how much it - * rocks) and `restTilt` (start it level or pre-tipped), and the fan `force`. + * Knobs to fine-tune the feel: the see-saw's `restTilt` (how steep it starts), + * `maxTilt` (how far your weight pitches it), the deck top height (must meet the + * tilted high end), and the fan `force`. */ import type { CourseSpec } from './spec' export const SEESAW_LAB: CourseSpec = { name: 'See-saw Lab', - spawn: [0, 2.3, 18], + spawn: [0, 2.9, 18], finish: { xMin: -6, xMax: 6, zMin: -32, zMax: -20, yMax: 4 }, boxes: [ // base floor — catches the drop off the plank and carries the finish { pos: [0, -0.5, -10], half: [10, 0.5, 32], color: '#efe3c6', roughness: 1 }, - // raised deck you roll along into the plank's near end (top y1.8) - { pos: [0, 0.9, 8], half: [4, 0.9, 12], color: '#f7edd2', cast: true }, + // raised deck (top y2.4) — meets the plank's pre-tilted HIGH near end at z-4 + { pos: [0, 1.2, 8], half: [4, 1.2, 12], color: '#f7edd2', cast: true }, // finish pad on the floor, past the drop { pos: [0, 0.4, -26], half: [6, 0.4, 4], color: '#FF6EB4', cast: true }, ], machines: [ - // fan on the deck — momentum to carry over the see-saw's pivot - { kind: 'fan', cfg: { id: 'sl-fan', position: [0, 2.2, 10], direction: [0, 0, -1], force: 30, range: 16, spread: 4 } }, - // the see-saw: deck flush into the +Z end (z-4), drops off the -Z end (z-12) to the floor - { kind: 'seesaw', cfg: { id: 'sl-seesaw', position: [0, 1.6, -8], plankSize: [8, 0.4, 3.5], tipAxis: 'z', maxTilt: 0.35 } }, + // fan on the deck — beam reaches PAST the pivot (range 22 covers the whole + // plank z-12..10) so it keeps pushing the blob over the pivot hump, not just + // up to it. Strong (force 40) because boarding the high end levels the plank + // and kills the downhill assist. + { kind: 'fan', cfg: { id: 'sl-fan', position: [0, 2.5, 10], direction: [0, 0, -1], force: 50, range: 22, spread: 4 } }, + // the see-saw: pre-tilted DOWN toward -Z. restTilt -0.15 raises the +Z (deck) + // end to ~2.4 (flush) and drops the -Z end to ~1.2 (roll off to floor). Small + // enough that boarding doesn't build a pivot hump the blob stalls on, but + // still visibly teetery + pitches further as your weight crosses. + { kind: 'seesaw', cfg: { id: 'sl-seesaw', position: [0, 1.6, -8], plankSize: [8, 0.4, 3.5], tipAxis: 'z', restTilt: -0.15, maxTilt: 0.4 } }, ], } diff --git a/src/machine/parts.ts b/src/machine/parts.ts index 5735c4b..f788bb4 100644 --- a/src/machine/parts.ts +++ b/src/machine/parts.ts @@ -328,6 +328,15 @@ export function createSeeSaw(world: World, cfg: SeeSawConfig): MachinePart { jd.limits = [-maxTilt, maxTilt] physics.createImpulseJoint(jd, anchor, plankBody, true) + // A tilting plank has no restoring force, so after a crossing it stays wherever + // it settled (neutral equilibrium). Snap it back to its rest tilt each run so a + // pre-tilted plank is boardable + teetery every time, not just the first. + world.events.on('race:respawn', () => { + plankBody.setRotation({ x: q0.x, y: q0.y, z: q0.z, w: q0.w }, true) + plankBody.setAngvel({ x: 0, y: 0, z: 0 }, true) + plankBody.setLinvel({ x: 0, y: 0, z: 0 }, true) + }) + const plank = new THREE.Mesh( new THREE.BoxGeometry(hx * 2, py, hz * 2), new THREE.MeshStandardMaterial({ color: '#a1887f', roughness: 0.7 }),