Fixed-step world runtime, event bus wire protocol between lanes, integration seam (game.ts) with placeholder ball. Lanes A/B/C own disjoint dirs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
57 lines
2.0 KiB
TypeScript
57 lines
2.0 KiB
TypeScript
/**
|
|
* INTEGRATION SEAM — owned by integration, not by lanes.
|
|
* Composes lane modules into the playable slice. While lanes are in flight this
|
|
* boots a placeholder ball on a flat floor so `npm run dev` always shows life.
|
|
*/
|
|
import * as THREE from 'three'
|
|
import type { World } from './contracts'
|
|
|
|
export function installGame(world: World) {
|
|
const { scene, physics, rapier } = world
|
|
|
|
// floor
|
|
const floor = new THREE.Mesh(
|
|
new THREE.BoxGeometry(60, 1, 60),
|
|
new THREE.MeshStandardMaterial({ color: '#e8d9b8' }))
|
|
floor.position.y = -0.5
|
|
floor.receiveShadow = true
|
|
scene.add(floor)
|
|
physics.createCollider(rapier.ColliderDesc.cuboid(30, 0.5, 30)
|
|
.setTranslation(0, -0.5, 0))
|
|
|
|
// placeholder blob-ball (replaced by lane A's createBlob at integration)
|
|
const ball = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.6, 32, 24),
|
|
new THREE.MeshStandardMaterial({ color: '#F5F5F7', roughness: 0.35 }))
|
|
ball.castShadow = true
|
|
scene.add(ball)
|
|
const body = physics.createRigidBody(
|
|
rapier.RigidBodyDesc.dynamic().setTranslation(0, 3, 0))
|
|
physics.createCollider(
|
|
rapier.ColliderDesc.ball(0.6).setRestitution(0.4).setFriction(1.0), body)
|
|
|
|
const keys = new Set<string>()
|
|
addEventListener('keydown', (e) => keys.add(e.code))
|
|
addEventListener('keyup', (e) => keys.delete(e.code))
|
|
|
|
world.addSystem({
|
|
update() {
|
|
const f = 18
|
|
const impulse = { x: 0, y: 0, z: 0 }
|
|
if (keys.has('KeyW') || keys.has('ArrowUp')) impulse.z -= f
|
|
if (keys.has('KeyS') || keys.has('ArrowDown')) impulse.z += f
|
|
if (keys.has('KeyA') || keys.has('ArrowLeft')) impulse.x -= f
|
|
if (keys.has('KeyD') || keys.has('ArrowRight')) impulse.x += f
|
|
body.applyImpulse({ x: impulse.x / 60, y: 0, z: impulse.z / 60 }, true)
|
|
const p = body.translation()
|
|
ball.position.set(p.x, p.y, p.z)
|
|
const q = body.rotation()
|
|
ball.quaternion.set(q.x, q.y, q.z, q.w)
|
|
},
|
|
})
|
|
|
|
world.onFrame(() => {
|
|
world.camera.lookAt(ball.position)
|
|
})
|
|
}
|