Implements THE WORLD CONTRACT (TECH.md) in web/js/world/; drop-in for the stub. boot.js now runs this on C's L2_esophagus (3600u, hash cefc4f83), zero console errors. - spline.js: centreline + arclength LUTs + parallel-transport frames + radius law + peristalsis phase + project() + hash(). No three.js import, so qa can run its 14-assertion selfcheck headless. - curviness is solved from a curvature budget, not authored as an amplitude: the pinch guard caught the tube folding through itself on a wide+curvy join (turn radius 9.9 vs tube radius 17.2). C can no longer author a pinch. - peristalsis: global OMEGA, k(s) = OMEGA/flow(s), phase K(s) = integral k ds. A crest therefore travels at exactly the local flow speed, so "surf the crest" == "ride the current". Esophagus wave amp 0.9 -> 1.4 (measured): moves wallRho for Lane B. - tube.js: chunked streaming, seams aligned to biome joins, arenas own their s-span. - arena.js: displaced icosphere shells v0 (three's polyhedron detail is 20*(detail+1)^2, not 20*4^detail — 720 tris was far too coarse); fog sized to the room, since biome fog tuned for 100u corridors renders C's 360u acid sea as a black rectangle. - index.js: prefers assets.texture() over get() (get returns raw JSON a shader can't eat), plus an explicit slug map — D's wall_smallint_a vs biome id small_intestine would miss silently forever under the assets-optional law. Measured (C's real L2 + D's real pack, 1920x1080, renderer.info): 8 draws worst frame (budget 150), 74k tris (500k), no geometry leak across a full sweep, <120ms load, pinch ratio 3.32. fps not claimed — the screenshot pane runs tabs hidden, pausing rAF. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
194 lines
13 KiB
Markdown
194 lines
13 KiB
Markdown
# 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
|
||
`<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
|
||
`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.
|