BLOBBO/src/course/toaster-alley.ts
type-two 67921925f5 feat(course): data-driven courses + a second course (Toaster Alley)
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>
2026-07-19 15:19:09 +10:00

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