TURNCRAFT/src/engine/HANDOFF.md
jing 5a39e3a947 TURNCRAFT: contracts, docs, and all five lane deliverables (pre-integration)
Lanes A (engine), B (player), C (worldgen), D (machines/quest),
E (audio/fx/ui) as landed, each with HANDOFF.md + update docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:50:56 +10:00

6.1 KiB
Raw Blame History

Lane A — Voxel Engine & Rendering — HANDOFF

Status: complete. All of A1A5 implemented, browser-verified, and typechecking clean in isolation. No stubs.

Public API (10 lines)

import { createRenderer, VoxelWorld, ChunkManager, buildAtlas, meshChunk } from './engine';
const { renderer, scene, camera, atlas, setEmissiveBoost, render, resize, dispose } = createRenderer(container);
const world = new VoxelWorld(WORLD_X, WORLD_Y, WORLD_Z);   // implements IVoxelWorld
world.fillBox(x0,y0,z0,x1,y1,z1,id);                        // bulk paint (worldgen)
world.setBlock(x,y,z,id); world.getBlock(x,y,z); world.isSolid(x,y,z);
const chunks = new ChunkManager(world, scene, atlas, /*remeshBudget*/ 4);
chunks.buildAll();        // once, after worldgen fills the world (< 3 s; ~25 ms here)
chunks.update();          // once per frame — drains dirty chunks, budgeted
setEmissiveBoost(v);      // Lane E pulses LED glow 0..3 (default 1)

atlas also carries opaqueMaterial / transparentMaterial (ChunkManager uses them). meshChunk(world,cx,cy,cz) is exported for anyone who wants raw geometry.

What's done

  • VoxelWorld — 32³ Uint8Array chunks, lazily allocated; oob reads AIR for y≥0 and a solid sentinel for y<0; setBlock marks the chunk + any bordering neighbour dirty; fillBox writes raw with a chunk-resolve cache and dirties the touched range +1; readChunkPadded gives the mesher a 1-voxel-padded id buffer (fast interior copy, getBlock only on the border shell). Dirty API: forEachDirtyChunk/clearDirty/dirtyCount.
  • Atlas — procedural 8×4 grid of 16×16 tiles, one per block id, all 10 patterns painted deterministically; a parallel emissive atlas (black except LED/strobe). Two MeshStandardMaterials (opaque, transparent) with an onBeforeCompile patch that fracts the per-quad tile-repeat UV into the block's atlas cell. NearestFilter, no mips, no padding needed (fract keeps the sample strictly inside a cell → no bleed).
  • Greedy mesher — per-chunk, 6 face directions, cells keyed by (blockId, 4×AO), coplanar equal cells merged into one quad; classic 3-neighbour AO baked to vertex colour; quad diagonal flips on AO anisotropy; correct outward winding per direction; opaque and transparent faces split into separate geometries; absolute world coords baked in (bounding sphere → Three.js frustum culling per chunk).
  • ChunkManagerbuildAll() meshes every non-empty chunk once; update() drains the dirty set at remeshBudget chunks/frame, disposing+swapping geometry in place; removes emptied meshes; meshCount/lastRemeshMs/lastRemeshCount for HUDs.
  • Renderer — WebGLRenderer (AA off, pixelRatio ≤2), sRGB output, ACES tone mapping, 75° camera, warm key DirectionalLight + cool HemisphereLight + small ambient, warm plywood fog, near-black warm background; self-wiring ResizeObserver; the single setEmissiveBoost hook Lane E drives.

Verified in demo-engine.html (all acceptance boxes)

Full 448×160×256 world + test pattern: build ~25 ms, 4647 draw calls, ~1.5 k tris, 86128 fps. AO visibly darkens the steel cave's inner corners with no chunk-seam cracks. Glass arch is see-through onto the geometry behind it; blue vinyl blends over the cream wall. All 31 block ids render with distinct patterns (labeled pillar row); LEDs glow and the emissive slider scales them 0→3. Torture (500 edits/s): min fps 128, dirty set drains, edits appear within ~1 frame.

Post-build adversarial review (done)

A 5-dimension correctness review + programmatic verification caught and fixed two bugs:

  • HIGH (fixed): Z-axis winding table (AXIS[2]) had flip flags swapped → every opaque block's +Z/Z faces were wound backwards and back-face-culled. Verified via a per-triangle geometric-vs- stored-normal check: 520 Z tris inverted before, 0 mismatches after (all six normals, opaque and transparent). If you ever touch the mesher, re-run that check (see LANE_A_update.md).
  • LOW (fixed): renderer.ts resize fallback used ?? where clientWidth returns 0 (not nullish); switched to || so a 0-width container falls back to the window.

Contract friction / notes for the integrator

  1. Repo-wide npm run typecheck currently FAILS — but not on Lane A code. The only error is in Lane C's src/worldgen/anchors.ts (a LAYOUT.deckB assigned where a deckA-typed value is expected). Lane A compiles clean on its own; verified with an isolated tsconfig over src/core + src/engine + src/demo/engineDemo.ts. Not my file to touch — flagging for whoever owns worldgen / integration.
  2. Dev-only console warning "Multiple instances of Three.js" appears on the demo page because OrbitControls (demo convenience only, not in the shipped engine API) pulls three through a second Vite resolution. Harmless; it will not appear in the integrated game (no OrbitControls there). If desired, add resolve.dedupe: ['three'] to vite.config — but that's a shared config file, so I left it to the integrator.
  3. Draw-call budget at full booth scale. The demo (empty-ish world) is ~47 calls, far under 300. The dense authored booth could push non-empty-chunks × 2 (opaque+transparent) upward; if it ever nears 300, the cheap wins are merging tiny transparent geometries or worker-threaded meshing. No web workers in v1 (per brief); meshing is fully synchronous and comfortably inside the per-frame budget so far.
  4. Wiring reminders (match docs/INTEGRATION.md): call chunks.buildAll() once after buildBooth(world), then chunks.update() every frame in the loop. createRenderer builds the atlas for you and exposes it as .atlas; hand that same object to ChunkManager so setEmissiveBoost and the meshes share one material set. Do not call buildAtlas() a second time — you'd get materials setEmissiveBoost can't reach.

Owned paths (no writes outside these)

src/engine/**, src/demo/engineDemo.ts, demo-engine.html. Core untouched. src/demo/engineDemo.ts exposes a tiny dev-only window.__demo.look() camera hook, clearly commented — used for inspection, harmless in normal play.