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>
22 KiB
LANE A — WORLD — NOTES
Round 2 (2026-07-16) — the law-compliant look, and what shape a stomach is
Context for the reader: the original round-2 session was cut off 15:08 before NOTES; F
committed the orphaned work as f0982e7 (its commit message is the NOTES for that half) and
a resuming session finished the lane. This section covers both halves; trust the commit
message for the mechanics of the first.
What landed
First half (f0982e7, per its message): crestSpeed(s) + CREST_FACTOR 1.6 with selfcheck
asserts (ruling #1) · radius blend widened ±12 → ±25 + no-cliff selfcheck (C's #4 dependency —
the taper C assumed now exists) · per-segment wave.amp override baked as per-vertex aWaveA
(ruling #8) · colorspace law in the wall shader (ruling #2) · TBN normal maps + matcap + dual
detail layers, D's perturb() with the trap documented in-shader · sample(s, out) v1.2 ·
slug map shrunk to the two real mismatches.
Second half (this commit):
- Biome-tint re-eyeball under the colorspace law (task #4, second half). Verdict: all six
ART_BIBLE tints stand —
biomes.jsis untouched. The wash was never in the palette; it was the shader's tuned constants, all eyeballed round 1 on the raw framebuffer. Once<colorspace_fragment>encodes the output, every midtone gains roughly a stop, and the fresnel rim — which an inside-a-tube camera sees at grazing angles almost everywhere — flooded the frame (the esophagus shot was effectively rim colour wall-to-wall; the oral fallback was near white-out). Retuned inwall_material.js, where the round-1 numbers were authored:uRimPow2.2 → 3.0 (confines the rim to fold edges, where the TBN normals bend),uRimGain0.9 → 0.35, textured base(0.35 + d·0.95)·0.9→0.12 + d·0.60, procedural based·0.8→d²·0.65(the sine bands only span ~0.4..1, and post-law that reads flat — squaring restores the dark troughs), crest sheen 0.10 → 0.04, matcap gain 0.22 → 0.14. - The six-biome contact sheet that the eyeball ran on, kept as infrastructure:
?lvl=biomesin the harness loadsSIX_BIOMES(_fixture.js) — one straight segment per registry biome. Whenever tints, textures, colorspace or TBN change again, one fly-through re-checks everything. Evidence:docs/shots/laneA/round2_tint_<biome>.png×6, plusround2_L2_hiatus_retuned.png(C's calm-wave override, real L2) andround2_arena_interior_retuned.png(the BackSide/arena material path). - Ruling #5 executed: the harness moved to
web/dev/laneA_world.html(it stays — it is the lane's instrument, not a boot workaround:pose/poseAtdeterministic evidence frames, the leaksweep, the contact sheet). It gainedDBG.post(name)which POSTs the canvas to D'sshot_sink— the house evidence tool — instead of the round-1 private sink._fixture.jsstays too, and now says why: the spline selfcheck (a qa gate) asserts against FIXTURE's exact shape, headless. It is not waiting on C's levels after all. - Stomach-arena shape read for C (task #7) — recommendation + sketch, § → Lane C below.
Measured / verified this half
qa.sh GREEN end-to-end (ESM, determinism, levels 3/3, world selfcheck, manifest). Spline
selfcheck green after every edit — the _fixture.js changes are additive (new export), and
FIXTURE itself is untouched. Contact sheet: 6 draws / 61k tris per posed frame at 1280×720,
consistent with round 1's budget headroom. fps still not claimed, same reason as round 1.
Deliberately not done
- Triplanar arena shells ("if time", task #7's tail): skipped, and not only for time — if C takes the swept-room proposal below, the stomach stops being an icosphere and triplanar stops being needed for the game's biggest room. The remaining icospheres (fundus dome, boss lairs, r ≲ 70) have modest pole pinch. Decide the shape first, then spend the shader work where it still matters. Revisit in round 3 with the L3 build.
- Slug-map deletion (ruling #3): waiting on D's rename ping, as sequenced. The map is
already down to the two real mismatches (
smallint,colon).
→ Lane C (round 2): what shape is the acid sea?
Recommendation: don't grow the sphere — sweep the room along the spline you already
authored. Your L3 segments[] (fundus r 40, body r 45, antrum 25, + wobble, + curviness)
already describe the stomach's bag: my proposal is that the "arena" becomes a mode over an
s-span, not a separate geometry — the tube pipeline extrudes those segments at room radii
exactly as it does corridors, and gameplay flags the span "open" (B's 6DOF, no disc clamp).
The J-bend comes from curviness, the lobes from wobble (plus a low-frequency bulge term I
add to the radius law if you want asymmetry — greater vs lesser curvature), and the two ends
cap with hemispheres at the cardia and pylorus sphincters. What this buys, concretely:
collision is the existing project()/wallRho path (varying radius is already supported
and selfchecked — no union-of-spheres code); UVs stay (theta, s) so D's gastric pits tile
like every other wall with no pole pinch (the round-1 starburst limitation dies rather
than getting a triplanar bandage); the acid plane is a horizontal chord of the swept section
(height driven by your events, #c8ff3a, round 3); and your mucus shallows are an (s, theta)
band hugging the greater-curvature side — expressible in level data. The fundus dome stays a
welded icosphere (it's a dead-end bulge off the centreline, which a sweep can't express) —
the existing primitive, existing arenaAt. Chained overlapping spheres were the alternative
and I sketched why not: every intersection of two displaced shells is a visible crease ring,
and union collide()/project() is new math for a worse look. Cost on my side, stated
honestly: radial resolution must scale with radius (64 segments at r 45 is an 8-unit facet;
same ~3u solve the arena already does), cap geometry at the span ends, and modest streaming
cost since room chunks are bigger. Schema ask, if you take it: segments[].mode: "open" (or
an arenas: [{from, to}] span form — your call which reads better as data), spec'd by you in
schema v3 early round 3, implemented by me the same round, before you author the acid sea
proper. Sketch: docs/shots/laneA/round2_stomach_arena_sketch.svg. arenaAt/modeAt keep
their contract for B either way (bounding {center, radius} per open span).
→ Lane D (round 2)
- Your measured detail curve changed — deliberately, and it's the law's fault, not your
measurement's.
(0.35 + d·0.95)·0.9was right on the raw framebuffer; under<colorspace_fragment>its pedestal alone displays as ~60% grey. Now0.12 + d·0.60(rationale in-shader). Your mean-0.50/std-0.19 normalisation is exactly what made retuning a one-liner — the pack didn't move, the curve did.normalScale0.6 unchanged and still right. - When your mouth + colon walls land, the eyeball harness is
web/dev/laneA_world.html?dbg=1&assets=1&lvl=biomes— all six biomes, one URL. I'll re-run the contact sheet then; expect no curve change (your normalisation again). - Slug rename: still on, map deletion is a same-day promise once your NOTES ping lands.
→ Lane B (round 2)
- Nothing gameplay-moving changed this half. The retune is fragment-shader only:
wallRho, wave amplitudes,crestSpeed, collision — all untouched sincef0982e7. Your speed-lock and envelope numbers stand. - The crest sheen is dimmer (0.10 → 0.04) but the crest still reads — it keeps its geometry,
normal tilt, and rim pop. If playtest says the boost tell got too subtle, the knob is one
constant in
wall_material.jsand I'll trade sheen against rim gain, not against the law.
→ Lane E (round 2)
biomeAt(s).palettehexes are unchanged (and now display as written end-to-end, which they never quite did before the law) — safe to key HUD accents offpalette.rim.
→ Lane F (round 2)
- Ruling #5 executed as above;
web/dev/now holds D's texview and my world harness. - Housekeeping:
launch.jsongainedguts-aon 8145 (8140/8141 were other sessions'). Shot sink on 8144 was reused as the instructions suggested and left running. - The two pre-retune shots
f0982e7committed (round2_L2_hiatus.png,round2_L2_tbn_wall.png) show the washed-out pre-eyeball state — superseded by the*_retunedset, kept as the before/after record of what ruling #2 predicted.
Round 3 (mine)
Swept-room implementation if C specs it (radial-res solve + caps + open-span mode) · acid plane with C's height events · villi band when a small-intestine level exists · sphincter joint geometry at biome seams · slug-map deletion on D's ping · stomach-asymmetry bulge term if C wants authored greater/lesser curvature.
Round 1 NOTES (kept verbatim below)
World v0 is in and the real game boots it: http://localhost:8140/?dbg=1 runs Lane A's
canal on C's L2_esophagus (3600 units, hash cefc4f83), zero console errors. No boot.js
change was needed — C's levels/index.js landing is what unblocked pickWorld().
What landed
web/js/world/ — nothing outside it was touched.
| file | what |
|---|---|
spline.js |
the math: centreline, arclength LUTs, parallel-transport frames, radius law, wave phase, project(), hash(), stats(). Imports no three.js → node web/js/world/spline.js --selfcheck runs headless (14 assertions). |
biomes.js |
palette/param registry (ART_BIBLE values). C ratified this as the biome-id source of truth. |
wall_material.js |
the synthetic-scanner wall: tint × detail + fresnel rim + fog, peristalsis in the vertex shader. |
tube.js |
chunked extrusion + streaming window. |
arena.js |
displaced-icosphere shells (the stretch goal), v0. |
flycam.js |
?fly=1 noclip. Not wired — snippet for F below. |
index.js |
createWorld() — THE WORLD CONTRACT, drop-in for the stub. |
dev.html |
my harness. Dev-only; delete when boot covers it. |
_fixture.js |
dev fixture level. Delete when C's levels cover every case (arenas: L3 does now). |
Contract is implemented as written — same surface, units and semantics as the stub, so B's
stub-built work should port unchanged. Extras beyond it: flowPulse(s,t), hash(), stats(),
dispose(), spline, and an optional 2nd arg on project(pos, hint) (see → Lane B).
Measured (say how you measured it — so: how)
All from dev.html on C's real L2_esophagus with D's real pack, 1920×1080, via
renderer.info (DBG.pose() / the sweep in DBG.sweep()):
| thing | measured | budget |
|---|---|---|
| draws, worst frame over a full-canal sweep | 8 | ≤150 (charter), ≤300 (TECH) |
| tris, worst frame | 74,420 | ≤500k |
| live chunks | 5–8 (+1 per arena) | — |
| geometries after full sweep + return to s=0 | 6 → 6, no leak | dispose-clean |
geometries after dispose() |
0 (textures left alone — D owns them) | — |
| level load, C's 3600-unit L2 | <120 ms | <2 s |
| pinch ratio (min turn radius ÷ max tube radius) | L2 3.32, L3 1.89, fixture 2.37 | >1.5 |
fps is NOT measured and I'm not claiming it. Screenshot tooling drives the tab hidden,
which pauses rAF; gl.finish() didn't sync either (it reported 0.12 ms/frame = 8219 fps,
which is obviously junk). Draws/tris are reliable and are 20× under budget, so I expect fps to
be fine — but someone with a real window has to confirm. → Lane F, at round end.
Determinism: same seed ⇒ same hash, and node and the browser agree (50a5b0ec for the
fixture in both). Different seed ⇒ different hash. Both asserted in the selfcheck.
Decisions (mine to make; flagging the ones that touch you)
curviness= fraction of the tightest bend this pipe can survive, not an amplitude. Octave amplitudes are solved from a curvature budgetkappa = curviness / (2.2 × baseRadius). Why: authoring amplitudes in units let a wide, curvy segment fold the tube through itself — the selfcheck's pinch guard caught exactly that on its first run (turn radius 9.9 vs tube radius 17.2). Now a wide pipe straightens itself and C never has to think about curvature. C's 0.15–0.95 range works as documented; nothing to change.- Peristalsis: global
OMEGA = 3.08 rad/s, wavenumberk(s) = OMEGA / flow(s), phaseK(s) = ∫k ds(baked per-vertex asaPhase). Consequence worth knowing: a wave crest travels at exactly the local flow speed, so "ride the crest for boost" is physically the same thing as "ride the current". Plaink·s − ω·ttears the wave at every flow change;K(s)doesn't. For a flow-14 esophagus this reproduces the stub's wave exactly (k = 0.22). - Esophagus
wave.amp0.9 → 1.4 (biomes.js). The stub's 0.9 is a barely-legible ripple; GDD §2 makes riding a crest the level's signature, and B can't surf what the player can't see. 1.4 reads as distinct rings and still leaves ~78% of a radius-10 tube flyable. Measured in the harness against 0.9 and 3.2. This moveswallRho→ Lane B. - Chunk seams align to biome joins; an arena owns its whole s-span and the tube is skipped
there (otherwise the corridor renders inside the room and
collide()disagrees with what you can see). Seam is chunk-quantised ±20u — fine until sphincter geometry exists. - Arena fog is sized to the room, not the biome:
min(biome.fog, 1.09/radius). Biome fog is tuned for ~100u corridor sightlines; reused in C's 360u-wide acid sea it renders a black rectangle (that's what my first arena shot literally was). Small lairs keep the murk. - One
uvattribute = (θ/2π, s in world units), no uv2 — tiling is derived in-shader from auTileuniform, so a second UV set would be dead weight. Charter said uv2; this is the simplification, flagged rather than done silently. - Colorspace: matched to the stub — the wall writes its colour with no
<colorspace_fragment>conversion. That's a whole-game decision (it moves every colour at once) so I'm not making it unilaterally → Lane F.
Known limitations (v0, stated plainly)
- Arena pole pinch. Arena UVs are spherical, so the fold pattern converges into a starburst
at the poles — visible top-right in
round1_L3_arena_acid_sea.png. A real texture will pinch there too. Round 2: triplanar projection for arena shells. - Arena shape. A sphere is the wrong shape for a stomach and C says so in their own
L3note. Round 2 should discuss a capsule/lobed form; thearenaAtcontract (centre+radius) survives either way for B's clamp. - No acid plane (GDD's animated
#c8ff3alevel) — round 2, needs C's event to drive height. - No villi (small-intestine InstancedMesh band) — round 2, no level needs it yet.
- No matcap / normal map. D ships
normalmaps already; using them needs a TBN in the wall shader. Round 2. Untested code paths are worse than absent ones. project()in arena mode is step-clamped rather than properly solved; it's accurate in tube mode (recovers s to <0.25, θ to <0.02 rad — asserted). Arena collision doesn't use it (sphere test), so this only matters if B callsprojecton a point way off the centreline.- Segment param blending lags the march by one 0.5u step. Deterministic, sub-step, harmless.
→ Lane F
- Pass
assetsintocreateWorld. The game currently boots my world but D's textures are loaded and then unused — I verified every material comes upUSE_DETAIL-less underboot.js. InpickWorld():
(const mod = await import('./world/index.js'); const level = await loadLevel(flags.lvl); const assets = await (await import('./core/assets.js')).createAssets({ flags, renderer }); return mod.createWorld(level, { rng: createRng(flags.seed ?? level.seed), assets });createWorldalready ignores a missing/failedassets— the fallback law holds either way.) - Add the world selfcheck to the qa gate — it has caught two real defects already:
# 4b. Lane A world math (headless; no three.js needed) node web/js/world/spline.js --selfcheck >/dev/null 2>&1 && green "world selfcheck" || red "world selfcheck failed" - Screenshot tooling traps, which will bite you at round-end boot-matrix time. The pane
drives tabs hidden: rAF never fires (so the loop isn't running when you shoot) and
innerWidthis 0 (so the canvas is 0×0 andtoDataURL()returns"data:,"). The pane screenshot still looks right because grabbing it fronts the tab.DBG.shot()'s anchor-click download also silently no-ops here. My harness works around all three inDBG.size/pose/poseAt— worth lifting intoboot.js's DBG. I got PNGs out by having the pagefetch(PUT)them to a 30-line local sink (in my scratchpad, happy to promote it totools/if you want it). - Dev port: 8140 was already held by another session's server, and
launch.jsonis yours, so I left it alone and reused the running one. If lanes are meant to run concurrently on one box this needs a call. - Contract freeze: I'd like
project(pos, hint)andflowPulse/hash/stats/disposefolded into TECH.md's world contract, andassets.texture()added to the asset contract (see → Lane D).
→ Lane B
wallRhoshrank: it subtractswave.amp + wave.breathe + 0.6and esophaguswave.ampis now 1.4 (was 0.9). Playable radius in a base-10 tube ≈ 7.85. Re-check your graze distances. It's time-independent on purpose — a collision surface that breathed would have you fighting the wall.project(pos, hint)— pass your previoussashintand it's near-free and exact. With no hint it seeds from-pos.zand still converges (6 fixed-point steps), but the hint is strictly better in your per-frame path. Both are asserted in the selfcheck.- Crest speed ==
biomeAt(s).flow, exactly, by construction.world.flowPulse(s, t)is the same function the vertex shader runs (0 = trough, 1 = crest), so pulse ≈ 1 means the wall is at its narrowest AND the crest is moving at flow speed right there. Boost window. biomeAt(s).flowis blended across segment joins, not stepped — safe to read per-frame.- Arena mode:
modeAt(s)/arenaAt(s)→{center, radius}for your 6DOF clamp.collide()branches automatically (sphere test inside a room, radius test in a tube); you don't have to. sample()allocates fresh Vector3s per call, same as the stub. If that shows up in your profile, ask and I'll addsampleInto(s, out).
→ Lane C
- Your schema v1 reads clean here — v1 is additive and my reader consumes L1/L2/L3 unchanged.
Your ratified arena semantics (sphere spanning
[at-radius, at+radius]) are exactly what's implemented, andmodeAt/arenaAtagree with your notes on all three L3 arenas. - Biome ids are the registry in
world/biomes.js:oral · esophagus · stomach · small_intestine · large_intestine · appendix. Unknown id ⇒ neutral fallback + one warning, never a crash. Thanks for writing the "biome = tissue family, not anatomy" rule into TECH — that's exactly right and it's whynamecarries the anatomy. curvinessbehaves as you documented (0 straight … 1 writhing). Under the hood 1.0 now means "as curvy as this radius can safely be", so you can't author a pinch — L2 measures 3.32× clear, L3 1.89×. Author freely.- Free lever if you want it:
flowis per-segment and it now sets the peristalsis wavelength (k = 3.08/flow). Low flow ⇒ tighter, slower rings. A "sluggish" segment reads sluggish. - Your L3 note that a sphere is the wrong shape for the stomach body: agreed, see limitations.
→ Lane D
- Naming mismatch, silent by construction. Your keys are
wall_smallint_a/ (future) colon; my biome ids aresmall_intestine/large_intestine.wall_${biomeId}_atherefore misses, and because assets are optional it falls back to procedural forever with no error. I've added an explicit slug map inindex.jsso it works today. Proposal for round 2: standardise on the biome ids (wall_small_intestine_a) and delete the map — one name, one place. Your call, just let's not leave it implicit. - Your
tilesemantics and mine converged independently —[repeats around theta, units of s per repeat], both read off the stub's uv. Ratified from my side;[4,16]looks right in engine (round1_L2_textured.png). texture()is the useful entry point, notget(). TECH namesget(category,name)as THE contract, but it returns raw JSON a shader can't eat. I preferassets.texture(name)and keepgetduck-typed as the documented floor. Suggest F promotestexture()into TECH.- FYI: tiling is applied in-shader from
tile. A raw ShaderMaterial has nouvTransform, so therepeatyou pre-apply on the texture is inert here. Not a problem — just don't count on it. wall_esophagus_alooks genuinely good in engine. The_bvariants and matcap aren't wired yet (round 2, with the TBN work).
→ Lane E
world.flowPulse(s, t)(0..1, crest = 1) is yours to pulse audio/HUD with; it's exact, cheap, and matches what the wall is visibly doing.world.biomeAt(s)→{id, palette, fog, flow, coatDrain}.palette.tint/rim/voidare the ART_BIBLE hexes — userimif you want HUD accents that match the biome you're in.
Round 2 (mine)
Triplanar arena shells + acid plane · villi band (InstancedMesh, small intestine) · normal-map
TBN + matcap · sphincter joint geometry at biome seams · sampleInto if B needs it · delete
_fixture.js/dev.html once boot + C's levels cover them.