/** * Lane I demo — every slot, twice. * * Left column is built with an EMPTY registry (today's game). Right column is * built with a test manifest pointing at the four farm GLBs. Same factories, * same arguments — the only difference is which registry is installed when the * factory runs, which is exactly the property the asset runtime promises. * * The paint check is deliberately numeric rather than visual: it stamps a splat * at a known point on the swapped body and prints coverage before/after, so * "the paint landed" is a number you can read, not a thing you squint at. * * NOTE: this page is not in vite.config.ts's `rollupOptions.input` (frozen * file), so it is served by `npm run dev` but is NOT emitted by `npm run build` * until integration adds the entry. */ import * as THREE from 'three' import { createWorld } from '../world' import type { World } from '../contracts' import { PaintSkin } from '../paint/skin' import { AssetRegistry, setAssets, paintableInfo } from '../assets/registry' import { resolveBlobBody } from '../assets/blobBody' import { assetUrl, type Manifest } from '../assets/manifest' import { SLOT_LABELS, type SlotId } from '../assets/slots' import { createPressurePlate, createSpringBoot, createBucketDump, createConveyorBelt, createBubbleArch, createFan, } from '../machine/parts' const FARM = (name: string) => assetUrl(`assets/meshes/${name}.glb`) /** * Fit transforms measured against the farm GLBs' own scale (they export large * and Z-up-ish); committed here so the demo is a real fitting example, not a * pile of identity transforms. */ const TEST_MANIFEST: Manifest = { 'machine.boot': { url: FARM('prop-spring-boot'), scale: 1.2, offset: [0, 0, 0] }, 'machine.bucket': { url: FARM('prop-paint-bucket'), scale: 1.0, offset: [0, -0.6, 0] }, 'course.scenery.cereal': { url: FARM('prop-toaster-launcher'), scale: 3.0, rotationDeg: [0, 180, 0] }, 'blob.body': { url: FARM('blobbo-base') }, } const log: string[] = [] const panel = document.createElement('div') panel.style.cssText = 'position:fixed;left:8px;top:8px;max-height:96vh;overflow:auto;width:400px;' + 'font:12px/1.5 ui-monospace,monospace;background:#0b0b12ee;color:#dfe;' + 'padding:10px 12px;border-radius:8px;white-space:pre-wrap;z-index:10' document.body.appendChild(panel) function say(line: string): void { log.push(line) panel.textContent = log.join('\n') console.log('[lane-i] ' + line) } /** Builds the whole slot line-up at an X offset. Identical calls both times. */ function buildColumn(world: World, x: number): void { createPressurePlate(world, { id: `plate${x}`, position: [x, 0.2, 6], massThreshold: 1, emits: 'sig' }) createSpringBoot(world, { id: `boot${x}`, position: [x, 0.2, 2], impulse: 8, onSignal: 'sig' }) createBucketDump(world, { id: `bucket${x}`, position: [x, 4, -2], color: 'blue', radius: 1.4, onSignal: 'sig' }) createConveyorBelt(world, { id: `belt${x}`, position: [x, 0.3, -8], velocity: [2, 0, 0], size: [8, 0.5, 3] }) createBubbleArch(world, { id: `arch${x}`, position: [x, 0.2, -14], fraction: 0.5 }) createFan(world, { id: `fan${x}`, position: [x, 1.5, -20], force: 4, direction: [0, 0, 1] }) } const world = await createWorld(document.getElementById('app')!) world.camera.position.set(0, 9, 22) world.camera.lookAt(0, 1, -6) // ---- LEFT: empty manifest = today's game ------------------------------------ const emptyReg = new AssetRegistry({}) setAssets(emptyReg) buildColumn(world, -9) const stockBody = resolveBlobBody(world, 0.5, () => makeStockSphere()) stockBody.position.set(-9, 2, 12) world.scene.add(stockBody) say('LEFT (x=-9): empty manifest — procedural, unchanged.') // ---- RIGHT: test manifest --------------------------------------------------- const testReg = new AssetRegistry(TEST_MANIFEST) say('\nRIGHT (x=+9): loading test manifest…') for (const [slot, entry] of Object.entries(TEST_MANIFEST) as [SlotId, { url: string }][]) { say(` ${SLOT_LABELS[slot]} <- ${entry.url.split('/').pop()}`) } await testReg.preload() setAssets(testReg) buildColumn(world, 9) const customBody = resolveBlobBody(world, 0.5, () => makeStockSphere()) customBody.position.set(9, 2, 12) world.scene.add(customBody) // ---- per-slot status -------------------------------------------------------- say('\n--- slot status ---') for (const slot of Object.keys(TEST_MANIFEST) as SlotId[]) { const inst = testReg.instanceSync(slot) say(` ${slot.padEnd(24)} ${inst ? 'LOADED' : 'FAILED -> fallback kept'}`) } // ---- paintability report for blob.body -------------------------------------- say('\n--- blob.body paintability ---') const info = testReg.paintability('blob.body') if (!info) { say(' no asset resolved; the built-in sphere is in use.') } else { say(` meshes ${info.meshCount} materials ${info.materialCount} triangles ${info.triCount}`) say(` UVs inside 0..1: ${info.uvOk ? 'yes' : 'NO'}${info.skinned ? ' (RIGGED — rejected)' : ''}`) say(` UV islands: ${info.uvCharts < 0 ? 'n/a' : info.uvCharts} seam vertices: ${Math.round(info.seamRatio * 100)}%`) say(` paint-safe: ${info.ok ? 'YES' : 'NO'}`) for (const p of info.problems) say(` ! ${p}`) } const swapped = customBody !== stockBody && customBody.geometry !== stockBody.geometry say(` body actually swapped: ${swapped ? 'YES' : 'no — using the built-in sphere'}`) say(` bounding radius: ${customBody.geometry.boundingSphere?.radius.toFixed(4)} (must be 0.5000 — it is a gameplay number)`) // ---- scripted splat: does paint land on whatever body is in use? ------------ say('\n--- scripted splat ---') for (const [label, mesh] of [['stock', stockBody], ['custom', customBody]] as const) { const skin = new PaintSkin(mesh, { size: 256 }) const before = skin.coverage().total // A point on the +X face of the body, in world space. const p = mesh.getWorldPosition(new THREE.Vector3()).add(new THREE.Vector3(0.5, 0, 0)) skin.splatAtPoint(p, 'red', 0.25) const after = skin.coverage() say(` ${label}: coverage ${before.toFixed(4)} -> ${after.total.toFixed(4)} ` + `red=${after.byColor.red.toFixed(4)} ${after.total > before ? 'PAINT LANDED' : 'NOTHING LANDED'}`) } say('\nSlots with no entry above fall back silently — that is the empty-manifest path.') world.start() function makeStockSphere(): THREE.Mesh { const m = new THREE.Mesh( new THREE.SphereGeometry(0.5, 48, 36), new THREE.MeshStandardMaterial({ color: '#F5F5F7', roughness: 0.5, emissive: new THREE.Color('#88e0ff'), emissiveIntensity: 0 }), ) m.castShadow = true return m }