guts/web/dev/laneA_world.html
jing ecf43940ec [lane A] Round 2 close-out: colorspace re-eyeball, stomach shape read, ruling #5
Finishes the lane A round-2 list that f0982e7 (the rescued first half) left open:

- Biome-tint re-eyeball under the colorspace law (#4 second half). Verdict: all
  six ART_BIBLE tints stand, biomes.js untouched — the wash was wall_material's
  round-1 constants, tuned on the raw framebuffer. Retuned: rim pow 2.2->3.0,
  rim gain 0.9->0.35, textured curve (0.35+d*.95)*.9 -> 0.12+d*0.60, procedural
  d*.8 -> d^2*.65, sheen .10->.04, matcap .22->.14. Rationale documented
  in-shader. Verified on a new six-biome contact sheet (?lvl=biomes, SIX_BIOMES
  fixture — kept as permanent kit), the real L2 hiatus, and an arena interior.
  Evidence: round2_tint_*.png x6 + two *_retuned frames. qa GREEN.

- Stomach-arena shape read for C (#7): recommend the spline-swept room (arena =
  "open" mode over an s-span; fundus dome stays a welded icosphere; chained
  spheres rejected — crease rings + union collision for a worse look). Full
  argument NOTES §-> Lane C + round2_stomach_arena_sketch.svg. Triplanar
  deliberately deferred: the swept room obsoletes it where it mattered.

- Ruling #5: harness -> web/dev/laneA_world.html (+DBG.post to the house shot
  sink); _fixture.js stays — the spline selfcheck asserts against it, now
  documented in-file. launch.json: guts-a on 8145.

- NOTES + progress written for both halves; ROUND2 mid-round box updated: lane
  A's round-2 list is closed, only round-3 items remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:14:30 +10:00

243 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>GUTS — Lane A world harness</title>
<!--
Lane A's world harness (web/dev/ per ruling #5 — dev-only, per-file lane ownership, never
shipped). NOT the game: js/boot.js is the game, F owns it. Boot shows the real world now;
this survives because it is the world lane's *instrument*: deterministic pose/poseAt evidence
frames, the streaming-leak sweep, and the six-biome tint contact sheet — none of which belong
in boot.
http://localhost:8140/dev/laneA_world.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 synthetic texture stand-in
...?dbg=1&assets=1 Lane D's real pack
...?lvl=L2_esophagus C's level (?lvl=biomes = the
six-biome eyeball canal)
...?shots=1 hide the overlay
Evidence flow: DBG.pose(s, t) then DBG.post('name') — POSTs the canvas to D's shot_sink
(pipeline/shot_sink.py, house tool per ruling #5). DBG.shot()'s download no-ops in the pane.
-->
<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 '../js/core/rng.js';
import { createWorld } from '../js/world/index.js';
import { createFlyCam } from '../js/world/flycam.js';
import { FIXTURE, SIX_BIOMES } from '../js/world/_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.get('lvl') === 'biomes') {
level = SIX_BIOMES; // the tint contact sheet — one segment per registry biome
} else if (p.has('lvl')) {
try {
const mod = await import('../js/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('../js/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();
},
/** POST the current canvas to D's shot_sink (the house evidence tool — ruling #5).
* Run pose()/poseAt() first; the sink names and lands the file, no download dance. */
post(name, port = 8144) {
return new Promise((res, rej) => renderer.domElement.toBlob((b) => {
if (!b || b.size < 8) return rej(new Error('empty canvas — did you pose() first?'));
fetch(`http://localhost:${port}/shot/${name}.png`, { method: 'POST', body: b })
.then((r) => (r.ok ? res(`${name}.png ${(b.size / 1024) | 0} KB`) : rej(new Error(`sink ${r.status}`))))
.catch(rej);
}, 'image/png'));
},
/**
* 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>