[lane F] Round 1 integration + ROUND2 instructions: the game is a game
Wired B's player/combat + event pump, D's assets, C's level pick into boot. Fixed qa ESM gate (node --check no-op, found by B) + added spline selfcheck. TECH: world contract FROZEN v1.1, bus events ratified, colorspace + crest-speed laws (CREST_FACTOR 1.6 — B proved surfing lost to throttle at 1.0). Stub complies. Verified integrated build via 60s stepped sim: 9 draws, 0 errors, 0 asset misses; one known gap (fiction-id spawn resolve) confirmed and assigned as B's top item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
efea7d98f3
commit
4ccf5edcc0
@ -6,6 +6,12 @@
|
||||
"runtimeExecutable": "python3",
|
||||
"runtimeArgs": ["-m", "http.server", "8140", "--directory", "web"],
|
||||
"port": 8140
|
||||
},
|
||||
{
|
||||
"name": "guts-f",
|
||||
"runtimeExecutable": "python3",
|
||||
"runtimeArgs": ["-m", "http.server", "8141", "--directory", "web"],
|
||||
"port": 8141
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
# GUTS — Round 1 lane instructions (from Fable, integrator)
|
||||
|
||||
> **⚠ SUPERSEDED by [ROUND2_INSTRUCTIONS.md](ROUND2_INSTRUCTIONS.md) (2026-07-16).** Round 1
|
||||
> is complete (lanes A–D landed; E's scope carries into round 2). Historical reference only.
|
||||
|
||||
Date: 2026-07-16 · Repo state: F's round-0 scaffold is committed on `main` — docs complete
|
||||
(README/PROCESS/GDD/TECH/ART_BIBLE/PIPELINE + charters), shell boots the **stub world**
|
||||
(straight pulsing tube, flat shading) with a fly camera, `tools/qa.sh` GREEN. Everything
|
||||
|
||||
172
docs/LANES/ROUND2_INSTRUCTIONS.md
Normal file
172
docs/LANES/ROUND2_INSTRUCTIONS.md
Normal file
@ -0,0 +1,172 @@
|
||||
# GUTS — Round 2 lane instructions (from Fable, integrator)
|
||||
|
||||
Date: 2026-07-16 · Written after reviewing all four round-1 lanes (A/B/C/D — E did not run
|
||||
and joins this round) and integrating their offered wiring. Every round-1 claim I checked
|
||||
held up: A's hash `cefc4f83` and 8-draw sweep, B's `node --check` ESM no-op (reproduced on a
|
||||
deliberately broken file), C's 3/3 selfcheck, D's manifest. Superb round, all of you.
|
||||
|
||||
## Repo state (F integrated, commit this round)
|
||||
|
||||
**The game is a game now.** Default boot = A's world on C's `L2_esophagus` with D's textures,
|
||||
B's ENDO-1 + camera + combat, level-event pump live. Verified by stepped simulation (60 sim
|
||||
seconds, deterministic dt): flow carried the player to s=777, checkpoints + pickups fired,
|
||||
9 draws / 52–61k tris, zero console errors, zero asset misses.
|
||||
Evidence: `docs/shots/laneF/round1_integrated_s777.png`.
|
||||
|
||||
F landed since round 1: B/C/D snippets in `boot.js` (assets → world, player/combat wiring,
|
||||
`?stub=1&lvl=` composing, event pump) · qa.sh **ESM gate fixed** (it had never checked
|
||||
anything — thank you B) + A's spline selfcheck gate · TECH: world contract **frozen v1.1**,
|
||||
bus events ratified, asset entry points ratified, colorspace + crest-speed laws · stub:
|
||||
`crestSpeed()`, CREST factor, colorspace fix. Second dev server config `guts-f` on **8141**
|
||||
(8140 was held by another session — lanes: if 8140 is busy, use your own port and say so).
|
||||
|
||||
## The ONE integration gap found (and it was predicted)
|
||||
|
||||
Stepped sim confirmed: **all 11 L2 spawn events no-op with `[combat] unknown enemy type
|
||||
"bolus_chunk"/"eosinophil_swarm"/"candida_bloom"`.** C's fiction-id → archetype resolve
|
||||
(LANE_C_NOTES → Lane B #1) is the single blocker between "it boots" and "it's a game with
|
||||
enemies". B: it is your first task.
|
||||
|
||||
## Integrator rulings (settled — argue in NOTES for round 3 if you must)
|
||||
|
||||
1. **Surf: CREST_FACTOR = 1.6.** B proved crest speed == flow makes surfing strictly worse
|
||||
than throttle (LANE_B_NOTES §surf). Ruling: `crestSpeed(s) = 1.6 × flow(s)` (beats
|
||||
throttleMax 1.4), A implements `k(s) = OMEGA/(1.6·flow)`, B speed-locks to
|
||||
`world.crestSpeed(s)` while riding, gameplay wave == visual wave stays law. Stub already
|
||||
complies — B can build now. C re-runs pacing (pars will compress; the finale's escape
|
||||
math changes deliberately — surfing becomes the stylish escape).
|
||||
2. **Colorspace law** (TECH §Shader law): every custom shader ends `main()` with
|
||||
`#include <colorspace_fragment>`. Stub done. A (wall), B (emissive) apply this round —
|
||||
expect a visible shift; A re-eyeballs biome tints against D's textures after.
|
||||
3. **Texture naming**: D renames keys to A's biome ids **early in the round** and pings in
|
||||
NOTES; A deletes the slug map after pulling. That order, one round, done.
|
||||
4. **`level:event` pump is boot's (F).** Landed. B: when you build respawn, tell F what
|
||||
reset/replay API the pump needs (`### → Lane F`), don't work around it.
|
||||
5. **`web/dev/` ratified** as the shared dev-harness dir, per-file lane ownership, never
|
||||
shipped. D's texview stays. A: `world/dev.html` may move there or die, your call.
|
||||
D's `pipeline/shot_sink.py` is the **house evidence tool** — all lanes:
|
||||
`python3 pipeline/shot_sink.py --dir docs/shots/lane<x> --port 8143` (NOT 8140/8141),
|
||||
then POST the canvas. `DBG.shot()`'s download silently no-ops in the pane (A was right).
|
||||
6. **WebP shots ratified** (B's 25 KB vs 1.9 MB argument). PNG also fine. Name the round.
|
||||
7. **C's GDD refinements + boss specs: signed off.** C folds the three boss one-pagers into
|
||||
GDD.md proper. Per-level pars stand. A/D treat boss specs as stable input.
|
||||
8. **A's uv simplification** (one uv attr + uTile uniform, no uv2): ratified, contract-level.
|
||||
**C's arena semantics + A's per-segment `wave` override request: approved** — C specs
|
||||
`segments[].wave: { amp }` into schema v2, A implements `waveMaxAt` preferring it.
|
||||
|
||||
## Round-2 goal
|
||||
|
||||
**L2 esophagus, playable start to finish, textured, scored, survivable and losable** — every
|
||||
spawn live, all three hazards real, checkpoints/death/respawn, gate → `level:complete`,
|
||||
HUD + audio on top. Tag `v0.2`. (Round-3 preview: arenas — L1 tutorial + L3 acid sea + the
|
||||
Pyloric Guardian.)
|
||||
|
||||
---
|
||||
|
||||
## Lane A — contract v1.2 + the look, deepened
|
||||
|
||||
1. **`crestSpeed(s)` + CREST_FACTOR 1.6** (ruling #1) — small, unblocks B, do first.
|
||||
2. **Answer C's biggest dependency** (LANE_C_NOTES → A #4): segment radius **blending**.
|
||||
C assumes a smooth ~40–60u taper at joins (12→9 at s 1100, 10→6.8 at 2600). Confirm or
|
||||
implement; a hard step reads as a level seam and breaks the constrictions.
|
||||
3. **Per-segment `wave` override** (ruling #8) — the hiatus should be tight AND calm.
|
||||
4. Colorspace one-liner in the wall shader (ruling #2), then re-eyeball all six biome tints
|
||||
with D's textures; adjust `biomes.js` freely (C's selfcheck re-checks live, as designed).
|
||||
5. **TBN normal maps** — lift D's `perturb()` from `web/dev/laneD_texview.html` (their NOTES
|
||||
has the exact GLSL and the trap: never add tangent-space normals to vertex normals). Wire
|
||||
`_b` variants + matcap. This is the round's visual headline; D called it "the single
|
||||
biggest visual win" and their shots agree.
|
||||
6. `sample(s, out)` out-param variant (v1.2) — B adopts on landing.
|
||||
7. Stomach-arena shape read for C (→ C #5): propose capsule/lobed/chained-spheres, one
|
||||
paragraph + sketch, before C authors the acid sea in round 3. Triplanar arena shells if
|
||||
time (the pole-pinch fix) — acid plane can wait for round 3 with the L3 work.
|
||||
Defer: villi band (no L4 level yet).
|
||||
|
||||
## Lane B — make L2 a game (priority order is the order)
|
||||
|
||||
1. **C's three consumption fixes** (LANE_C_NOTES → B #1–3, exact snippets provided):
|
||||
`getEnemy()` resolve, `theta` arrays, `spread`. Verify against the live L2: sim to s=800
|
||||
and the console warns must be gone, bolus/eosinophil/candida all spawning.
|
||||
2. **The three L2 hazards** (params in `levels/enemies.js` HAZARDS): `reflux_surge` (rear
|
||||
chaser; emit `hazard:proximity {kind,distance}` — C's most important UI dependency),
|
||||
`aortic_squeeze` (**directional** — one-sided by design, keep contact non-lethal or tell
|
||||
C), `ring_gate` (timeable point gate). Emit `hazard:warn {kind,s,eta}` from each event's
|
||||
`warn` field.
|
||||
3. **Checkpoint / death / respawn** (≤30 s law). Define the pump reset API with F (ruling
|
||||
#4). Respawn never inside a hazard zone (C placed checkpoints accordingly — keep it true).
|
||||
4. **Pickups**: nutrient, mucin, B12, antacid ammo, biopsy samples (`pickup` events are in
|
||||
L2 already; economy numbers from C via NOTES if you need more than `levels/enemies.js`).
|
||||
5. **Gate → `level:complete {stats}`** with score/time/samples for E's medal card.
|
||||
6. **Surf speed-lock** on `world.crestSpeed(s)` (stub has it now; A lands theirs early).
|
||||
Re-measure: riding a crest must beat throttle-mashing by a margin worth learning.
|
||||
7. Colorspace in `emissive.js` (your own finding) — then re-shoot `round1_combat`'s hostile
|
||||
amber and confirm it reads amber.
|
||||
8. Stretch only: arena 6DOF controller (round 3 needs it for L1/L3).
|
||||
Your tuning.js header still cites the stale `wallRho` numbers (C flagged) — fix the comment.
|
||||
|
||||
## Lane C — validate with a real canal
|
||||
|
||||
1. **Fly L2 on the integrated build** once B's #1 lands (mid-round — watch B's NOTES). Use
|
||||
F's stepped-sim technique (f-progress documents it) or the browser; replace sim numbers
|
||||
with observed ones. **The hiatus (s 2600–3000) is the thing to validate**; your open
|
||||
question #4 (`MIN_CLEARANCE 2.5`) gets its answer here, alongside John's playtest.
|
||||
2. **Re-run pacing under CREST 1.6 surf** (ruling #1): pars, the finale escape math, and
|
||||
whether antacid-stalling is still the smart play. Adjust `par` values with rationale.
|
||||
3. **Schema v2**: `segments[].wave` override (approved) — spec it, re-author the hiatus calm
|
||||
(amp ~0.6), selfcheck it.
|
||||
4. Fold boss specs into GDD.md (signed off, ruling #7).
|
||||
5. **L1 encounter design on paper** (molar crush-cycles, saliva tides, epiglottis exam) —
|
||||
full authoring waits for B's arena controller in round 3, but the design doc and the
|
||||
skeleton→full upgrade plan land now.
|
||||
6. Give B the pickup economy (counts/values) as data or NOTES — B's #4 consumes it.
|
||||
|
||||
## Lane D — heroes and the full pack
|
||||
|
||||
1. **Slug rename first** (ruling #3): `wall_small_intestine_a` etc., manifest + batch JSONs,
|
||||
ping A in NOTES same session.
|
||||
2. **Hero meshes, the round-2 headline**: ENDO-1 ship, `bolus_chunk`, `eosinophil_swarm` —
|
||||
concepts via flux_local (ART_BIBLE stems + C's tints in `levels/enemies.js`) →
|
||||
**concepts into `docs/shots/laneD/` + NOTES for F/C sign-off BEFORE any TRELLIS run** →
|
||||
sf3d draft → trellis_mac → normalize (port `normalize.py` + `glb_stat.py` from PROCITY —
|
||||
the GLB law is unenforced until they exist) → thumbs → manifest. `candida_bloom` as a
|
||||
decal/sprite sheet (your call, ratified — C agreed).
|
||||
3. **Colon + mouth walls** (2 each) + acid/decal sprites — completes the biome texture set.
|
||||
4. **Full audio pack**: E must hand you their key list by mid-round (E's #5); if it hasn't
|
||||
arrived, propose the keys yourself in NOTES and E adapts. Beds for all six biomes, sfx
|
||||
(torpedo, gate, alarm, checkpoint, death/respawn, UI ticks), 3 boss stingers, ≤10 MB law.
|
||||
5. Tapeworm stays a **round-3** fal.ai candidate — do NOT spend on it yet; if concepts are
|
||||
ready early, put it on `FAL_LIST.md` with the concept attached for John's go.
|
||||
|
||||
## Lane E — you have a full house waiting (round-1 charter + these specifics)
|
||||
|
||||
Read your charter + the `→ Lane E` sections in B/C/D/A NOTES — all four wrote APIs for you.
|
||||
1. **HUD** from `player:state` + `combat:state` (emitted every frame, live now): coat/hull,
|
||||
heat, throttle/speed/flow, boost cd, score/combo. Bus-only, no imports from B.
|
||||
2. **Gut-map v1** (the signature): `listLevels()`/`getLevel()` in `levels/index.js` give you
|
||||
names/lengths/pars; L2 has 7 named segments + 10 checkpoints authored for exactly this.
|
||||
3. **Rear-proximity indicator** for `hazard:proximity` + telegraph pings for `hazard:warn` —
|
||||
C calls this the single most important UI request (the surge chases from behind).
|
||||
4. Title (over `?fly=1`-style drift) · pause · **medal card** on `level:complete` (par data
|
||||
in the level) · death/respawn feed-drop. Damage = feed corruption, never red vignette.
|
||||
5. **Audio engine**: D's pack via `assets.audioUrl/audio` (keys + gain law in LANE_D_NOTES →
|
||||
Lane E), procedural fallback beeps (`?localassets=0` must make noise), bed crossfade on
|
||||
`biomeAt`, heartbeat tempo → danger scalar (define it from bus events, document mapping).
|
||||
**Hand D your round-3 key wishlist by mid-round** (D's #4).
|
||||
6. Respect `?shots=1`/`?mute=1`; every screen screenshotted into `docs/shots/laneE/`.
|
||||
|
||||
## Lane F (me) — done this round: the integration above. On-call for contract questions;
|
||||
end of round: boot matrix, v0.2 tag, ROUND3.
|
||||
|
||||
---
|
||||
|
||||
## 🎮 John's playtest checklist (the things no agent can measure)
|
||||
|
||||
Open `http://localhost:8140/?dbg=1` (or 8141) in a real browser and just play:
|
||||
1. **The fps number** in the bottom-left overlay (we can only prove draws/tris; rAF is
|
||||
paused in our tooling — every lane flagged this honestly).
|
||||
2. **Feel** — B set `bank.gain`, `cam.discFollow`, `aim.lag` by reasoning, not play. Say
|
||||
"banking feels heavy/floaty/right" in any form; B will translate.
|
||||
3. **The hiatus** (s 2600–3000, once B's hazards land): thrilling or miserable? This decides
|
||||
C's clearance floor.
|
||||
4. **Dart readability** when a turret volley passes the camera (B suspects too dominant).
|
||||
5. **Audio ear-check** of D's pack (spectrograms pass; ears are yours).
|
||||
71
docs/TECH.md
71
docs/TECH.md
@ -44,28 +44,49 @@ export function createX({ scene, world, bus, rng, flags, assets }) {
|
||||
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)
|
||||
## THE WORLD CONTRACT — **FROZEN v1.1** (round-1 sign-off, 2026-07-16)
|
||||
|
||||
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.
|
||||
Changes now go through NOTES + F referee only. A implements (`world/index.js`); the stub is
|
||||
the executable reference. 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 (lengths story-dilated ~14×, see
|
||||
LANE_C_NOTES). +Y up, right-handed. Frame convention (B builds against it):
|
||||
`worldPos = pos + nor·y + bin·x`, `theta = atan2(x, y)`; camera `up` = `nor`, never world-up.
|
||||
|
||||
```js
|
||||
const world = await createWorld(levelData, { rng, quality });
|
||||
const world = await createWorld(levelData, { rng, assets, quality }); // assets optional
|
||||
world.length // total arclength
|
||||
world.level // the levelData it was built from
|
||||
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.project(pos, hint?) // → { s, theta, rho }; pass previous s as hint (near-free path)
|
||||
world.wallRho(s, theta) // → max safe ρ (radius − conservative displacement); time-independent
|
||||
world.collide(pos, r) // → null | { push:Vector3, kind:'wall'|'hazard', biome }
|
||||
world.biomeAt(s) // → { id, palette, fog, flow, coatDrain } (see biomes.js registry)
|
||||
world.biomeAt(s) // → { id, palette, fog, flow, coatDrain }; flow BLENDS across joins
|
||||
world.modeAt(s) // → 'tube' | 'arena'
|
||||
world.arenaAt(s) // → null | { center:Vector3, radius } (bounds for 6DOF clamp)
|
||||
world.flowPulse(s, t?) // → 0..1 peristalsis phase (1 = crest); same fn the shader runs
|
||||
world.crestSpeed(s) // → u/s the crest travels; == CREST_FACTOR × flow(s), see law below
|
||||
world.update(dt, playerS) // stream chunks, advance shader time
|
||||
world.hash() // golden determinism hash (qa gate)
|
||||
world.dispose() // frees geometries/materials it created (not D's textures)
|
||||
```
|
||||
|
||||
Determinism law: same `levelData.seed` ⇒ byte-identical geometry. A maintains a golden
|
||||
hash (`world.hash()` over sampled frames) checked by qa.
|
||||
Planned v1.2 (A, round 2; B adopts when it exists): `sample(s, out)` out-param variant —
|
||||
`sample` currently allocates ~6 Vector3s/call and it's B's hot path.
|
||||
|
||||
**Crest-speed law (round-2 ruling, from B's round-1 finding):** the wave must outrun the
|
||||
player or surfing loses to throttle-mashing. `crestSpeed = CREST_FACTOR × flow` with
|
||||
**CREST_FACTOR = 1.6** (> throttleMax 1.4), i.e. A's `k(s) = OMEGA / (1.6 · flow(s))`.
|
||||
Gameplay wave == visual wave, always. B speed-locks to `crestSpeed(s)` while riding.
|
||||
|
||||
Determinism law: same `levelData.seed` ⇒ byte-identical geometry, node == browser.
|
||||
|
||||
**Shader law (round-1 finding, B):** every custom shader that writes to the default target
|
||||
must end `main()` with `#include <colorspace_fragment>` — three converts `THREE.Color`
|
||||
inputs to linear but does NOT convert a raw shader's output back to sRGB, so un-converted
|
||||
colours ship visibly wrong (measured: ART_BIBLE amber `#ff5a2a` displayed as pure red).
|
||||
Stub complies; A's wall + B's emissive materials comply from round 2.
|
||||
|
||||
## Level data schema **v1** (C owns — ratified round 1; F drafted v0)
|
||||
|
||||
@ -142,10 +163,22 @@ re-tuned. Below 2.5 a constriction is not a challenge, it is a toll.
|
||||
|
||||
## 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}`.
|
||||
`player:spawn {s}` · `player:damage {amount, kind}` (**discrete hits only** — continuous
|
||||
drain rides `player:state`, never events, or E's damage flash pins on) · `player:death` ·
|
||||
`player:boost` · `enemy:spawn {id,type}` · `enemy:die {id,type,s,score,kind}` ·
|
||||
`pickup {kind}` · `level:event {…}` (raw C events as the player crosses their `s`; **the
|
||||
pump is owned by boot.js/F**, wired round 2) · `level:checkpoint {s}` ·
|
||||
`level:complete {stats}` · `level:neutralize {s,radius,duration}` (B's antacid torpedo →
|
||||
A/C acid zones) · `boss:start {id}` / `boss:phase` / `boss:end` · `combo {n}` ·
|
||||
`audio:cue {name}`.
|
||||
|
||||
Ratified round 2 (round-1 requests):
|
||||
- `player:state {coat,coatMax,hull,hullMax,speed,flow,throttle,boostReady,boostCd,boostCdMax,surfing,iframes,s,length,progress,biome,alive}` — every frame, read-only, don't retain (B → E).
|
||||
- `combat:state {heat,heatMax,overheated,ammo,ammoMax,score,kills,enemies}` — every frame (B → E).
|
||||
- `player:surf {active, s}` — edge-triggered (B → E).
|
||||
- `hazard:warn {kind, s, eta}` — fires `warn` seconds before a hazard (C events carry `warn`).
|
||||
- `hazard:proximity {kind, distance}` — rear-chaser telemetry (reflux surge); drives E's
|
||||
rear indicator, C's most important UI dependency.
|
||||
|
||||
## Asset manifest contract (D owns `assets.js` + `manifest.json`)
|
||||
|
||||
@ -163,6 +196,16 @@ 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.
|
||||
|
||||
Ratified round 2: `get(category, name)` is the raw floor; the consumer entry points are
|
||||
**`assets.texture(name)`** → `{map, normalMap, tile, repeat}` (repeat pre-computed
|
||||
`[tile[0], 1/tile[1]]`; multiply into UVs yourself — `texture.repeat` is inert in a raw
|
||||
ShaderMaterial), **`assets.audioUrl(group, key)`** (browser-correct ogg/m4a pick) and
|
||||
**`assets.audio(group, key)`** → `{ogg, m4a, loop, gain, seconds}` (respect `gain`).
|
||||
Misses are ledgered: first miss logs once, `assets.misses()` lists all (drifted names
|
||||
announce themselves instead of falling back silently forever).
|
||||
Texture naming law: keys use **A's biome ids** (`wall_small_intestine_a`), no abbreviations
|
||||
— D renames + A deletes its slug map in round 2, in that order.
|
||||
|
||||
## Flags (F owns this table; lanes request rows via NOTES)
|
||||
|
||||
| flag | owner | effect |
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
# Lane F (Fable) — progress
|
||||
|
||||
## 2026-07-16 — Round 1 review + integration (round 2 opened)
|
||||
|
||||
Reviewed all four lanes; every checked claim held (A's hash cefc4f83 + 8-draw sweep, B's
|
||||
node --check ESM no-op — reproduced, C's 3/3 selfcheck + sim, D's manifest + seam numbers).
|
||||
|
||||
Integrated (this commit): B's player/combat wiring + level-event pump into boot.js · D's
|
||||
assets into boot + world · C's ?stub=1&lvl= composition · qa.sh ESM-gate fix (was a no-op
|
||||
on every module in the repo) + spline selfcheck gate · TECH world contract FROZEN v1.1
|
||||
(project hint, flowPulse, crestSpeed, hash, dispose, level) + bus ratifications
|
||||
(player:state, combat:state, player:surf, hazard:warn, hazard:proximity, level:neutralize)
|
||||
+ assets.texture/audioUrl/audio promotion + Shader-law (colorspace) + crest-speed law
|
||||
(CREST_FACTOR 1.6, from B's surf finding) · stub: crestSpeed + CREST wave + colorspace.
|
||||
|
||||
Verified integrated build (guts-f server, :8141): default boot = A world + C L2 + D pack +
|
||||
B ship. 60 sim-seconds stepped (rAF is paused in the hidden pane — deterministic dt loop
|
||||
via the boot module exports instead): s→777, checkpoints+pickups fired, 9 draws / 61k tris,
|
||||
0 console errors, 0 asset misses. ONE gap: all 11 spawns no-op on fiction ids ("unknown
|
||||
enemy type bolus_chunk") — exactly C's predicted → B item; top of B's round 2.
|
||||
Evidence: docs/shots/laneF/round1_integrated_s777.png. fps/feel still needs John (checklist
|
||||
in ROUND2 §John).
|
||||
|
||||
shot_sink usage that works from the pane: `python3 pipeline/shot_sink.py --dir
|
||||
docs/shots/laneF --port 8143` then POST canvas blob (DBG.shot download no-ops, as A warned).
|
||||
|
||||
## 2026-07-16 — Round 0: scaffold
|
||||
|
||||
Repo initialized from empty. Landed:
|
||||
|
||||
BIN
docs/shots/laneF/round1_integrated_s777.png
Normal file
BIN
docs/shots/laneF/round1_integrated_s777.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
13
tools/qa.sh
13
tools/qa.sh
@ -7,12 +7,14 @@ FAIL=0
|
||||
red() { printf '\033[31m✗ %s\033[0m\n' "$1"; FAIL=1; }
|
||||
green() { printf '\033[32m✓ %s\033[0m\n' "$1"; }
|
||||
|
||||
# 1. every JS module parses
|
||||
# 1. every JS module parses.
|
||||
# NB: plain `node --check` exits 0 on ANY file containing import/export (ESM detection
|
||||
# no-op — found by Lane B round 1, verified on node v26). Must force module parsing.
|
||||
JSBAD=0
|
||||
while IFS= read -r -d '' f; do
|
||||
node --check "$f" 2>/dev/null || { node --check "$f"; JSBAD=1; }
|
||||
node --check --input-type=module < "$f" 2>/dev/null || { echo "SYNTAX: $f"; node --check --input-type=module < "$f"; JSBAD=1; }
|
||||
done < <(find web/js -name '*.js' -print0)
|
||||
[ "$JSBAD" = 0 ] && green "node --check: all web/js modules parse" || red "JS syntax errors"
|
||||
[ "$JSBAD" = 0 ] && green "node --check (ESM): all web/js modules parse" || red "JS syntax errors"
|
||||
|
||||
# 2. determinism law: Math.random only in core/rng.js
|
||||
RAND=$(grep -rn 'Math\.random' web/js --include='*.js' | grep -v 'core/rng.js' || true)
|
||||
@ -35,6 +37,11 @@ if [ -f web/js/levels/index.js ]; then
|
||||
node web/js/levels/index.js --selfcheck && green "levels selfcheck" || red "levels selfcheck failed"
|
||||
fi
|
||||
|
||||
# 4b. Lane A world math (headless; no three.js needed)
|
||||
if [ -f web/js/world/spline.js ]; then
|
||||
node web/js/world/spline.js --selfcheck >/dev/null 2>&1 && green "world selfcheck" || red "world selfcheck failed"
|
||||
fi
|
||||
|
||||
# 5. Lane D manifest validation (when it exists)
|
||||
if [ -f pipeline/validate_manifest.py ]; then
|
||||
python3 pipeline/validate_manifest.py && green "manifest valid" || red "manifest errors"
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
// boot.js (Lane F) — the shell. Creates renderer/scene/loop, picks the world (real or
|
||||
// stub), hosts the debug camera + DBG. Lanes: request wiring via NOTES, don't edit here.
|
||||
// stub), constructs assets + player + combat, pumps level events, hosts DBG.
|
||||
// Lanes: request wiring via NOTES, don't edit here. Round 2: A/B/C/D snippets integrated.
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { parseFlags } from './core/flags.js';
|
||||
import { createRng } from './core/rng.js';
|
||||
import { createBus } from './core/bus.js';
|
||||
import { createAssets } from './core/assets.js';
|
||||
import { createWorld as createStubWorld, STUB_LEVEL } from './stub/world_stub.js';
|
||||
import { createPlayer } from './flight/player.js';
|
||||
import { createCombat } from './combat/index.js';
|
||||
|
||||
const flags = parseFlags();
|
||||
const bus = createBus();
|
||||
@ -15,31 +19,50 @@ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
document.getElementById('game').appendChild(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);
|
||||
const assets = await createAssets({ flags, renderer });
|
||||
|
||||
async function loadLevel(id) {
|
||||
try {
|
||||
const mod = await import('./levels/index.js'); // Lane C's registry
|
||||
return mod.getLevel(id); // default level lives there
|
||||
} catch (e) {
|
||||
console.info('[boot] Lane C registry not available —', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function pickWorld() {
|
||||
const level = (await loadLevel(flags.lvl)) ?? STUB_LEVEL;
|
||||
const rng = createRng(flags.seed ?? level.seed);
|
||||
if (!flags.stub) {
|
||||
try {
|
||||
const mod = await import('./world/index.js'); // Lane A's world, when it lands
|
||||
const level = await loadLevel(flags.lvl);
|
||||
return mod.createWorld(level, { rng: createRng(flags.seed ?? level.seed) });
|
||||
const mod = await import('./world/index.js'); // Lane A's world
|
||||
return mod.createWorld(level, { rng, assets });
|
||||
} catch (e) {
|
||||
console.info('[boot] Lane A world not available, using stub —', e.message);
|
||||
}
|
||||
}
|
||||
return createStubWorld(STUB_LEVEL, { rng: createRng(flags.seed ?? STUB_LEVEL.seed) });
|
||||
}
|
||||
async function loadLevel(id) {
|
||||
const mod = await import('./levels/index.js'); // Lane C's registry, when it lands
|
||||
return mod.getLevel(id);
|
||||
return createStubWorld(level, { rng, assets }); // ?stub=1&lvl=<id> composes (beat 1 only — stub is single-segment)
|
||||
}
|
||||
|
||||
const world = await pickWorld();
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);
|
||||
scene.add(world.group);
|
||||
scene.background = new THREE.Color(world.biomeAt(0).palette.void);
|
||||
|
||||
// --- debug drift/fly camera (placeholder until Lane B's player rig; ?fly=1 = manual) ---
|
||||
// --- player + combat (Lane B). ?fly=1 keeps the noclip/drift camera instead. ---
|
||||
const rng = createRng(flags.seed ?? world.level?.seed ?? 0);
|
||||
const player = flags.fly ? null
|
||||
: createPlayer({ scene, world, bus, rng, flags, camera, dom: renderer.domElement });
|
||||
const combat = player ? createCombat({ scene, world, bus, rng, flags, player }) : null;
|
||||
|
||||
// --- level-event pump (owned by boot per round-2 ruling): emits C's events as the
|
||||
// player crosses their s. Checkpoint/respawn replay semantics: revisit with B in round 2.
|
||||
let _evts = [...(world.level?.events ?? [])].sort((a, b) => a.s - b.s), _ei = 0;
|
||||
const pumpTo = (s) => { while (_ei < _evts.length && s >= _evts[_ei].s) bus.emit('level:event', _evts[_ei++]); };
|
||||
|
||||
// --- debug drift/fly camera (?fly=1, or fallback when B's player is absent) ---
|
||||
let sCam = 5, look = { yaw: 0, pitch: 0 }, dragging = false, speed = world.biomeAt(0).flow * 0.6;
|
||||
const keys = new Set();
|
||||
addEventListener('keydown', (e) => keys.add(e.code));
|
||||
@ -47,7 +70,7 @@ addEventListener('keyup', (e) => keys.delete(e.code));
|
||||
addEventListener('mousedown', () => (dragging = true));
|
||||
addEventListener('mouseup', () => (dragging = false));
|
||||
addEventListener('mousemove', (e) => {
|
||||
if (!dragging) return;
|
||||
if (!dragging || player) return;
|
||||
look.yaw -= e.movementX * 0.003;
|
||||
look.pitch = THREE.MathUtils.clamp(look.pitch - e.movementY * 0.003, -1.2, 1.2);
|
||||
});
|
||||
@ -78,7 +101,7 @@ window.DBG = {
|
||||
get draws() { return stats.draws; },
|
||||
get tris() { return stats.tris; },
|
||||
get fps() { return fps; },
|
||||
world,
|
||||
world, player, combat, assets,
|
||||
shot(name = 'shot') {
|
||||
const a = document.createElement('a');
|
||||
a.download = `${name}.png`;
|
||||
@ -97,8 +120,15 @@ let last = performance.now();
|
||||
function frame(now) {
|
||||
const dt = Math.min((now - last) / 1000, 0.05);
|
||||
last = now;
|
||||
if (player) {
|
||||
player.update(dt); // must run first: combat hit-tests this frame's ship position
|
||||
pumpTo(player.state.s);
|
||||
combat.update(dt);
|
||||
world.update(dt, player.state.s);
|
||||
} else {
|
||||
updateCamera(dt); // ?fly=1 noclip / title drift
|
||||
world.update(dt, sCam);
|
||||
updateCamera(dt);
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
stats.draws = renderer.info.render.calls;
|
||||
stats.tris = renderer.info.render.triangles;
|
||||
@ -106,12 +136,14 @@ function frame(now) {
|
||||
frames++; fpsT += dt;
|
||||
if (fpsT >= 0.5) {
|
||||
fps = Math.round(frames / fpsT); frames = 0; fpsT = 0;
|
||||
if (flags.dbg && !flags.shots)
|
||||
dbgEl.textContent = `${fps} fps · ${DBG.draws} draws · ${(DBG.tris / 1000) | 0}k tris · s=${sCam.toFixed(0)} · ${world.level?.id ?? '?'} ${world.hash?.() ?? ''}`;
|
||||
if (flags.dbg && !flags.shots) {
|
||||
const s = player?.state.s ?? sCam;
|
||||
dbgEl.textContent = `${fps} fps · ${DBG.draws} draws · ${(DBG.tris / 1000) | 0}k tris · s=${s.toFixed(0)} · ${world.level?.id ?? '?'} ${world.hash?.() ?? ''}`;
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
if (flags.dbg && !flags.shots) dbgEl.style.display = 'block';
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
export { scene, camera, renderer, bus, flags, world }; // read-only handles for lanes
|
||||
export { scene, camera, renderer, bus, flags, world, player, combat, assets };
|
||||
|
||||
@ -44,7 +44,9 @@ const BIOME = { // mirrors ART_BIBLE esophagus row; A's biomes.js is the real
|
||||
|
||||
const WAVE_A = 0.9; // peristalsis displacement amplitude (units, inward)
|
||||
const WAVE_K = 0.22; // spatial frequency (rad/unit along s)
|
||||
const WAVE_W = 3.0; // angular speed (rad/s)
|
||||
const CREST = 1.6; // crest phase speed = CREST × flow. Round-2 ruling: must beat
|
||||
// throttleMax (1.4) or surfing loses to throttle-mashing (Lane B,
|
||||
// round 1). ω = k · CREST · flow, so at flow 14: 22.4 u/s.
|
||||
const SKIN = 0.6; // collision safety margin
|
||||
|
||||
export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) {
|
||||
@ -64,7 +66,8 @@ export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) {
|
||||
return { pos: centre(s), tan, nor, bin, radius: radiusAt(s) };
|
||||
}
|
||||
const radiusAt = (s) => R * (1 + seg.radius.wobble * Math.sin(s * 0.05) * Math.sin(s * 0.013 + 2.1));
|
||||
const flowPulse = (s, t) => Math.pow(Math.max(0, Math.sin(WAVE_K * s - WAVE_W * t)), 3);
|
||||
const waveW = WAVE_K * CREST * seg.flow; // crest phase speed ω/k = CREST × flow
|
||||
const flowPulse = (s, t) => Math.pow(Math.max(0, Math.sin(WAVE_K * s - waveW * t)), 3);
|
||||
|
||||
// --- geometry: rings along s, uv = (theta/2pi, s), inward-facing ---
|
||||
// seam ring vertex is duplicated (RADIAL+1 columns) so uv.x runs 0..1 without wrapping
|
||||
@ -114,7 +117,7 @@ export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) {
|
||||
uniform float uTime;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
float pulse = pow(max(0.0, sin(${WAVE_K.toFixed(3)} * uv.y - ${WAVE_W.toFixed(2)} * uTime)), 3.0);
|
||||
float pulse = pow(max(0.0, sin(${WAVE_K.toFixed(3)} * uv.y - ${waveW.toFixed(3)} * uTime)), 3.0);
|
||||
float breathe = 0.15 * sin(uv.y * 0.7 + uTime * 0.8) * sin(uv.x * 6.2831 * 3.0);
|
||||
vec3 disp = position + aInward * (${WAVE_A.toFixed(2)} * pulse + breathe);
|
||||
vec4 mv = modelViewMatrix * vec4(disp, 1.0);
|
||||
@ -135,6 +138,7 @@ export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) {
|
||||
float d = length(vView);
|
||||
col = mix(col, uVoid, 1.0 - exp(-uFog * d * 0.55));
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
#include <colorspace_fragment>
|
||||
}`,
|
||||
});
|
||||
|
||||
@ -151,6 +155,7 @@ export async function createWorld(levelData = STUB_LEVEL, { rng } = {}) {
|
||||
group,
|
||||
sample,
|
||||
flowPulse: (s, t = time) => flowPulse(s, t),
|
||||
crestSpeed: (_s) => CREST * seg.flow, // u/s the crest travels at s (contract v1.1)
|
||||
project(v) {
|
||||
const s = THREE.MathUtils.clamp(-v.z, 0, LEN);
|
||||
const f = sample(s);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user