The shape read in LANE_A_NOTES was written from reading code, so I built it to
find out if it was true. Flyable: web/dev/laneA_world.html?dbg=1&assets=1&lvl=swept
Nothing here is imposed on anyone: every addition is additive and a level that
opts out behaves exactly as today. C can bin the whole proposal and lose nothing.
WHAT THE PROTOTYPE DISPROVED (my own cost estimates, all four):
- "radial resolution must scale with radius" — wrong. 64 segments is a 0.05u
circle error at r=70. The arena needs it because it fbm-displaces; the tube's
wave is smooth in theta. No change made.
- "cap geometry at the span ends" — a thinko. The canal is continuous; a room is
a fat part of it. Only the fundus dome caps, and it stays an icosphere.
- cost — cheaper than feared: 9 draws / 98k tris worst-frame for a radius-70
cathedral vs 8 / 74k for the L2 corridor. No leak over a full sweep.
- "one schema flag" — held. `segments[].mode: "open"` is the entire ask.
THE TWO REAL COSTS, NEITHER PREDICTED:
1. D's `tile[0]` is a COUNT, not a density — only a texel size if radius never
changes. Measured 6.1:1 smear at r=70. The wall now derives the count from
each ring's own arc length (aRadius attr + uThetaSpan); D's authored numbers
keep their exact meaning at their reference radius, no re-authoring. That fix
then laid a seam down the canal (a fractional count can't wrap) -> rounded to
an integer, seam gone. Not a no-op on corridors: L2's hiatus takes 3 repeats
where it took 4, which is the texel size correctly holding still as the pipe
narrows. Eyeballed.
2. MY OWN PINCH GUARD WAS WRONG and would have blocked C. The fixture scored
1.30 and "failed" the >1.5 law; it was never bent. stats() divided the
tightest turn ANYWHERE by the widest radius ANYWHERE — 996 units apart, a
bend in the radius-13 pylorus against the radius of the sea. Locally its
worst point is 6.4. pinchRatio is now min over s of turnRadius(s)/radius(s).
Provably one-directional (global <= local by construction) so nothing that
passed can fail; real levels GAINED headroom: L2 3.32->4.16, L3 1.91->6.99,
L1 2.11->6.06. This mattered — the guard is what backs the round-1 promise
that C never has to think about curvature.
THE ACID SEA (world/acid.js, `level.acid {from,to,height,biome}`):
Flat emissive #c8ff3a, level and NOT tube-following — that's the mechanic. The
best find of the session: `height` is ONE NUMBER that tunes the level. Measured:
-18 -> 0% of the sea floor is dry shallow, -55 -> 14%, -62 -> 48%. So C's "position
IS the resource" falls out of the radius wobble they already author — the mucus
shallows are just "where the wall rises above the waterline", nothing to place —
and rising acid literally drowns them: free escalation, same scalar their event
pump drives. Also disproved my own claim: the centreline stays level (+/-1u over
700u), so shallows come from radius wobble, NOT the canal's bends. C must author
vertical bends deliberately if they want depth by anatomy.
Stated plainly, not papered over: the waterline is prototype quality (the plane
meets faceted rings and the shoreline steps). Acid does not drain the coat —
depthAt(pos) is the hook and the cost is Lane B's call. Nothing drives height but
DBG until C's events do.
qa GREEN incl. the new provenance gate; spline selfcheck green; L2 corridor
re-eyeballed for regression. Rulings requested from F in NOTES §-> Lane F.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
245 lines
11 KiB
HTML
245 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, SWEPT_STOMACH } 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.get('lvl') === 'swept') {
|
|
level = SWEPT_STOMACH; // the swept-room + acid-sea prototype (→ Lane C)
|
|
} 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>
|