Wave 2 integration: zones staged, fork machines own the district, tuning pass
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
8875663c18
commit
0f2aca542b
@ -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).
|
||||
|
||||
83
src/game.ts
83
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;
|
||||
|
||||
@ -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)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@ -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'),
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user