feat(course): Butter Run — the oil-luge course
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 <noreply@anthropic.com>
This commit is contained in:
parent
639de3adf8
commit
e06e17296e
55
src/course/butter-run.ts
Normal file
55
src/course/butter-run.ts
Normal file
@ -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]],
|
||||
}
|
||||
@ -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=<name>`. */
|
||||
export const COURSES: Record<string, CourseSpec> = {
|
||||
toaster: TOASTER_ALLEY,
|
||||
seesaw: SEESAW_LAB,
|
||||
butter: BUTTER_RUN,
|
||||
seesaw: SEESAW_LAB, // tuning lab — URL-only, not in the switcher rotation
|
||||
}
|
||||
|
||||
11
src/game.ts
11
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<string, { href: string; label: string }> = {
|
||||
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}`
|
||||
|
||||
Loading…
Reference in New Issue
Block a user