Implements THE WORLD CONTRACT (TECH.md) in web/js/world/; drop-in for the stub. boot.js now runs this on C's L2_esophagus (3600u, hash cefc4f83), zero console errors. - spline.js: centreline + arclength LUTs + parallel-transport frames + radius law + peristalsis phase + project() + hash(). No three.js import, so qa can run its 14-assertion selfcheck headless. - curviness is solved from a curvature budget, not authored as an amplitude: the pinch guard caught the tube folding through itself on a wide+curvy join (turn radius 9.9 vs tube radius 17.2). C can no longer author a pinch. - peristalsis: global OMEGA, k(s) = OMEGA/flow(s), phase K(s) = integral k ds. A crest therefore travels at exactly the local flow speed, so "surf the crest" == "ride the current". Esophagus wave amp 0.9 -> 1.4 (measured): moves wallRho for Lane B. - tube.js: chunked streaming, seams aligned to biome joins, arenas own their s-span. - arena.js: displaced icosphere shells v0 (three's polyhedron detail is 20*(detail+1)^2, not 20*4^detail — 720 tris was far too coarse); fog sized to the room, since biome fog tuned for 100u corridors renders C's 360u acid sea as a black rectangle. - index.js: prefers assets.texture() over get() (get returns raw JSON a shader can't eat), plus an explicit slug map — D's wall_smallint_a vs biome id small_intestine would miss silently forever under the assets-optional law. Measured (C's real L2 + D's real pack, 1920x1080, renderer.info): 8 draws worst frame (budget 150), 74k tris (500k), no geometry leak across a full sweep, <120ms load, pinch ratio 3.32. fps not claimed — the screenshot pane runs tabs hidden, pausing rAF. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
227 lines
9.8 KiB
HTML
227 lines
9.8 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>GUTS — Lane A world harness</title>
|
|
<!--
|
|
Lane A's own harness. NOT the game: js/boot.js is the game, F owns it.
|
|
|
|
This exists because boot.js can't reach js/world/index.js yet — pickWorld() imports Lane C's
|
|
js/levels/index.js in the same try block, and that module doesn't exist, so every non-stub
|
|
boot falls back to the stub regardless of my work landing. Patch offered in LANE_A_NOTES.md
|
|
§-> Lane F. Until it's applied, this is how the canal gets eyeballed. Delete it once boot can
|
|
show the real world.
|
|
|
|
http://localhost:8140/js/world/dev.html?dbg=1 rail cam down the fixture canal
|
|
...?dbg=1&fly=1 noclip (WASD/QE, drag to look)
|
|
...?dbg=1&s=430 park the rail cam at s=430
|
|
...?dbg=1&fakeassets=1 exercise the Lane D texture path
|
|
...?lvl=L2_esophagus C's level, when it lands
|
|
...?shots=1 hide the overlay
|
|
-->
|
|
<style>
|
|
html, body { margin: 0; height: 100%; overflow: hidden; background: #02100d; }
|
|
canvas { display: block; }
|
|
#dbg { position: fixed; left: 10px; bottom: 10px; color: #39e6ff;
|
|
font: 12px/1.5 ui-monospace, monospace; text-shadow: 0 0 4px #000;
|
|
pointer-events: none; white-space: pre; }
|
|
</style>
|
|
<script type="importmap">
|
|
{ "imports": { "three": "../../vendor/three.module.js",
|
|
"three/addons/": "../../vendor/addons/" } }
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="dbg"></div>
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { createRng } from '../core/rng.js';
|
|
import { createWorld } from './index.js';
|
|
import { createFlyCam } from './flycam.js';
|
|
import { FIXTURE } from './_fixture.js';
|
|
|
|
const p = new URLSearchParams(location.search);
|
|
const on = (k) => p.has(k) && p.get(k) !== '0';
|
|
const num = (k, d) => (p.has(k) ? parseFloat(p.get(k)) : d);
|
|
|
|
// --- level: C's if it's landed, fixture otherwise ---------------------------------------
|
|
let level = FIXTURE;
|
|
if (p.has('lvl')) {
|
|
try {
|
|
const mod = await import('../levels/index.js');
|
|
level = await mod.getLevel(p.get('lvl'));
|
|
} catch (e) {
|
|
console.warn('[devharness] no Lane C registry yet, using fixture —', e.message);
|
|
}
|
|
}
|
|
const seed = p.has('seed') ? (parseInt(p.get('seed'), 10) >>> 0) : level.seed;
|
|
|
|
// --- fake assets (?fakeassets=1) ---------------------------------------------------------
|
|
// Stands in for Lane D so the USE_DETAIL branch of the wall shader is exercised before their
|
|
// pack exists: a seamless grayscale fold pattern, same duck-type index.js expects from
|
|
// assets.get('textures', 'wall_<biome>_a'). Proves the integration, not the art.
|
|
function fakeAssets() {
|
|
const S = 256, cv = document.createElement('canvas');
|
|
cv.width = cv.height = S;
|
|
const ctx = cv.getContext('2d'), img = ctx.createImageData(S, S), r = createRng(seed)('assets.fake');
|
|
for (let y = 0; y < S; y++) for (let x = 0; x < S; x++) {
|
|
const u = x / S, v = y / S; // periodic in both axes => tiles cleanly
|
|
let g = 0.5 + 0.22 * Math.sin(Math.PI * 2 * 3 * u + 2 * Math.sin(Math.PI * 2 * 2 * v))
|
|
+ 0.16 * Math.sin(Math.PI * 2 * 5 * v)
|
|
+ 0.10 * Math.sin(Math.PI * 2 * 11 * u + 1.7);
|
|
g = Math.max(0, Math.min(1, g + (r() - 0.5) * 0.10));
|
|
const i = (y * S + x) * 4, b = (g * 255) | 0;
|
|
img.data[i] = img.data[i + 1] = img.data[i + 2] = b; img.data[i + 3] = 255;
|
|
}
|
|
ctx.putImageData(img, 0, 0);
|
|
const tex = new THREE.CanvasTexture(cv);
|
|
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
|
return { get: (kind, id) => (kind === 'textures' && /^wall_/.test(id) ? { isTexture: false, texture: tex, tile: [3, 14] } : null) };
|
|
}
|
|
|
|
// --- shell -------------------------------------------------------------------------------
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
document.body.appendChild(renderer.domElement);
|
|
const scene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);
|
|
|
|
const t0 = performance.now();
|
|
// ?assets=1 uses Lane D's real loader + pack; ?fakeassets=1 uses the synthetic stand-in above.
|
|
let assets = null;
|
|
if (on('fakeassets')) assets = fakeAssets();
|
|
else if (on('assets')) {
|
|
try {
|
|
const m = await import('../core/assets.js');
|
|
assets = await m.createAssets({ flags: { localassets: p.get('localassets') !== '0' } });
|
|
} catch (e) {
|
|
console.warn('[devharness] Lane D assets.js unavailable, procedural walls —', e.message);
|
|
}
|
|
}
|
|
|
|
const world = await createWorld(level, { rng: createRng(seed), quality: p.get('quality') || 'high', assets });
|
|
const loadMs = performance.now() - t0;
|
|
scene.add(world.group);
|
|
scene.background = new THREE.Color(world.biomeAt(0).palette.void);
|
|
|
|
const fly = on('fly') ? createFlyCam({ camera, dom: renderer.domElement, world }) : null;
|
|
let sCam = num('s', 5);
|
|
const railSpeed = num('speed', 1) * (p.has('s') ? 0 : 1); // ?s= parks the cam for clean shots
|
|
|
|
function rail(dt) {
|
|
sCam = (sCam + world.biomeAt(sCam).flow * 0.6 * railSpeed * dt) % world.length;
|
|
const f = world.sample(sCam), ahead = world.sample(Math.min(sCam + 12, world.length));
|
|
camera.position.copy(f.pos).addScaledVector(f.nor, -f.radius * 0.25);
|
|
camera.up.copy(f.nor);
|
|
camera.lookAt(ahead.pos);
|
|
}
|
|
|
|
addEventListener('resize', () => {
|
|
camera.aspect = innerWidth / innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
});
|
|
|
|
const dbgEl = document.getElementById('dbg');
|
|
if (!on('dbg') || on('shots')) dbgEl.style.display = 'none';
|
|
renderer.info.autoReset = false;
|
|
let frames = 0, fpsT = 0, fps = 0, draws = 0, tris = 0;
|
|
|
|
window.DBG = {
|
|
world, scene, camera, renderer,
|
|
get draws() { return draws; }, get tris() { return tris; }, get fps() { return fps; },
|
|
shot(name = 'shot') {
|
|
const a = document.createElement('a');
|
|
a.download = `${name}.png`;
|
|
a.href = renderer.domElement.toDataURL('image/png');
|
|
a.click();
|
|
},
|
|
/**
|
|
* Fix the drawing buffer to an explicit size for evidence frames. Two traps, both learned
|
|
* the hard way, both of which will bite anyone shooting boot.js (see LANE_A_NOTES §-> Lane F):
|
|
* 1. Screenshot tooling drives the tab HIDDEN. A hidden tab gets no requestAnimationFrame,
|
|
* so the render loop simply is not running when you shoot.
|
|
* 2. A hidden tab also reports innerWidth/innerHeight 0 — so the canvas is 0x0 and
|
|
* toDataURL() returns the literal string "data:,". The pane screenshot still looks fine
|
|
* because grabbing it fronts the tab; JS-side capture does not.
|
|
* Sizing explicitly also makes every committed shot the same resolution regardless of window.
|
|
*/
|
|
size(w = 1920, h = 1080) {
|
|
renderer.setPixelRatio(1);
|
|
renderer.setSize(w, h, false);
|
|
camera.aspect = w / h;
|
|
camera.updateProjectionMatrix();
|
|
return [renderer.domElement.width, renderer.domElement.height];
|
|
},
|
|
|
|
/** Deterministic evidence frame: park the rail cam at s, set the wave to exactly t, render. */
|
|
pose(s = 5, t = 0, w = 1920, h = 1080) {
|
|
this.size(w, h);
|
|
world.update(t - world.time, s); // land the wave phase exactly on t
|
|
for (let i = 0; i < 12; i++) world.update(0, s); // let the 2-chunks/update budget fill
|
|
sCam = s;
|
|
rail(0);
|
|
renderer.render(scene, camera);
|
|
const r = { s, t, draws: renderer.info.render.calls, tris: renderer.info.render.triangles,
|
|
pulse: world.flowPulse(s, t), ...world.stats() };
|
|
renderer.info.reset();
|
|
return r;
|
|
},
|
|
|
|
/** Park the noclip cam explicitly (arena interiors, wall close-ups), then render. */
|
|
poseAt(pos, lookAt, t = 0, w = 1920, h = 1080) {
|
|
this.size(w, h);
|
|
const p = new THREE.Vector3(...pos), l = new THREE.Vector3(...lookAt);
|
|
const s = world.project(p).s;
|
|
world.update(t - world.time, s);
|
|
for (let i = 0; i < 12; i++) world.update(0, s);
|
|
camera.position.copy(p);
|
|
camera.up.set(0, 1, 0);
|
|
camera.lookAt(l);
|
|
renderer.render(scene, camera);
|
|
const r = { s, draws: renderer.info.render.calls, tris: renderer.info.render.triangles };
|
|
renderer.info.reset();
|
|
return r;
|
|
},
|
|
|
|
/** Streaming leak probe: fly the whole canal, report renderer.info deltas. */
|
|
sweep(step = 20) {
|
|
const g0 = renderer.info.memory.geometries;
|
|
for (let s = 0; s <= world.length; s += step) world.update(0.016, s);
|
|
world.update(0.016, 0);
|
|
const g1 = renderer.info.memory.geometries;
|
|
return { geometriesBefore: g0, geometriesAfter: g1, delta: g1 - g0, ...world.stats() };
|
|
},
|
|
};
|
|
|
|
let last = performance.now();
|
|
function frame(now) {
|
|
const dt = Math.min((now - last) / 1000, 0.05);
|
|
last = now;
|
|
if (fly) fly.update(dt); else rail(dt);
|
|
const camS = fly ? world.project(camera.position).s : sCam;
|
|
world.update(dt, camS);
|
|
renderer.render(scene, camera);
|
|
draws = renderer.info.render.calls;
|
|
tris = renderer.info.render.triangles;
|
|
renderer.info.reset();
|
|
frames++; fpsT += dt;
|
|
if (fpsT >= 0.5) {
|
|
fps = Math.round(frames / fpsT); frames = 0; fpsT = 0;
|
|
if (on('dbg') && !on('shots')) {
|
|
const st = world.stats();
|
|
dbgEl.textContent =
|
|
`${fps} fps · ${draws} draws · ${(tris / 1000) | 0}k tris · load ${loadMs.toFixed(0)}ms\n` +
|
|
`${level.id} seed=${seed} hash=${world.hash()} L=${world.length}\n` +
|
|
`s=${camS.toFixed(0)} ${world.biomeAt(camS).id} ${world.modeAt(camS)} · flow ${world.biomeAt(camS).flow.toFixed(1)} · pulse ${world.flowPulse(camS).toFixed(2)}\n` +
|
|
`chunks ${st.chunks} live / ${st.built} built / ${st.disposed} disposed · turnR ${st.minTurnRadius.toFixed(0)} vs R ${st.maxRadius.toFixed(1)}`;
|
|
}
|
|
}
|
|
requestAnimationFrame(frame);
|
|
}
|
|
requestAnimationFrame(frame);
|
|
</script>
|
|
</body>
|
|
</html>
|