feat(seesaw): teetery pre-tilted tipping-plank in the see-saw lab

Best-shot "teetery" tuning (John's call; fine-tune the feel later). The lab
see-saw now rests PRE-TILTED (~9° down toward the exit): you roll off a raised
deck onto its high end and down the plank, your weight pitches it steeper as you
cross the pivot, and you drop off the low end to the floor. Pre-tilt makes the
teeter read; a strong fan whose beam covers the whole plank carries the blob
over the pivot hump; the downhill line keeps it from wedging.

createSeeSaw now snaps the plank back to its rest tilt on race:respawn (a
free-tipping plank has no restoring force, so it would otherwise stay wherever
it settled and stop being boardable). Verified: rests -8.6deg, pitches to
-10.8deg mid-crossing, clears the pivot and drops off the far end.

Still lab-only (?course=seesaw), not in a shipped course.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 00:23:11 +10:00
parent 85c29111fa
commit 2cf2d15623
2 changed files with 33 additions and 15 deletions

View File

@ -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 } },
],
}

View File

@ -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 }),