A course is now a plain data object (boxes + machines + cannons + spawn/finish) built by buildCourseFromSpec, which dispatches to the greybox box builder and the Lane C machine factories. New courses are DATA, not a game.ts fork; load one with ?course=<name>. Added COURSES.toaster = Toaster Alley, a flat, reliably completable second level that puts the never-placed FAN into play (tailwind) and reuses the mine→gate loop from a spec. game.ts refactor: Breakfast Rush's tuned build moves verbatim into buildBreakfastRush (returns its finish box); installGame picks the course by ?course=, parametrises the race loop's finish + respawn spawn, keeps a per-course best time, and adds a small on-screen course switcher. Breakfast Rush is untouched behaviourally (greybox now builds after the blob — colliders still exist before the first physics step). Deliberately NOT done: porting the finely-tuned Breakfast Rush to a spec (no gameplay gain, real regression risk), and placing a see-saw (it's a supported spec kind but wants an elevated bridge = deliberate level design). Browser-verified: Toaster Alley completes end-to-end (best 4.73s saved under its own key, green mine→gate loop opens the line); Breakfast Rush unchanged (spawns on the plateau, crosses the gap-jump, puddles paint it). tsc + vite build green; all 10 test suites pass (360 assertions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
2.6 KiB
TypeScript
49 lines
2.6 KiB
TypeScript
/**
|
|
* TOASTER ALLEY — a second course, authored entirely as DATA (see spec.ts).
|
|
* Deliberately flat (one big catch-floor, no fall-outs and no elevated
|
|
* geometry) so it is reliably completable without the fine tuning Breakfast
|
|
* Rush needed. It also puts the FAN — a machine that shipped but was never
|
|
* placed — into play as a tailwind.
|
|
*
|
|
* Flow (+Z start → -Z finish): a fan sweeps you off the start pad; red cannons
|
|
* paint you for BURN speed; a green reward mine gives GRIP; a green colour-gate
|
|
* (fed by that mine) opens the straight line to the finish.
|
|
*
|
|
* NOTE: the SEE-SAW is also a supported spec kind (`{kind:'seesaw'}`), but a
|
|
* good see-saw needs an elevated bridge (approach platform + landing) tuned so
|
|
* the blob steps on cleanly — that's level design worth doing deliberately, so
|
|
* it's left out of this flat demo rather than shipped half-working.
|
|
*/
|
|
import type { CourseSpec } from './spec'
|
|
|
|
export const TOASTER_ALLEY: CourseSpec = {
|
|
name: 'Toaster Alley',
|
|
spawn: [0, 1.8, 30],
|
|
finish: { xMin: -7, xMax: 7, zMin: -38, zMax: -26, yMax: 4 },
|
|
boxes: [
|
|
// base floor — spans the whole alley so nothing falls out
|
|
{ pos: [0, -0.5, -5], half: [14, 0.5, 42], color: '#efe3c6', roughness: 1 },
|
|
// start pad
|
|
{ pos: [0, 0.4, 28], half: [6, 0.4, 5], color: '#f7edd2', cast: true },
|
|
// gate flank walls (centre gap x[-2,2] is the gate) — clear side lanes remain
|
|
{ pos: [-3, 1.5, -22], half: [1, 1.5, 0.4], color: '#5b6b7a', roughness: 0.85, cast: true },
|
|
{ pos: [3, 1.5, -22], half: [1, 1.5, 0.4], color: '#5b6b7a', roughness: 0.85, cast: true },
|
|
// finish podium
|
|
{ pos: [0, 0.6, -32], half: [7, 0.6, 5], color: '#FF6EB4', cast: true },
|
|
],
|
|
machines: [
|
|
// FAN — a tailwind that sweeps you off the start pad down the alley
|
|
{ kind: 'fan', cfg: { id: 'ta-fan', position: [0, 1.8, 20], direction: [0, 0, -1], force: 20, range: 18, spread: 4.5 } },
|
|
// green reward mine — GRIP, and it feeds the green gate downstream
|
|
{ kind: 'mine', cfg: { id: 'ta-mine', position: [0, 0.05, 4], color: 'green', radius: 0.9, splats: 5, impulse: 5, direction: [0, 1, -0.3], size: [3.4, 3.4] } },
|
|
// green gate — opens for the runner who took the reward mine (same loop as
|
|
// Breakfast Rush, proving the gate works identically from a data spec)
|
|
{ kind: 'gate', cfg: { id: 'ta-gate', position: [0, 0.02, -22], color: 'green', size: [4, 3, 0.6] }, needColor: 'green', needPct: 0.4 },
|
|
],
|
|
cannons: [
|
|
// RED crossfire on the run-up — BURN speed to carry you down the alley
|
|
{ color: 'red', position: [-9, 3.5, 12], interval: 1.8 },
|
|
{ color: 'red', position: [9, 3.5, 12], interval: 1.8 },
|
|
],
|
|
}
|