/** * 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() 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) }) }