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>
243 lines
11 KiB
HTML
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>
|