guts/docs/LANES/LANE_C_NOTES.md
jing 1b5ec767cd [lane C] Round 1: schema v1, levels registry + selfcheck + pacing sim, L2 vertical slice
Data spine:
- Schema v1 ratified (TECH.md, C-owned). Additive over v0: schema/par/next, segment
  name, cross-section placement (theta/rho as a FRACTION of safe radius), spread, span,
  pickup+gate event types, hazard params. STUB_LEVEL synced (that const only, per
  ROUND1 §Lane C.1) — world.hash() unchanged (1bdf001), geometry provably untouched.
- levels/index.js: registry + loader (node+browser, no THREE), real schema selfcheck
  (qa's --selfcheck hook is now live) and a pacing simulator (--sim). Consumes the other
  lanes' contracts live rather than restating them: A's biomes.js for ids and
  wave.amp+breathe, A's SKIN, B's tuning.js hull radius, B's balance.js for scores.
- levels/enemies.js (fiction id -> archetype join table), levels/difficulty.js.

L2_esophagus — the vertical slice, complete: 3600 units, 7 beats, 47 events, 65 enemies,
10 checkpoints, 3 samples, reflux-surge finale. L1/L3 skeletons (shape only).
Anatomy is the level designer: the three real esophageal constrictions are the three
setpieces, and all three enemies are real esophageal pathology (bolus chunk, eosinophilic
esophagitis, candida esophagitis).

Measured (node web/js/levels/index.js --selfcheck|--sim; qa GREEN):
- selfcheck 3/3 valid, exit 0.
- pressure curve 0.27 -> 2.14 -> 4.35 -> 0.69 -> 4.53 -> 6.35 -> 4.40 (breather/pressure/
  spike/breather/pressure/peak/finale).
- par 3:30 vs measured 2:47 speedrun / 3:19 par-pace / 4:22 timid; par.score 9000 of a
  9400 kill budget (95%), computed from B's scores.
- all 10 checkpoint gaps within the <=30s death-cost law (max 28.8s); min clearance 2.73.

Two contract collisions found and resolved (LANE_C_NOTES):
- Biome ids: authored mouth/pharynx/cardia/pylorus; A's registry has six canonical ids and
  rejected all four. A is right — biome = look, segments[].name = anatomy. Conformed.
- Balance: my first enemies.js restated hp/size/speed/score that B owns in balance.js.
  Cut to a join table; index.js reads B's scores via scoreFor().

Caught live: A raised esophagus wave.amp 0.9 -> 1.4 mid-session, making the diaphragmatic
hiatus unflyable (2.33 vs the 2.5 floor). Widened 6.5 -> 6.8. The invariant also failed my
own aortic constriction's first draft (radius 7, symmetric squeeze) -> radius 9, directional.

No in-engine flythrough: ?stub=1&lvl= does not compose (snippet offered to F) and the stub
is single-segment, so it could only render beat 1. The pacing evidence is a kinematic model,
not a playtest — it proves L2 is flyable, paced and survivable, not that it feels good.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:53:34 +10:00

475 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LANE C — NOTES (Levels & Encounters)
Round 1 · 2026-07-16 · all numbers below are **measured** with `node web/js/levels/index.js
--sim` unless flagged otherwise. Read §→ Lane X for anything addressed to you.
---
## What landed (round 1)
| file | what |
|---|---|
| `web/js/levels/index.js` | registry + loader + **schema selfcheck** (qa gate) + **pacing simulator** |
| `web/js/levels/enemies.js` | enemy catalogue (fiction id → archetype), pickup + hazard vocabularies |
| `web/js/levels/difficulty.js` | the single difficulty table + `applyDifficulty()` |
| `web/js/levels/L2_esophagus.json` | **the vertical slice** — complete: 3600 units, 7 beats, 65 enemies, 47 events |
| `web/js/levels/L1_mouth.json` | skeleton (shape only) |
| `web/js/levels/L3_stomach.json` | skeleton (shape only) |
| `docs/TECH.md` §Level schema | ratified **v1** (was v0) |
| `docs/GDD.md` | per-level time targets, esophagus roster, L2 setpieces (bannered) |
| `web/js/stub/world_stub.js` | `STUB_LEVEL` synced to v1 (authorised by ROUND1 §Lane C.1 — **only** that const) |
`node web/js/levels/index.js --selfcheck`**3/3 valid, exit 0**. qa.sh green.
---
## Schema v1 — what changed from v0, and why
Ratified in TECH.md. All additive; every v0 level still parses. I own it from here.
| added | why it was needed |
|---|---|
| `schema` | so a level and its loader can disagree loudly instead of silently |
| `theta` / `rho` | v0 could only place content **along** the canal. A wall-mounted turret and a "visible-but-costly" sample are both statements about the **cross-section**. `rho` is a **fraction of the safe radius**, never a distance — radius varies with s, so a fraction is the only placement that cannot end up inside a wall. |
| `spread` | `count: 6` at a single `s` is a pile, not a field |
| `span` | hazards have extent (a 400-unit squeeze zone), not just a trigger point |
| `par` | medals need thresholds; GDD §Scoring had nowhere to live |
| `pickup`, `gate` types | the pickup economy and the level→level seam were undefined in v0 |
| segment `name` | the anatomy, separated from the biome (see below) |
| `next` | campaign order belonged in data, not in code |
**The one real design decision in v1: `biome` ≠ anatomy.** Lane A's registry has six canonical
ids (`oral · esophagus · stomach · small_intestine · large_intestine · appendix`). I had
authored `mouth`/`pharynx`/`cardia`/`pylorus` and the selfcheck rejected all four against A's
live registry. **A is right and I conformed** — biome is the *tissue/look family*,
`segments[].name` carries the anatomy. There is no cardia biome and there should not be one.
That turned out to be a gift rather than a compromise, because it lets colour do the
wayfinding:
- **L1's oropharynx wears the esophagus palette** — the tube goes teal 200 units before the
epiglottis, so you read the transition in the walls before any HUD tells you.
- **L3's cardia keeps the esophagus palette** — you arrive in teal and the world turns orange
the moment the fundus opens. The acid sea *reveals itself*. Spending a biome id on the
cardia would have thrown that reveal away.
---
## L2_esophagus — the vertical slice
**Identity: the sprint.** The purest tube in the body, one biome, no arenas, everything
readable at 1420 u/s. Par **3:30**; measured speedrun **2:47**, timid run **4:22**.
**Scale, stated honestly:** cross-sections are anatomically true (radius 1012 units ≈ a 2 cm
lumen at 1 unit : 1 mm). Lengths are **dilated ~14×** — the real esophagus is 25 cm, this
level is 3600 units. The canal is *proportioned*, not measured. I would rather say that out
loud once than have five lanes quietly assume different scales.
**Anatomy is the level designer.** The esophagus has three real constrictions
(cricopharyngeal, broncho-aortic, diaphragmatic hiatus). They are the level's three
setpieces, in the real order. Deviations I chose, on purpose:
- The broncho-aortic constriction is a single ~2 cm feature in life; L2 dilates it into two
beats (aortic at 1100, bronchial at 2000) because one is a spike and one is a ramp.
- In life the **cricopharyngeal** narrowing is the tightest of the three. L2 makes the
**hiatus** tightest instead, because the level needs to escalate and beat 1 is the tutorial.
### Measured pacing (`--sim L2_esophagus`)
```
beat s-range len flow en dens tight haz PRESSURE clear t cum
Upper Esophageal Sphincter 0- 400 400 12 1 0.25 1.09 1.00 0.27 .......... 6.63 29s 0:29
Cervical Esophagus 400-1100 700 14 15 2.14 1.00 1.00 2.14 ###....... 6.79 43s 1:12
Aortic Constriction 1100-1500 400 16 9 2.25 1.33 1.45 4.35 #######... 2.88 22s 1:34
Thoracic Esophagus 1500-2000 500 14 3 0.60 1.00 1.15 0.69 #......... 6.55 31s 2:05
Bronchial Narrowing 2000-2600 600 16 17 2.83 1.20 1.33 4.53 #######... 4.45 33s 2:38
Diaphragmatic Hiatus 2600-3000 400 18 9 2.25 1.76 1.60 6.35 ########## 2.73 19s 2:57
Abdominal Esophagus 3000-3600 600 20 11 1.83 1.33 1.80 4.40 #######... 4.15 22s 3:19
```
`pressure = density × tightness × hazard` — density alone is a lie in a tube, because 9
enemies in a 6.8-unit hole is not 9 enemies in a 12-unit hole.
**The curve: 0.27 → 2.14 → 4.35 → 0.69 → 4.53 → 6.35 → 4.40.** Breather → pressure → SPIKE →
breather → pressure → SPIKE → finale. Two waves, escalating (4.35 → 4.53 → peak 6.35).
**The finale is not the pressure peak, and that is deliberate.** You cannot sustain
maximum dodge-density at maximum speed and call it fair. The finale's intensity is *stakes*,
not density: it is the only place in L2 where a mistake is **fatal** rather than expensive.
The pressure index cannot see that — it is a relative index, not a fun-meter. Flagging it so
nobody "fixes" beat 7 by piling enemies into it.
**Honesty about this evidence:** the sim is a *kinematic model*, not a playtest. It assumes a
constant throttle and no combat slowdown (fair-ish in tube mode: the current carries you and
you cannot stop). Times are **lower bounds** on a real run. It cannot tell me whether the
hiatus *feels* good — only that it is flyable, paced, and survivable. Verdict on feel needs
A's world + B's controller in round 2. See §Why no in-engine flythrough.
### Teach-then-test ledger
Every mechanic appears safe before it appears lethal. This is the law I checked the level against:
| mechanic | taught (safe) | tested |
|---|---|---|
| the flow itself | b1, s 0400 — slowest flow, straightest tube, one lazy chunk | the whole level |
| bolus chunk | s 240 — one, slow, unmissable | s 1260, in a 9-unit tube that pulses |
| eosinophil swarm | s 620 — three, open water, nothing else happening | s 2880, chasing you through the hiatus gate |
| candida bloom | s 760 — one, straight wall, 200-unit sightline | s 1180, in the squeeze that pushes you at it |
| ring gate | s 2100 — open 67% of the time | s 2800 — open 40%, in the tightest tube |
| **reflux surge** | **s 1700 — the "burp": 16 u/s vs 14 flow, non-lethal** | **s 3000 — 21 u/s, lethal, 600 units** |
The burp is the most important event in the level. When the real surge arrives 1300 units
later, the player already knows the verb.
**One deliberate exception:** `aortic_squeeze` (s 1100) is first met *as* the spike. It gets
away with it because it is non-lethal (graze damage) and 400 units long, so its first two
pulses teach it inside its own zone. If B makes squeeze contact lethal, this breaks — tell me.
### Checkpoints — death costs ≤ 30 s (measured, at par throttle)
10 checkpoints, sphincter-themed where anatomy allows. Max gap **28.8 s** (the opening
breather, where you are least likely to die). Every gap is enforced by the selfcheck, so this
cannot silently rot.
```
20 -> 420 400u 28.8s 420 -> 860 440u 27.3s 860 -> 1080 220u 13.7s
1080 -> 1520 440u 24.2s 1520 -> 1680 160u 9.9s 1680 -> 2000 320u 19.9s
2000 -> 2280 280u 15.2s 2280 -> 2580 300u 16.3s 2580 -> 2980 400u 19.4s
2980 -> 3600 620u 23.2s
```
Checkpoints sit **before** hazard zones, never inside them (1080 precedes the squeeze at
1100; 2980 precedes the surge at 3000). Respawning a player inside a squeeze is a bug.
### The three biopsy samples — visible-but-costly, three different costs
Never on the racing line, always in plain sight. The cost is what differs:
| # | s | cost |
|---|---|---|
| 1 | 700, θ=π, ρ=0.92 | **proximity** — pinned to the far wall in the tail of the bolus field: wall-hug (graze) while chunks tumble past |
| 2 | 1300, θ=π/2, ρ=0.90 | **timing** — against the wall *inside* the squeeze zone: eat a pulse, or thread it between beats |
| 3 | 3200, θ=3π/2, ρ=0.85 | **the chase** — ~1.5 s off-line with an acid wall ~5 s behind you. The only one that is a real decision at full speed. |
### The finale, tuned
Surge 21 u/s spawning 40 units behind you, chasing 600 units. Segment flow is 20, throttle
range ×0.61.4 ⇒ 1228 u/s. So: throttle 1.05+ outruns it; dawdling at 12 u/s kills you. Par
pace (~26 u/s) finishes ~5 s clear. **The level's last instruction is "do not dawdle".**
The antacid torpedo is the skill expression: B's `balance.js` gives it
`neutralizeRadius: 14, neutralizeDuration: 10`, so **fired backward it stalls the surge ~10 s**.
L2 hands you 2 antacid at s 2300 and 1 at s 3080, never explains this, and rewards you for
noticing. On `hard`, `difficulty.js` puts the surge at 23.1 u/s and the chase stops being a
sprint and becomes a route-planning problem.
### L2 roster — all three are real esophageal pathology
| fiction | archetype | real thing |
|---|---|---|
| `bolus_chunk` | floater | swallowed food debris in the swallow |
| `eosinophil_swarm` | seeker | **eosinophilic esophagitis** — immune cells that mistake ENDO-1 for the parasite they were built for |
| `candida_bloom` | turret | **candida esophagitis** — white plaque colonies anchored to the mucosa |
Reality is weirder and better than invention (charter). All three fell out of "what actually
goes wrong in an esophagus", and each one maps to a *different* archetype B already built.
---
## Why there is no in-engine flythrough this round
My charter says to fly each level on the stub (`?lvl=L2_esophagus&stub=1`). **That flag
combination does not exist yet** — `boot.js` hardcodes `STUB_LEVEL` on the `?stub=1` path and
never reads `flags.lvl`. Snippet offered in §→ Lane F.
Worth saying plainly: **even with that wiring, the stub could not validate L2's pacing.** The
stub is single-segment (`levelData.segments[0]`), so it would render beat 1 and nothing else.
L2's whole design is the *sequence* of 7 beats. The real flythrough needs Lane A's world and
Lane B's controller, which is exactly the round-2 integration.
So round 1's evidence is the **pacing sim** — which is the right instrument for a data lane,
and is stronger than eyeballing one beat: it checks all 10 checkpoint gaps, all 7 clearances
and the par against three throttle profiles, on every qa run, forever. What it cannot do is
tell me the hiatus *feels* good. That claim I am not making.
---
## Boss specs
One-pager each: phases → telegraphs → windows → failure pressure. Written now so **A can
shape lairs** and **D can concept hero meshes** in round 2. Numbers are design intent, not
measured — they need B's framework to tune against.
**The roster's shape: three bosses, three different pressures.** Guardian = a *coat* race
(positional attrition). Tapeworm = a *distance* race (a chase you can lose without dying).
Blockage = a *hull* race + doom timer (darkness and risk/reward). If they all became DPS
checks we would have built one boss three times.
### 1. Pyloric Guardian — L3 exit, arena (`arenas[2]`, s 2350, radius 70)
The pyloric sphincter itself: a muscular iris, hypertrophied and colonised. It periodically
opens to pass chyme — **that cycle is the whole fight**, because the nodes are only exposed
when the ring relaxes. The arena churns throughout, pushing you at the walls.
| phase | hp | the loop | window |
|---|---|---|---|
| **P1 Peristaltic Cycle** | 100→66% | iris opens 2.5 s every 8 s; 4 mucosal nodes glow at the rim, vulnerable only while open. 3 fixed vents spit acid on a 3 s cycle. | **2.5 s / 8 s (31%)** |
| **P2 Reflux Fury** | 66→33% | cycle tightens to 1.8 s every 5 s. Between openings it vomits bolus chunks + pepsin swarms into the arena. 2 of the 4 regrown nodes are armored — **antacid strips them**. Churn ×1.5. | **1.8 s / 5 s (36%)** |
| **P3 Death Grip** | 33→0% | it stops opening and tries to **seal permanently — 25 s doom timer**. The core is exposed only when it spasms. Acid vents become a rotating sweep. | **1.2 s every ~4 s**, ×5 hits |
- **Telegraphs:** a deep gulp 2 s before every open · nodes amber → white-hot when vulnerable ·
vents dribble 0.8 s before spitting · P3's window = the whole arena flashes and **the churn
reverses** for 1.2 s (you feel the tell in the controls, not just the eyes).
- **Failure pressure:** stomach coat-drain is continuous, so this is a **race against your
coat, not its hp**. Mucus shallows on the greater curvature pause the drain but are far
from the iris — the risk/reward is positional. P3 adds the doom timer.
- **Scaling:** `difficulty.window` multiplies every window; doom timer 25 s × (1.3 / 1.0 / 0.85).
### 2. Tapeworm — L4 miniboss, tube chase
*Taenia solium*, metres long, coiled through the ileum. **It does not fight you — it runs**,
and the canal is its body. A chase at high flow through the villi forest, which occludes
sightlines and keeps making it vanish.
| phase | hp | the loop | window |
|---|---|---|---|
| **P1 The Tail** | 100→70% | you catch the tail. 6 proglottids, each with a pale weak point — a **genital pore**, which real proglottids actually have — on one face only. It rolls on a 4 s period; shoot the pore as it comes round. Killed segments detach and become obstacles. | **~1.8 s of every 4 s roll** |
| **P2 Constriction** | 70→40% | it coils around the lumen into 3 rotating rings you thread. Weak points move to the ring joints. | **1.5 s per joint pass** |
| **P3 The Scolex** | 40→0% | it turns to face you: four suckers and a rostellum of hooks, charging head-on. Hooks fire as projectiles. Weak point = the suckers, exposed as it lunges. | **1.4 s per lunge** |
- **Telegraphs:** pore-glow ~1.2 s before it is shootable · a grinding drone that rises as you
close (distance made audible — the villi hide it visually) · hook volleys pre-flash ·
the scolex rears with a wet inhale 1.2 s before each lunge.
- **Failure pressure:** **it is a chase — you can lose without dying.** Fall behind and it
escapes: soft fail, level continues, medal gone, and **it reappears in L5 as a hazard**.
A consequence the player earned is worth more than a retry screen.
### 3. The Blockage — L5 final boss, tube→arena, in the dark
A fecalith the size of the lumen: compacted mass, colonised, embedded with antibody turrets
and **gas pockets**. The colon behind it has been distending for the whole level. Darkness is
the medium — **the gas pockets are the light source**, which is the whole look of the fight.
| phase | hp | the loop | window |
|---|---|---|---|
| **P1 The Face** | 100→60% | 5 armored plates (3 hits each). Turrets fire homing darts from between them. **Gas pockets bulge between plates: shoot one and it detonates, destroying adjacent plates outright — and hurting you inside ~15 units.** Optimal play = detonate 23 from max range. | continuous; the pockets are the timer |
| **P2 Fragmentation** | 60→30% | the mass breaks into 4 tumbling chunks, each with a turret, orbiting the arena. Gas vents fire jets that shove you into them. Core opens intermittently. | **2 s every 6 s** |
| **P3 The Push** | 30→0% | the colon begins the final peristaltic push: the arena contracts on a 5 s beat and drives the Blockage toward the exit **with you behind it**. **30 s doom timer** — if it plugs the rectum, you lose. Core exposed on each contraction. | **1.6 s every 5 s** |
- **Finale:** it blows, and the canal expels you both. (GDD: triumphant ejection.)
- **Telegraphs:** pockets *glow* — in the dark they are the brightest thing in the room, so
the risk telegraphs itself · plate cracks show damage state · a full-canal groan 1.5 s
before each contraction · turret pre-fire flash.
- **Failure pressure:** **hull attrition, inverting the Guardian.** Colon coat-drain is mild,
but gas blasts and darts hit the hull, and hull is pickup-only. Plus the P3 doom timer.
---
## → Lane B
Four things, in priority order. (1) is the only one that blocks you consuming my levels.
**1. Resolve fiction ids → archetypes.** Your pools are keyed by archetype; my levels name
enemies in fiction. One line in `combat/index.js`:
```js
import { getEnemy } from '../levels/index.js';
// ...inside the level:event handler, replacing enemies.spawn(ev.enemy, ...):
const spec = getEnemy(ev.enemy);
enemies.spawn(spec?.archetype ?? ev.enemy, { s: ..., theta: ..., rho: ... });
```
`?? ev.enemy` keeps bare archetype names working, and `enemies.js` has passthrough entries
for `floater`/`seeker`/`turret`, so **this is backward compatible and STUB_LEVEL still works
unchanged** — I deliberately did not rename the stub's enemies out from under your harness.
**2. `theta` may be an array** (one per instance) for wall-mounted groups — `theta: [1.05, 4.19]`
with `count: 2`. Your loop currently passes `ev.theta` whole to every instance. Needs:
```js
theta: Array.isArray(ev.theta) ? ev.theta[i % ev.theta.length] : ev.theta ?? null,
```
Until that lands I have kept STUB_LEVEL on scalar thetas so nothing breaks. L2 uses arrays.
**3. `spread`** (units along s a group occupies) — you currently hardcode `i * 3.5`. L2 tunes
this per encounter (60 = a wall of traffic in the hiatus; 200 = strung out along the chase):
```js
const spread = ev.spread ?? (3.5 * ((ev.count ?? 1) - 1));
const s = (ev.s ?? 0) + (ev.count > 1 ? (i / (ev.count - 1)) * spread : 0);
```
**4. Heads-up — your tuning.js header is stale.** It documents
`wallRho = radius waveAmp(0.9) skin(0.6) => ~7.0..10.0 playable disc`. Lane A has since
**measured the esophagus at `amp 1.4`** (+ `breathe 0.15`, and A's `waveMaxAt` subtracts
both). Your real envelope in a radius-10 tube is now **~6.35..9.35**, and your disc top speed
/ graze tuning was sized against the old number. Not my call to change — flagging it because
it moved under both of us. My selfcheck reads A's registry live, which is how I caught it.
**Balance ownership — agreed, and I removed my duplicate.** You own mechanics (hp, radius,
speed, damage, fire rate, **score**), keyed by archetype, in `balance.js`. I own composition
(which fiction, how many, where, in what order). My first cut of `enemies.js` restated
hp/size/speed/score; that was over-reach — an `hp: 2` here and an `hp: 24` there (against
`damage: 12`) is the same number twice. **`levels/enemies.js` is now a join table only**, and
`levels/index.js` reads your scores via `scoreFor()` to compute the score budget:
**L2 par.score 9000 of a 9400 kill budget = 95%**, computed from *your* numbers, not mine.
If you retune scores, my par re-checks itself on the next qa run.
**Hazards I placed that are yours:** `reflux_surge` (see `HAZARDS` in `levels/enemies.js` for
params). L2 has a non-lethal one at s 1700 and the lethal finale at s 3000. They are **inert
data until you implement them** — that is by design (charter: stub it in data first), not a bug.
Also: if `aortic_squeeze` contact ever becomes lethal rather than graze, tell me — L2's
teach-then-test ledger depends on it being survivable.
## → Lane A
**1. Biome conformance — done, no action needed.** I had authored `mouth`/`pharynx`/`cardia`/
`pylorus`; your registry rejected all four and **your design is right**. `biome` = look,
`segments[].name` = anatomy. **You do not need to add biome ids for C.** Your six cover the
campaign, and I made the constraint pay for itself (see §Schema v1 — the cardia keeping the
esophagus palette is why the acid sea gets to *reveal* itself).
**2. Your amp 0.9 → 1.4 landed mid-session and cost me 0.5 units of flyable radius.** No
complaint — your reasoning (B cannot surf a crest the player cannot see) is better than my
tube. The diaphragmatic hiatus went unflyable at base 6.5 and I widened it to **6.8** (free
radius 2.73, against my 2.5 floor). This is working as intended: my selfcheck reads
`wave.amp + wave.breathe` from your registry live, so **if you retune again, my levels fail qa
and I re-tune them.** Please keep changing it if it looks better — just expect a red qa.
**3. Request (round 2): per-segment `wave` override.** Anatomically the hiatus is a fixed
muscular ring held by the diaphragm crura — it does **not** have big peristaltic waves; the
esophagus above it does. Right now one amp applies to the whole biome, so the level's tightest
hole gets the level's biggest wave, which is both wrong and the thing squeezing my design. I
have **not** added it to the schema unilaterally. Proposed:
`segments[].wave: { amp: 0.6 }` overriding the biome, `waveMaxAt(s)` preferring the segment.
Your call — say yes in NOTES and I will spec it into v2 and re-tune.
**4. Do segments blend radius at boundaries?** L2 steps 12 → 9 (s 1100) and 10 → 6.8 (s 2600).
**C's design assumes a smooth blend over ~4060 units**, not a cliff — a hard radius step
would read as a wall and the constrictions are supposed to feel like the body narrowing, not
like a level seam. If your tube hard-steps, tell me and I will author explicit taper segments
instead. This is my biggest open dependency on you.
**5. Arena semantics — ratified in TECH v1 as I read your `arenaSpatial`:** a sphere of
`radius` centred on the centreline at `at`, spanning `[at-radius, at+radius]`, `modeAt`
`'arena'` inside. **Problem for L3:** the stomach body is not a sphere. `L3_stomach.json`
currently fakes it with one radius-180 sphere and that is flagged PROVISIONAL in the file. It
needs either a capsule/blob primitive or chained overlapping spheres. **Not urgent** (L3 is
round 3+), but it will decide how I author the acid sea, so I would like your read early.
**6. Hazards that are yours:** `aortic_squeeze` and `ring_gate` (params + rationale in
`HAZARDS`, `levels/enemies.js`). Both are inert data until you build them — fine by me.
The important one: **`aortic_squeeze` is directional (one-sided) on purpose.** It presses from
`theta` and the opposite arc stays clear. A symmetric squeeze would be an untimeable toll,
because in a flow-locked tube the player cannot stop and would eat all ~10 pulses of a
400-unit zone. Directional turns it into a positioning verb — and it is also what the aorta
actually does. `ring_gate` is the opposite: a **point**, therefore timeable, because the
player can modulate throttle on approach to arrive on an open beat. Two hazards, two verbs.
## → Lane D
- **Concept these three for L2** (round 2 hero-ish props, ART_BIBLE emissive law):
`bolus_chunk` (tumbling debris, amber `0xd98a3a`), `eosinophil_swarm` (small fast, red
`0xff4d3d`), `candida_bloom` (**wall-mounted white plaque colony**, `0xffd166`). Tints in
`levels/enemies.js` are advisory — yours to make beautiful.
- **Boss specs are ready above** (§Boss specs) — phases and silhouettes are stable enough to
concept the Pyloric Guardian (a muscular iris with 4 rim nodes) and the Tapeworm (segmented
body + a 4-sucker scolex). The Blockage is a mass of plates + glowing gas pockets.
- The `candida_bloom` is the interesting modelling problem: it is **flat against a curved
wall**, not a free-floating prop. Might be a decal/plane rather than a GLB.
- L2 needs your esophagus wall pack — already on your round-1 list, nothing extra from me.
## → Lane E
**1. Bus events I need** (F referees the names — proposing here):
- `hazard:warn { kind, s, eta }` — fires `warn` seconds before a hazard (my events carry
`warn`, default 2.5 s). Drives your HUD ping + `audio:cue`.
- `hazard:proximity { kind, distance }`**the reflux surge chases you from behind.** This is
a forward-facing game; without a rear indicator the finale is unfair rather than tense. This
is the single most important UI request I have. A rear-distance bar, a rising audio bed,
screen-edge acid creep — your call, but the player must *feel* 5 s of margin shrink.
**2. Gut-map** — L2 is **3600 units, 7 named segments, 10 checkpoints**. Segment `name`s are
authored for exactly this (`Upper Esophageal Sphincter`, `Aortic Constriction`,
`Diaphragmatic Hiatus`…). `listLevels()` in `levels/index.js` gives you
`{ id, name, tagline, length, par, skeleton }` for menus; `getLevel(id)` gives the rest.
**3. Telegraph budget:** L2 is flown at 1420 u/s (up to 28 in the finale). Anything lethal
telegraphs ≥ 2 s = **4056 units** upstream. My selfcheck warns if I author a lethal hazard
with `warn < 2`.
## → Lane F
**1. `?stub=1&lvl=<id>` does not compose.** `boot.js` `pickWorld()` hardcodes `STUB_LEVEL` on
the stub path and never reads `flags.lvl` — so the flag table's C-owned `?lvl=<id>` row does
not work with `?stub=1`, which is the combination my charter tells lanes to verify levels
with. Offered (your file, your call):
```js
async function pickWorld() {
const level = await loadLevel(flags.lvl).catch(() => null); // C's registry, when present
if (!flags.stub) {
try {
const mod = await import('./world/index.js');
const lv = level ?? STUB_LEVEL;
return mod.createWorld(lv, { rng: createRng(flags.seed ?? lv.seed) });
} catch (e) {
console.info('[boot] Lane A world not available, using stub —', e.message);
}
}
const lv = level ?? STUB_LEVEL; // ?stub=1&lvl=L2 => C's data
return createStubWorld(lv, { rng: createRng(flags.seed ?? lv.seed) });
}
```
Caveat so nobody expects too much: the stub is single-segment, so `?stub=1&lvl=L2_esophagus`
renders **beat 1 only**. Still useful (it proves the data path end-to-end); it is not a
flythrough. The real one is round 2 on A's world.
**2. `getLevel()` defaults to `L2_esophagus`** (`DEFAULT_LEVEL` in `levels/index.js`) — booting
with no `?lvl` gives you the vertical slice. Change it if you would rather it be L1.
**3. Bus table (TECH §Bus events)** — please referee `hazard:warn {kind,s,eta}` and
`hazard:proximity {kind,distance}` (rationale in §→ Lane E). Also note B is already emitting
`level:neutralize`, which isn't in the table yet.
**4. Schema v1 is ratified** in TECH.md §Level data schema — I rewrote that section only.
I also touched **exactly one const** in your `world_stub.js` (`STUB_LEVEL`, per ROUND1 §Lane
C.1) and left the geometry untouched, so your round-0 screenshot still stands.
**5. qa.sh, minor:** the `web/js/levels/*.json` loop asserts `id`/`seed`/`segments` on every
JSON in that directory. No collision today (my catalogue and difficulty table are `.js`), but
it means C can never put a non-level JSON there. Fine by me — noting it so it is a decision
rather than a trap.
---
## Open questions
1. **Segment radius blending (A)** — biggest one. If the tube hard-steps at segment
boundaries, L2's constrictions read as seams and I re-author with taper segments.
2. **Per-segment `wave` override (A)** — would let the hiatus be tight *and* calm, which is
both better design and better anatomy.
3. **Ship radius 0.9 (B)** — I took it from `flight/tuning.js` (`ship.radius`, the collision
sphere, vs a hull that "reads ~1.9 long, 1.2 wide"). My `MIN_CLEARANCE` of 2.5 is ~2.8× it.
If the effective collision radius is really the visual 1.2, my two spikes lose 0.3 and the
hiatus goes under the floor.
4. **Is `MIN_CLEARANCE = 2.5` right?** It is my judgement call, unvalidated by anything except
arithmetic. It is the number that decides whether L2's setpieces are exciting or miserable,
and only B's controller in a real tube can settle it. **If one thing from this round gets
playtested in round 2, make it the hiatus at s 26003000.**
5. **Par time 3:30 vs the GDD's old flat "610 min/level"** — I refined the GDD to per-level
targets rather than pad L2. A flow-locked tube bounds its own length: a timid run is 4:22
and there is no way to dawdle further. F to sign off.
## Next round (C)
- Fly L2 on A's world + B's controller. **Replace the sim's numbers with real ones**, and
finally answer whether the hiatus feels good.
- L1 encounter pass (tutorial beats, molar crush-cycles, saliva tides, the epiglottis exam) —
needs B's arena controller.
- L3 encounter pass + the acid sea's mucus-shallow lanes — needs A's arena decision (§→ A.5).
- `levels/bosses/pyloric_guardian.js` — the phase state machine, once B's enemy framework has
a boss hook. Spec is above and stable.
- Fold the boss specs into the GDD proper once F signs them off.
- Tune `difficulty.js` against real play (the table is authored, not validated).