# TECH — architecture, contracts, ownership Stack: vanilla three.js **r175** (vendored, `web/vendor/`), ES modules + import map, zero build step, `python3 -m http.server` to run. No physics engine, no framework. This is house style (proven in PROCITY) — keep it. ## Module layout & ownership ``` web/ index.html F shell + import map + flags table wiring js/boot.js F creates renderer/scene/loop, wires lane factories, window.DBG js/core/rng.js F mulberry32 seeded RNG (THE only randomness source) js/core/bus.js F event bus (on/off/emit) js/core/flags.js F URL flag parsing js/core/assets.js D manifest loader + `depot:`/local resolution + fallback law js/stub/world_stub.js F straight-tube world implementing the world contract js/world/** A createWorld() — spline, tube chunks, arenas, biome shaders js/flight/** B createPlayer() — controller, camera rig, collision response js/combat/** B weapons, damage, enemy framework + behaviors js/levels/*.json C level data (schema below) js/levels/index.js C level registry + loader + schema selfcheck js/ui/** E HUD, menus, gut-map, screens js/audio/** E WebAudio engine (plays D's pack; procedural fallback) assets/** D manifest.json + generated webp/glb/ogg/m4a + thumbs pipeline/** D generation scripts (run on the m3ultra box) tools/qa.sh F the gate docs/** all (own lane's files only) ``` Never edit outside your column. Shell wiring requests → your NOTES (`### → Lane F`), with the exact snippet to paste (PROCITY house style). ## Lane factory convention Every lane exports pure factories, constructed by boot, no side effects at import time: ```js export function createX({ scene, world, bus, rng, flags, assets }) { return { update(dt, ctx){}, dispose(){}, /* lane-specific API */ }; } ``` Default-off behind flags until F wires them on. Dispose must actually free GPU resources (geometries, materials, textures) — enter/exit cycles are leak-tested. ## THE WORLD CONTRACT (A implements; stub implements it today; frozen after round-1 sign-off) Spline-space: `s` = arclength along the canal centreline (units), `θ` = angle in the cross-section (rad), `ρ` = radial distance from centreline. Units are metres in three.js terms; fiction scale ≈ 1 unit : 1 mm. +Y up, right-handed. ```js const world = await createWorld(levelData, { rng, quality }); world.length // total arclength world.group // THREE.Group (boot adds to scene) world.sample(s) // → { pos, tan, nor, bin, radius } (parallel-transport frame) world.project(pos) // → { s, theta, rho } (world → spline space, player-local fast path) world.wallRho(s, theta) // → max safe ρ (radius minus conservative displacement amplitude) world.collide(pos, r) // → null | { push:Vector3, kind:'wall'|'hazard', biome } world.biomeAt(s) // → { id, palette, fog, flow, coatDrain } (see biomes.js registry) world.modeAt(s) // → 'tube' | 'arena' world.arenaAt(s) // → null | { center:Vector3, radius } (bounds for 6DOF clamp) world.update(dt, playerS) // stream chunks, advance shader time ``` Determinism law: same `levelData.seed` ⇒ byte-identical geometry. A maintains a golden hash (`world.hash()` over sampled frames) checked by qa. ## Level data schema v0 (C owns; F drafted; sign-off in round 1) ```jsonc { "id": "L2_esophagus", "name": "Esophagus", "seed": 20260716, "segments": [ // concatenated along s { "biome": "esophagus", "length": 600, "radius": { "base": 10, "wobble": 0.25 }, // wobble: seeded radius variation 0..1 "curviness": 0.6, // 0 straight .. 1 writhing "flow": 14 } // base current, units/s ], "arenas": [ { "at": 580, "radius": 90, "biome": "stomach" } ], "events": [ // consumed by B (spawns) & E (telegraphs) via bus { "s": 40, "type": "spawn", "enemy": "bolus_chunk", "count": 6 }, { "s": 120, "type": "hazard", "kind": "reflux_surge", "speed": 22 }, { "s": 300, "type": "checkpoint" }, { "s": 560, "type": "boss", "id": "pyloric_guardian" } ] } ``` ## Bus events (append here when adding; F referees name collisions) `player:spawn {s}` · `player:damage {amount, kind}` · `player:death` · `player:boost` · `enemy:spawn {id,type}` · `enemy:die {id,type,s}` · `pickup {kind}` · `level:event {…}` (raw C events as player crosses their `s`) · `level:checkpoint {s}` · `level:complete {stats}` · `boss:start {id}` / `boss:phase` / `boss:end` · `combo {n}` · `audio:cue {name}`. ## Asset manifest contract (D owns `assets.js` + `manifest.json`) ```jsonc { "textures": { "wall_esophagus_a": { "url": "gen/wall_esophagus_a.webp", "normal": "gen/wall_esophagus_a_n.webp", "tile": [3, 8] } }, "matcaps": { "tissue_wet": { "url": "gen/matcap_tissue.webp" } }, "models": { "macrophage": { "url": "models/macrophage.glb", "tris": 4200 } }, "audio": { "beds": { "stomach": {"ogg": "...", "m4a": "..."} }, "sfx": { … } } } ``` Law: **every entry is optional at runtime.** `assets.get('textures','x')` returns `null` on miss and the consumer must have a procedural fallback (flat palette color, primitive mesh, synthesized beep). The game must be fully playable with an empty `assets/gen/`. House GLB law (from PROCITY): metres, +Y up, sensible origin, **≤5k tris**, WebP textures ≤1024, no Draco. `?localassets=0` simulates empty-manifest boot for fallback testing. ## Flags (F owns this table; lanes request rows via NOTES) | flag | owner | effect | |---|---|---| | `?stub=1` | F | world stub instead of Lane A world | | `?lvl=` | C | boot straight into a level | | `?seed=` | F | override level seed | | `?fly=1` | A | noclip fly camera (no player) | | `?dbg=1` | F | perf HUD + `window.DBG` helpers | | `?mute=1` | E | no audio | | `?shots=1` | F | hide HUD/debug for clean screenshots | | `?localassets=0` | D | boot with empty manifest (fallback test) | ## Perf & QA budgets ≤300 draw calls worst view · ≤500k tris in frustum · 60 fps M-series & mid laptops · level load <2 s · zero console errors · dispose-clean on level exit (renderer.info deltas). `window.DBG` (F provides): `{draws, tris, fps, shot(name)}` — cite it when claiming numbers. `tools/qa.sh` gates: `node --check` every js file · no `Math.random` outside `core/rng.js` · level JSONs parse + required keys · vendor present · (grows per round).