# LANE A — NOTES (round 1) 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) 1. **`curviness` = fraction of the tightest bend this pipe can survive**, not an amplitude. Octave amplitudes are solved from a curvature budget `kappa = 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. 2. **Peristalsis: global `OMEGA = 3.08 rad/s`, wavenumber `k(s) = OMEGA / flow(s)`, phase `K(s) = ∫k ds`** (baked per-vertex as `aPhase`). 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". Plain `k·s − ω·t` tears 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). 3. **Esophagus `wave.amp` 0.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 moves `wallRho`** → Lane B. 4. **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. 5. **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. 6. **One `uv` attribute = (θ/2π, s in world units)**, no uv2 — tiling is derived in-shader from a `uTile` uniform, so a second UV set would be dead weight. Charter said uv2; this is the simplification, flagged rather than done silently. 7. **Colorspace: matched to the stub** — the wall writes its colour with no `` 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 `L3` note. Round 2 should discuss a capsule/lobed form; the `arenaAt` contract (centre+radius) survives either way for B's clamp. - **No acid plane** (GDD's animated `#c8ff3a` level) — 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 `normal` maps 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 calls `project` on a point way off the centreline. - Segment param blending lags the march by one 0.5u step. Deterministic, sub-step, harmless. --- ## → Lane F 1. **Pass `assets` into `createWorld`.** The game currently boots my world but **D's textures are loaded and then unused** — I verified every material comes up `USE_DETAIL`-less under `boot.js`. In `pickWorld()`: ```js 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 }); ``` (`createWorld` already ignores a missing/failed `assets` — the fallback law holds either way.) 2. **Add the world selfcheck to the qa gate** — it has caught two real defects already: ```bash # 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" ``` 3. **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 `innerWidth` is **0** (so the canvas is 0×0 and `toDataURL()` 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 in `DBG.size/pose/poseAt` — worth lifting into `boot.js`'s DBG. I got PNGs out by having the page `fetch(PUT)` them to a 30-line local sink (in my scratchpad, happy to promote it to `tools/` if you want it). 4. **Dev port**: 8140 was already held by another session's server, and `launch.json` is 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. 5. Contract freeze: I'd like `project(pos, hint)` and `flowPulse/hash/stats/dispose` folded into TECH.md's world contract, and `assets.texture()` added to the asset contract (see → Lane D). ## → Lane B - **`wallRho` shrank**: it subtracts `wave.amp + wave.breathe + 0.6` and esophagus `wave.amp` is 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 previous `s` as `hint` and it's near-free and exact. With no hint it seeds from `-pos.z` and 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).flow` is **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 add `sampleInto(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, and `modeAt`/`arenaAt` agree 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 why `name` carries the anatomy. - `curviness` behaves 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: `flow` is 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 are `small_intestine` / `large_intestine`. `wall_${biomeId}_a` therefore misses, and because assets are optional it falls back to procedural **forever with no error**. I've added an explicit slug map in `index.js` so 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 `tile` semantics 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, not `get()`.** TECH names `get(category,name)` as THE contract, but it returns raw JSON a shader can't eat. I prefer `assets.texture(name)` and keep `get` duck-typed as the documented floor. Suggest F promotes `texture()` into TECH. - FYI: tiling is applied **in-shader** from `tile`. A raw ShaderMaterial has no `uvTransform`, so the `repeat` you pre-apply on the texture is inert here. Not a problem — just don't count on it. - `wall_esophagus_a` looks genuinely good in engine. The `_b` variants 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/void` are the ART_BIBLE hexes — use `rim` if 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.