From 0f2aca542bda408e2ae7febebcea6d6778cfd0c5 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 18 Jul 2026 12:18:30 +1000 Subject: [PATCH] Wave 2 integration: zones staged, fork machines own the district, tuning pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - installZones + cannonConfigs through the single cannon path; old centre plate/boot removed (fork versions supersede); belt+bucket to centre lane (sunk flush — proud face curbed the line); arch punishes failed MEGA runs - puddles: distance-based stamping (speed-independent coverage), rate 4/u, contact tolerance 1.0 (bounce hops starved strips), fork strips widened toward the racing line + neutral-centre red top-up strip - under-shelf volumes filled solid in the centre (side corridors left open); launcher catch widened to the full gap - verified: naive hold-W finishes ~12s, ghost records+replays, MINI (0.77) passes the tunnel with 47% pink, buffs activate (GRIP live at 29% green), 78% total coverage on a wandering run Co-Authored-By: Claude Fable 5 --- src/course/zones.ts | 11 ++++-- src/game.ts | 83 +++++++++++++++++++------------------------- src/paint/puddles.ts | 18 +++++++--- vite.config.ts | 1 + 4 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/course/zones.ts b/src/course/zones.ts index 8d35180..8a50f27 100644 --- a/src/course/zones.ts +++ b/src/course/zones.ts @@ -89,8 +89,15 @@ const PUDDLE_LAYOUT: PaintPuddleConfig[] = [ // PURPLE/PINK fork (machine district, z≈-37..-58) on the machine-area floor. // Strips sit at the district ENTRANCE so the blob is fully MEGA/MINI BEFORE it // reaches the plate / tunnel further in. - { color: 'purple', position: [-6, 0.27, -40], size: [6, 7], splatRadius: 0.34 }, - { color: 'pink', position: [6.5, 0.27, -40], size: [6, 7], splatRadius: 0.34 }, + // Widened toward the racing line (integration tune): at race speed a fork + // strip at x±6 was unreachable by the time you could steer post-launcher. + // A ~1.5u neutral gap at x≈0 still lets a centre runner stay unforked. + { color: 'purple', position: [-4.5, 0.27, -40], size: [8, 8], splatRadius: 0.34 }, + { color: 'pink', position: [5, 0.27, -40], size: [8, 8], splatRadius: 0.34 }, + // Neutral-centre red top-up (integration tune): the fast launcher route is + // airborne through most strips; this grounded stretch lets a centre runner's + // red cross the 20% threshold — every route should earn SOME buff. + { color: 'red', position: [0, 0.27, -44], size: [2.2, 12], splatRadius: 0.34 }, ] // Fork geometry constants (obstacles sit PAST the entrance strips). diff --git a/src/game.ts b/src/game.ts index 40fef5b..f5d9333 100644 --- a/src/game.ts +++ b/src/game.ts @@ -11,7 +11,8 @@ import { createBlobController } from './blob/controller' import { createBlobFeel } from './blob/feel' import { createFollowCamera } from './blob/camera' import { PaintSkin, PaintCannon, BuffSystem, PaintHUD, installPaint } from './paint/index' -import { createPressurePlate, createSpringBoot, createBucketDump, createBubbleArch, createConveyorBelt } from './machine/index' +import { createBucketDump, createBubbleArch, createConveyorBelt } from './machine/index' +import { installZones } from './course/zones' import { installAudio } from './audio/index' import { installGhost } from './ghost/index' import { installTitle } from './ui/title' @@ -34,62 +35,46 @@ export function installGame(world: World) { const camera = createFollowCamera(world, blob) world.onFrame((dt) => camera.update(dt)) - // ---- paint (Lane B): cannons staged along the racing line ---- + // ---- paint economy (Lane G): puddles + colour zones + MEGA/MINI fork ---- + // Zones build the fork's own plate/boot (purple lane) and the MINI tunnel + // (pink lane), and return cannon configs for the one cannon-staging path here. const skin = blob.paint as PaintSkin - const cannonSpecs = [ - { color: 'red' as const, position: new THREE.Vector3(-9, 2.5, 14), triggerId: 'cannon-red' }, - { color: 'green' as const, position: new THREE.Vector3(9, 4.5, -6), triggerId: 'cannon-green' }, - { color: 'blue' as const, position: new THREE.Vector3(-9, 2.0, -30), triggerId: 'cannon-blue' }, - ] - for (const spec of cannonSpecs) { + const zones = installZones(world, blob, skin) + for (const spec of zones.cannonConfigs) { world.addSystem(new PaintCannon({ world, - position: spec.position, + position: new THREE.Vector3(...spec.position), color: spec.color, target: blob.mesh, paint: skin, targetRadius: blob.radius, triggerId: spec.triggerId, - interval: 2.2, + interval: spec.interval, muzzleSpeed: 26, // default 16 caps ballistic range at ~18u — course distances need ~26 splatRadius: 0.45, })) } - // ---- machine chain (Lane C) on the course's machine area ---- - // The pressure plate needs MORE than a clean blob's mass: paint = weight = - // machine access (GDD §5.2/§5.4). Clean blobs roll over it; painted trip it. + // ---- centre-lane machine garnish (Lane C) ---- + // The fork owns plate/boot now (zones.ts). The belt+bucket moved to the + // CENTRE lane: un-forked runners get slow-carried under a purple dump — + // a free taste of MEGA with no roof above (never dump grow-juice inside + // the MINI tunnel: growing under the roof would wedge you). const baseMass = blob.body.mass() - createPressurePlate(world, { - id: 'plate-1', position: [0, 0.24, -40], - massThreshold: baseMass * 1.25, emits: 'boot-1', size: [4, 3], - }) - createSpringBoot(world, { - id: 'boot-1', position: [0, 0.2, -42.5], // 0.5s telegraph ≈ 3u of roll past the plate - impulse: baseMass * 18, // sized for a plate-tripping (painted, heavy) blob to reach the bucket - direction: [0, 1, -0.55], - onSignal: 'boot-1', strikeSize: [2.4, 1.4, 3], - }) - // Bucket hangs over the conveyor: the belt delivers you underneath at 4 u/s, - // the catch trigger arms the teeter, and the pour lands at the spout point - // (bucket.x + 1.4) — which is the belt's centerline. Linger and get soaked. createBucketDump(world, { - // Timed empirically: catch trips at z≈-46, teeter ≈0.8s, belt 3 u/s → - // blob reaches the spout (bucket.z) as the pour lands. Miss was 1.73u - // with bucket at -50 / belt 4. - id: 'bucket-1', position: [4.6, 6, -46.5], + id: 'bucket-1', position: [-1.4, 6, -50], color: 'purple', radius: 0.8, - // Shallow catch just upstream of the spout: observed teeter drift is only - // 0.4-2u depending on speed, so trip near the spout, not a belt-length away. + // Shallow catch just upstream of the spout (bucket.x+1.4 = belt centre x=0): + // observed teeter drift is 0.4-2u depending on speed. trigger: { size: [3, 2, 1.6], offset: [1.4, -5.2, 0.75] }, }) - createBubbleArch(world, { - id: 'arch-1', position: [-4.5, 0.12, -56], fraction: 0.6, // far-side lane: cleansing is a detour you choose - }) createConveyorBelt(world, { - id: 'belt-1', position: [6, 0.25, -50], + id: 'belt-1', position: [0, 0.0, -50], // sunk flush with the machine pad — a proud belt face curbed the racing line size: [3, 0.5, 10], velocity: [0, 0, -3], }) + createBubbleArch(world, { + id: 'arch-1', position: [-4.5, 0.12, -56], fraction: 0.6, // purple-lane exit: cleansing punishes a failed MEGA attempt + }) world.addSystem(new BuffSystem(world, { mesh: blob.mesh, modifiers: blob.modifiers, paint: skin })) installPaint(world, { mesh: blob.mesh, paint: skin }) @@ -111,16 +96,20 @@ export function installGame(world: World) { world.scene.add(tramp) world.physics.createCollider( world.rapier.ColliderDesc.cuboid(6, 0.25, 2).setTranslation(0, 0.25, 0)) - // Seal the under-shelf cavity: fast racers overshoot the slot and slip - // beneath the landing shelf into an inescapable wedge (constant minZ -17 in - // testing). Wall is flush with the shelf face, so it reads as its plinth. - const plinth = new THREE.Mesh( - new THREE.BoxGeometry(14, 2.5, 0.5), - new THREE.MeshStandardMaterial({ color: '#0A5FCB', roughness: 0.8 })) - plinth.position.set(0, 1.25, -2) - world.scene.add(plinth) - world.physics.createCollider( - world.rapier.ColliderDesc.cuboid(7, 1.25, 0.25).setTranslation(0, 1.25, -2)) + // Fill BOTH under-shelf volumes solid: the front-face plinth stopped + // overshooters, but lane-huggers rolled in from the open x±7 sides and + // wedged (observed at (-4.1,-7.9)). Under-shelf space serves no gameplay — + // make the shelves read as solid blocks down to the floor. + const plinthMat = new THREE.MeshStandardMaterial({ color: '#0A5FCB', roughness: 0.8 }) + // Central fill only (x±5.2): observed wedges are all |x|<4.5; full-width + // fills walled off the floor-level side corridors lane-hug routes use. + for (const [zc, hz] of [[6, 4], [-6, 4]] as const) { + const fill = new THREE.Mesh(new THREE.BoxGeometry(10.4, 2.8, hz * 2), plinthMat) + fill.position.set(0, 1.4, zc) + world.scene.add(fill) + world.physics.createCollider( + world.rapier.ColliderDesc.cuboid(5.2, 1.4, hz).setTranslation(0, 1.4, zc)) + } // Deterministic launcher, not restitution: raw bounciness pachinkos off the // shelf's front edge and can still slip underneath. Catch a falling blob on // the pad and set a clean arc onto the landing shelf. @@ -130,7 +119,7 @@ export function installGame(world: World) { bounceCooldown = Math.max(0, bounceCooldown - dt) const t = blob.body.translation() const v = blob.body.linvel() - const onPad = Math.abs(t.x) < 6 && t.z > -2.2 && t.z < 2.2 && t.y < 1.4 + const onPad = Math.abs(t.x) < 7.2 && t.z > -2.2 && t.z < 2.2 && t.y < 1.4 // full gap width — lane-huggers fall at |x|≈7 if (onPad && v.y < 0.5 && bounceCooldown === 0) { // High + centered: held-W air control (~24 u/s²) flattens shallow arcs // into the shelf face, so go UP hard and pull toward the slot center; diff --git a/src/paint/puddles.ts b/src/paint/puddles.ts index d62d54c..8a33f1b 100644 --- a/src/paint/puddles.ts +++ b/src/paint/puddles.ts @@ -34,10 +34,13 @@ export interface PaintPuddleConfig { splatRadius?: number } -const DEFAULT_RATE = 6 +/** Stamps per world-unit TRAVELLED (distance-based; speed-independent coverage). */ +const DEFAULT_RATE = 4 const DEFAULT_SPLAT_RADIUS = 0.3 +/** Idle drip: parking in paint still coats, slowly (world-units-equivalent/s). */ +const IDLE_DRIP = 0.35 /** How close the belly must be to the strip plane to count as "rolling on it". */ -const CONTACT_TOLERANCE = 0.6 +const CONTACT_TOLERANCE = 1.0 // integration tune: landing-bounce hops starved strips at 0.6 export class PaintPuddle { readonly color: PaintColor @@ -98,6 +101,7 @@ export class PaintPuddle { */ step( dt: number, + moved: number, cx: number, cz: number, bellyY: number, @@ -111,7 +115,11 @@ export class PaintPuddle { this.acc = 0 return 0 } - this.acc += dt + // Distance-based accumulation (integration fix, 2026-07-18): a time-based + // cadence made coverage inversely proportional to speed — a racer at 9 u/s + // crossed a 7u strip with ~5 stamps while a slow test ball got dozens. + // Stamp per unit TRAVELLED, plus a small idle drip so parking still coats. + this.acc += moved + IDLE_DRIP * dt let laid = 0 while (this.acc >= this.period) { this.acc -= this.period @@ -163,6 +171,7 @@ export function installPuddles( const bellyY = t.y - r // advance the roll phase from horizontal displacement (roll w/o slipping) + let movedStep = 0 if (hasPrev) { const dx = t.x - prev.x const dz = t.z - prev.z @@ -170,6 +179,7 @@ export function installPuddles( if (moved > 1e-5) { dir.set(dx / moved, 0, dz / moved) phase += moved / r + movedStep = moved } } prev.set(t.x, t.y, t.z) @@ -184,7 +194,7 @@ export function installPuddles( } for (const p of puddles) { - p.step(dt, t.x, t.z, bellyY, phase, stampPoint, skin, world.events, scratch) + p.step(dt, movedStep, t.x, t.z, bellyY, phase, stampPoint, skin, world.events, scratch) } }, }) diff --git a/vite.config.ts b/vite.config.ts index cb8e376..5d55343 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ 'lane-b': resolve(__dirname, 'demos/lane-b.html'), 'lane-c': resolve(__dirname, 'demos/lane-c.html'), 'lane-f': resolve(__dirname, 'demos/lane-f.html'), + 'lane-g': resolve(__dirname, 'demos/lane-g.html'), 'lane-h': resolve(__dirname, 'demos/lane-h.html'), }, },