From e06e17296ee27f0976d71c37b0d8755072d835ed Mon Sep 17 00:00:00 2001 From: type-two Date: Mon, 20 Jul 2026 17:14:22 +1000 Subject: [PATCH] =?UTF-8?q?feat(course):=20Butter=20Run=20=E2=80=94=20the?= =?UTF-8?q?=20oil-luge=20course?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third course (?course=butter), pure data. Surfaces are the star: two long butter/oil strips form the fast centre line where steering barely works (slip: scrub x0.12, brake x0.08 — commit and ride), while the safe side lanes weave through staggered slalom walls. A CROSSWIND fan blows across the gap between the strips; the fan applies equal impulse to every body, so mass is the counter — pop the purple weight-bubbles before the luge (MEGA = heavy) and the wind barely moves you, run in light and it shoves you into the slalom. Paint = weight = route, no new mechanics. Red start cannons hand out BURN speed; two checkpoint pairs (pre/post-luge); flat floor, no fall-outs. Course switcher now cycles breakfast -> toaster -> butter -> breakfast (the see-saw lab stays URL-only — it's a tuning rig, not a course). Verified: course builds clean (strips/walls/posts/bubbles/finish all present, centre lane trap-free), the controller's ground ray resolves the strips to {surface:'oil'} friction 0.04 (the exact input the previously-verified slip branch consumes), no console errors, tsc + build green. Note: motion *feel* not measurable in the test harness (background tab throttles the sim to ~1 step/s) — the slip code path is identical to Breakfast Rush's verified patch. Co-Authored-By: Claude Fable 5 --- src/course/butter-run.ts | 55 ++++++++++++++++++++++++++++++++++++++++ src/course/spec.ts | 4 ++- src/game.ts | 11 +++++--- 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 src/course/butter-run.ts diff --git a/src/course/butter-run.ts b/src/course/butter-run.ts new file mode 100644 index 0000000..425b412 --- /dev/null +++ b/src/course/butter-run.ts @@ -0,0 +1,55 @@ +/** + * BUTTER RUN — the oil-luge course (?course=butter). Surfaces are the star: + * two long butter/oil strips form the fast centre line where steering barely + * works (slip: scrub ×0.12, brake ×0.08 — you commit and ride), while the safe + * side lanes wind through slalom walls, slow but steerable. + * + * The twist mid-luge: a CROSSWIND fan blows across the gap between the strips. + * The fan applies the same impulse to every body, so mass is the counter — + * pop the PURPLE bubbles before the luge (MEGA = heavy) and the wind barely + * moves you; run in light and it shoves you into the slalom lane. Paint = + * weight = route, no new mechanics, just the systems talking to each other. + * + * RED cannons at the start hand out BURN speed so you enter the luge hot. + * Flat wide floor throughout — no fall-outs; falls are only for the void edges. + */ +import type { CourseSpec } from './spec' + +export const BUTTER_RUN: CourseSpec = { + name: 'Butter Run', + spawn: [0, 1.8, 30], + finish: { xMin: -7, xMax: 7, zMin: -44, zMax: -34, yMax: 4 }, + boxes: [ + // base floor — one slab, spans everything + { pos: [0, -0.5, -8], half: [14, 0.5, 48], color: '#efe3c6', roughness: 1 }, + // start pad + { pos: [0, 0.4, 28], half: [6, 0.4, 5], color: '#f7edd2', cast: true }, + // slalom walls — SIDE lanes only (centre |x|<4 stays clear for the luge). + // Staggered so the safe route is a weave, not a straight line. + { pos: [-7, 1.1, 6], half: [3, 1.1, 0.5], color: '#8d6e63', cast: true }, + { pos: [7, 1.1, 0], half: [3, 1.1, 0.5], color: '#8d6e63', cast: true }, + { pos: [-7, 1.1, -6], half: [3, 1.1, 0.5], color: '#8d6e63', cast: true }, + { pos: [7, 1.1, -12], half: [3, 1.1, 0.5], color: '#8d6e63', cast: true }, + { pos: [-7, 1.1, -18], half: [3, 1.1, 0.5], color: '#8d6e63', cast: true }, + { pos: [7, 1.1, -24], half: [3, 1.1, 0.5], color: '#8d6e63', cast: true }, + // finish podium + { pos: [0, 0.6, -39], half: [7, 0.6, 4], color: '#FF6EB4', cast: true }, + ], + machines: [ + // purple weight-bubbles before the luge: MEGA mass = crosswind resistance + { kind: 'bubbles', cfg: { id: 'br-bubbles', position: [0, 1.3, 18], color: 'purple', radius: 0.6, splats: 2, count: 6, area: [4, 1.0, 3], drift: [0, 0.3, 0], bubbleRadius: 0.6 } }, + // butter strip A — enter hot, ride it straight + { kind: 'oil', pos: [0, 0.06, 2], half: [2.5, 0.06, 8] }, + // CROSSWIND across the gap between the strips (blows +x, into the slalom) + { kind: 'fan', cfg: { id: 'br-wind', position: [-11, 1.0, -10], direction: [1, 0, 0], force: 26, range: 22, spread: 2.6 } }, + // butter strip B — re-align in the windy gap, then commit again + { kind: 'oil', pos: [0, 0.06, -20], half: [2.5, 0.06, 8] }, + ], + cannons: [ + // RED crossfire at the start: BURN speed to enter the luge fast + { color: 'red', position: [-9, 3.2, 24], interval: 1.8 }, + { color: 'red', position: [9, 3.2, 24], interval: 1.8 }, + ], + // pre-luge and post-luge — a crosswind shove into a wall costs seconds, not the run + checkpoints: [[0, 0, 16], [0, 0, -30]], +} diff --git a/src/course/spec.ts b/src/course/spec.ts index 8ae8c84..b20a548 100644 --- a/src/course/spec.ts +++ b/src/course/spec.ts @@ -25,6 +25,7 @@ import type { } from '../machine/index' import { TOASTER_ALLEY } from './toaster-alley' import { SEESAW_LAB } from './seesaw-lab' +import { BUTTER_RUN } from './butter-run' export interface FinishBox { xMin: number; xMax: number; zMin: number; zMax: number; yMax: number } @@ -173,5 +174,6 @@ export function buildCourseFromSpec( * game.ts and is the default). Load with `?course=`. */ export const COURSES: Record = { toaster: TOASTER_ALLEY, - seesaw: SEESAW_LAB, + butter: BUTTER_RUN, + seesaw: SEESAW_LAB, // tuning lab — URL-only, not in the switcher rotation } diff --git a/src/game.ts b/src/game.ts index 944132f..7d90b68 100644 --- a/src/game.ts +++ b/src/game.ts @@ -372,10 +372,15 @@ export function installGame(world: World) { setTimeout(() => toast.remove(), 3200) }) - // ---- course switcher: a small link to hop between courses (URL param) ---- + // ---- course switcher: cycles breakfast → toaster → butter → breakfast. + // (The see-saw lab stays URL-only — it's a tuning rig, not a course.) ---- { - const other = spec ? { href: 'index.html', label: '🍳 Breakfast Rush' } - : { href: '?course=toaster', label: '🍞 Toaster Alley' } + const next: Record = { + breakfast: { href: '?course=toaster', label: '🍞 Toaster Alley' }, + toaster: { href: '?course=butter', label: '🧈 Butter Run' }, + butter: { href: 'index.html', label: '🍳 Breakfast Rush' }, + } + const other = next[which ?? 'breakfast'] ?? next.butter // unknown/lab courses cycle home const link = document.createElement('a') link.href = other.href link.textContent = `▸ course: ${other.label}`