BLOBBO/src/demo/lane-a.ts
type-two 4b08ba65ac Lane A: blob controller, feel layer, follow camera, greybox
- src/blob/createBlob.ts: rotation-locked dynamic ball + clean UV-sphere body
  mesh (untouched UVs for Lane B) + cosmetic eyes; implements frozen Blob.
- src/blob/controller.ts: fixed-step System. Camera-relative WASD via impulses,
  buffered+coyote jump, grounded raycast, emits blob:jumped/blob:landed. Reads
  blob.modifiers every frame: speedMul, jumpMul, massMul, grip, size.
- src/blob/feel.ts: the comedy engine. Underdamped springs for landing squash,
  jump stretch, inertial jelly lean, idle breathing + run bob; base-pivoted so
  squash plants on the floor. Driven by the frozen wire events. glow -> emissive.
- src/blob/camera.ts: soft-follow third-person cam with position lag, yaw ease
  behind travel, and FOV kick at speed.
- src/course/greybox.ts: Breakfast Rush greybox — start plateau, ramp, gap jump,
  slope, translucent milk river (surface:water), machine drop area, finish pad.
- src/demo/lane-a.ts: boots world+greybox+blob, HUD + once/sec state log, debug
  keys 1/2/3 (speed/jump/size) plus 4/5/6 (mass/grip/glow), 0 reset, R respawn.

npm run build passes (tsc strict + vite). Owned paths only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:11:55 +10:00

122 lines
4.4 KiB
TypeScript

/**
* Lane A demo — the playground.
*
* Boots the shared world, builds the greybox, spawns a blob, and wires the
* controller (physics), feel (wobble, via the frozen events) and follow camera.
*
* CONTROLS
* WASD / Arrows move (camera-relative)
* Space jump
*
* DEBUG KEYS (toggle — press again to reset to base). Proves modifier plumbing:
* 1 speedMul 1.6 2 jumpMul 1.5 3 size 1.5
* 4 massMul 3.0 5 grip 1.0 6 glow 1.0 (super-state pulse)
* 0 reset all modifiers R respawn at start
*
* A HUD (top-left) and a once-per-second console "state" line report live values
* so integration can verify fast without a screenshot.
*/
import { createWorld } from '../world'
import { defaultModifiers } from '../contracts'
import { buildGreybox } from '../course/greybox'
import { createBlob } from '../blob/createBlob'
import { createBlobController } from '../blob/controller'
import { createBlobFeel } from '../blob/feel'
import { createFollowCamera } from '../blob/camera'
const world = await createWorld(document.getElementById('app')!)
const course = buildGreybox(world)
const blob = createBlob(world, { position: course.spawn })
world.blob = blob
// physics system (fixed 60Hz)
world.addSystem(createBlobController(world, blob))
// feel layer — driven by the FROZEN wire events, updated per render frame
const feel = createBlobFeel(blob)
world.events.on('blob:jumped', () => feel.onJump())
world.events.on('blob:landed', (p: { impact: number }) => feel.onLand(p.impact))
world.onFrame((dt) => feel.update(dt))
// follow camera
const camera = createFollowCamera(world, blob)
world.onFrame((dt) => camera.update(dt))
// ---- event counters + last-impact (for HUD/console) ----
let jumps = 0
let lands = 0
let lastImpact = 0
world.events.on('blob:jumped', () => { jumps++ })
world.events.on('blob:landed', (p: { impact: number }) => { lands++; lastImpact = p.impact })
// ---- respawn if you fall off the world ----
world.onFrame(() => {
const t = blob.body.translation()
if (t.y < -25) respawn()
})
function respawn(): void {
blob.body.setTranslation({ x: course.spawn.x, y: course.spawn.y, z: course.spawn.z }, true)
blob.body.setLinvel({ x: 0, y: 0, z: 0 }, true)
}
// ---- debug keys ----
const m = blob.modifiers
const toggle = (cur: number, on: number, base: number) => (cur === base ? on : base)
addEventListener('keydown', (e) => {
switch (e.code) {
case 'Digit1': m.speedMul = toggle(m.speedMul, 1.6, 1); break
case 'Digit2': m.jumpMul = toggle(m.jumpMul, 1.5, 1); break
case 'Digit3': m.size = toggle(m.size, 1.5, 1); break
case 'Digit4': m.massMul = toggle(m.massMul, 3.0, 1); break
case 'Digit5': m.grip = toggle(m.grip, 1.0, 0); break
case 'Digit6': m.glow = toggle(m.glow, 1.0, 0); break
case 'Digit0': Object.assign(m, defaultModifiers()); break
case 'KeyR': respawn(); break
default: return
}
console.log('[lane-a] modifiers', JSON.stringify(m))
})
// ---- HUD overlay ----
const hud = document.createElement('div')
hud.style.cssText =
'position:fixed;top:10px;left:10px;font:12px/1.5 ui-monospace,Menlo,monospace;' +
'color:#0a2540;background:rgba(255,255,255,.72);padding:10px 12px;border-radius:8px;' +
'white-space:pre;pointer-events:none;z-index:10'
document.body.appendChild(hud)
// ---- per-frame HUD + fps + once/sec console state line ----
let frames = 0
let fps = 0
let acc = 0
let logAcc = 0
world.onFrame((dt) => {
frames++
acc += dt
if (acc >= 0.5) { fps = Math.round(frames / acc); frames = 0; acc = 0 }
const t = blob.body.translation()
const v = blob.body.linvel()
const speed = Math.hypot(v.x, v.z)
hud.textContent =
`BLOBBO · lane-a ${fps} fps\n` +
`pos ${t.x.toFixed(1)} ${t.y.toFixed(1)} ${t.z.toFixed(1)}\n` +
`spd ${speed.toFixed(2)} jumps ${jumps} lands ${lands} impact ${lastImpact.toFixed(1)}\n` +
`mods speed ${m.speedMul} jump ${m.jumpMul} size ${m.size} mass ${m.massMul} grip ${m.grip} glow ${m.glow}\n` +
`WASD move · Space jump · 1 speed 2 jump 3 size 4 mass 5 grip 6 glow · 0 reset · R respawn`
logAcc += dt
if (logAcc >= 1) {
logAcc = 0
console.log(
`[lane-a] state fps=${fps} pos=(${t.x.toFixed(1)},${t.y.toFixed(1)},${t.z.toFixed(1)}) ` +
`spd=${speed.toFixed(2)} jumps=${jumps} lands=${lands} impact=${lastImpact.toFixed(1)} ` +
`mods=${JSON.stringify(m)}`,
)
}
})
world.start()
console.log('[lane-a] booted. WASD to move, Space to jump, keys 1-6 modifiers, 0 reset, R respawn.')