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

29 KiB
Raw Blame History

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 --selfcheck3/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 yetboot.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:

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:

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):

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 names 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):

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).