// TURNCRAFT — Lane A standalone demo. Run: `npm run dev` → open /demo-engine.html // // WHAT TO LOOK AT / PRESS (maps to the acceptance criteria in LANE_A_ENGINE.md): // • Full-size (448×160×256) empty world + a test pattern near the origin renders // at 60 fps with few draw calls — see the HUD (top-left): FPS, draw calls, // triangles, meshes. Draw calls stay well under 300. // • AO: orbit into the hollow steel CAVE (left) — inner corners visibly darken; // no cracks or z-fighting where chunks meet. // • Transparency: the GLASS ARCH and the BLUE VINYL wall (in front of a cream // wall) render in a correct transparent pass over the opaque blocks behind. // • All 10 patterns + every block id: the labeled PILLAR ROW — one stepped // pillar per block id, each with a floating "id name" label. // • Emissive: the LED WALL. Drag the "emissive boost" slider (0→3) — LED/strobe // glow scales; nothing else changes. // • Dirty remeshing: press "TORTURE" — ~500 random block edits/sec in the flicker // cube; FPS stays ≳55 and edits appear within ~1–2 frames. // // Uses ONLY Lane A code + core. OrbitControls is a demo convenience (not shipped // in the engine API). import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { WORLD_X, WORLD_Y, WORLD_Z } from '../core/constants'; import { BLOCKS, BLOCK_BY_NAME } from '../core/blocks'; import { createRenderer } from '../engine/renderer'; import { VoxelWorld } from '../engine/VoxelWorld'; import { ChunkManager } from '../engine/ChunkManager'; const B = (name: string) => BLOCK_BY_NAME.get(name)!.id; const app = document.getElementById('app')!; const { renderer, scene, camera, atlas, setEmissiveBoost, setBloom, render } = createRenderer(app); let hud: HTMLDivElement; // populated by buildHud(), read by updateHud() const world = new VoxelWorld(WORLD_X, WORLD_Y, WORLD_Z); buildTestPattern(world); const chunks = new ChunkManager(world, scene, atlas, 4); const buildT0 = performance.now(); chunks.buildAll(); const buildMs = performance.now() - buildT0; // Camera / controls framed on the test pattern. camera.position.set(34, 46, 118); const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(33, 8, 33); controls.enableDamping = true; controls.dampingFactor = 0.08; controls.update(); // DEBUG: dev-only hook so a camera can be flown to a viewpoint for inspection. (window as unknown as { __demo?: unknown }).__demo = { scene, atlas, camera, controls, setBloom, setEmissiveBoost, look(px: number, py: number, pz: number, tx: number, ty: number, tz: number) { camera.position.set(px, py, pz); controls.target.set(tx, ty, tz); controls.update(); render(); }, render, }; // Floating labels over each pillar. addPillarLabels(scene); buildHud(); // --- main loop --- let last = performance.now(); let fps = 60, minFpsTorture = 999; let torturing = false; function frame() { const t = performance.now(); const dt = Math.min(0.1, (t - last) / 1000); last = t; fps = fps * 0.9 + (1 / Math.max(1e-3, dt)) * 0.1; if (torturing) { torture(dt); if (fps < minFpsTorture) minFpsTorture = fps; } controls.update(); chunks.update(); render(); updateHud(dt); requestAnimationFrame(frame); } requestAnimationFrame(frame); // --------------------------------------------------------------------------- // Test pattern // --------------------------------------------------------------------------- function buildTestPattern(w: VoxelWorld): void { // 64×64 plywood floor at y=0. w.fillBox(2, 0, 2, 65, 0, 65, B('plywood')); // Labeled pillar row: one stepped pillar per block id (skip air id 0). for (const def of BLOCKS) { if (def.id === 0) continue; const x = 3 + def.id * 2; const h = 2 + (def.id % 7); w.fillBox(x, 1, 6, x, h, 6, def.id); } // Glass arch (transparent portal) — see through it to the pillars/floor. w.fillBox(10, 1, 40, 11, 8, 41, B('glass')); w.fillBox(20, 1, 40, 21, 8, 41, B('glass')); w.fillBox(10, 8, 40, 21, 9, 41, B('glass')); // Blue translucent vinyl wall in front of a cream label wall. w.fillBox(30, 1, 46, 40, 6, 46, B('label_cream')); w.fillBox(30, 1, 44, 40, 6, 44, B('vinyl_blue')); // LED wall — mixed colours + strobe dots (emissive slider test). const leds = [B('led_red'), B('led_green'), B('led_amber'), B('led_blue')]; for (let x = 48; x <= 58; x++) for (let y = 1; y <= 8; y++) { const id = (x + y) % 5 === 0 ? B('strobe_dot') : leds[x % 4]; w.setBlock(x, y, 40, id); } // Hollow steel cave — AO test on interior corners, with an entrance. w.fillBox(2, 1, 50, 20, 12, 64, B('steel_grey')); w.fillBox(4, 2, 52, 18, 11, 63, 0); // hollow it w.fillBox(9, 1, 50, 13, 6, 50, 0); // doorway on the front face } // --------------------------------------------------------------------------- // Torture: ~500 random edits/sec in a flicker cube (dirty-remesh stress). // --------------------------------------------------------------------------- const TZ = { x0: 4, x1: 28, y0: 1, y1: 24, z0: 20, z1: 44 }; const TORTURE_BLOCKS = [0, B('steel_grey'), B('vinyl_blue'), B('led_green'), B('brushed_alu')]; let editCarry = 0; function torture(dt: number): void { editCarry += 500 * dt; const n = editCarry | 0; editCarry -= n; for (let i = 0; i < n; i++) { const x = TZ.x0 + ((Math.random() * (TZ.x1 - TZ.x0 + 1)) | 0); const y = TZ.y0 + ((Math.random() * (TZ.y1 - TZ.y0 + 1)) | 0); const z = TZ.z0 + ((Math.random() * (TZ.z1 - TZ.z0 + 1)) | 0); world.setBlock(x, y, z, TORTURE_BLOCKS[(Math.random() * TORTURE_BLOCKS.length) | 0]); } } // --------------------------------------------------------------------------- // Labels + HUD // --------------------------------------------------------------------------- function addPillarLabels(sc: THREE.Scene): void { for (const def of BLOCKS) { if (def.id === 0) continue; const x = 3 + def.id * 2; const h = 2 + (def.id % 7); const sprite = makeTextSprite(`${def.id} ${def.name}`); sprite.position.set(x + 0.5, h + 2.2, 6.5); sc.add(sprite); } } function makeTextSprite(text: string): THREE.Sprite { const pad = 6, fs = 22; const c = document.createElement('canvas'); const ctx = c.getContext('2d')!; ctx.font = `${fs}px monospace`; const w = Math.ceil(ctx.measureText(text).width) + pad * 2; const hgt = fs + pad * 2; c.width = w; c.height = hgt; ctx.font = `${fs}px monospace`; ctx.fillStyle = 'rgba(10,10,12,0.72)'; ctx.fillRect(0, 0, w, hgt); ctx.fillStyle = '#e8e6df'; ctx.textBaseline = 'middle'; ctx.fillText(text, pad, hgt / 2); const tex = new THREE.CanvasTexture(c); tex.colorSpace = THREE.SRGBColorSpace; const mat = new THREE.SpriteMaterial({ map: tex, depthTest: true, transparent: true }); const s = new THREE.Sprite(mat); s.scale.set((w / hgt) * 2.2, 2.2, 1); return s; } function buildHud(): void { hud = document.createElement('div'); hud.style.cssText = 'position:fixed;top:8px;left:8px;font:12px/1.5 monospace;color:#d8d6cf;' + 'background:rgba(10,9,8,0.72);padding:10px 12px;border-radius:6px;white-space:pre;' + 'user-select:none;pointer-events:none;z-index:10'; document.body.appendChild(hud); const panel = document.createElement('div'); panel.style.cssText = 'position:fixed;top:8px;right:8px;font:12px monospace;color:#d8d6cf;' + 'background:rgba(10,9,8,0.8);padding:10px 12px;border-radius:6px;z-index:10;' + 'display:flex;flex-direction:column;gap:8px;align-items:stretch'; const btn = document.createElement('button'); btn.textContent = 'TORTURE: off'; btn.style.cssText = 'font:12px monospace;padding:6px 10px;cursor:pointer'; btn.onclick = () => { torturing = !torturing; if (torturing) minFpsTorture = 999; btn.textContent = torturing ? 'TORTURE: ON (500 edits/s)' : 'TORTURE: off'; }; const label = document.createElement('label'); label.style.cssText = 'display:flex;flex-direction:column;gap:2px'; label.textContent = 'emissive boost'; const slider = document.createElement('input'); slider.type = 'range'; slider.min = '0'; slider.max = '3'; slider.step = '0.05'; slider.value = '1'; const val = document.createElement('span'); val.textContent = '1.00'; slider.oninput = () => { setEmissiveBoost(+slider.value); val.textContent = (+slider.value).toFixed(2); }; label.appendChild(slider); label.appendChild(val); panel.appendChild(btn); panel.appendChild(label); document.body.appendChild(panel); } let hudAccum = 0; function updateHud(dt: number): void { hudAccum += dt; if (hudAccum < 0.2) return; // refresh ~5×/s hudAccum = 0; const calls = renderer.info.render.calls; const tris = renderer.info.render.triangles; hud.textContent = `TURNCRAFT — Lane A engine demo\n` + `world ${WORLD_X}×${WORLD_Y}×${WORLD_Z} (initial build ${buildMs.toFixed(0)} ms)\n` + `fps ${fps.toFixed(0)}${torturing ? ` (min under torture ${minFpsTorture.toFixed(0)})` : ''}\n` + `draw calls ${calls}\n` + `triangles ${tris.toLocaleString()}\n` + `meshes ${chunks.meshCount}\n` + `dirty ${world.dirtyCount} remesh ${chunks.lastRemeshMs.toFixed(2)} ms / ${chunks.lastRemeshCount} chunk(s)`; }