- 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>
122 lines
4.4 KiB
TypeScript
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.')
|