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>
475 lines
29 KiB
Markdown
475 lines
29 KiB
Markdown
# 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 14–20 u/s. Par **3:30**; measured speedrun **2:47**, timid run **4:22**.
|
||
|
||
**Scale, stated honestly:** cross-sections are anatomically true (radius 10–12 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 0–400 — 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.6–1.4 ⇒ 12–28 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 2–3 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 ~40–60 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 14–20 u/s (up to 28 in the finale). Anything lethal
|
||
telegraphs ≥ 2 s = **40–56 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 2600–3000.**
|
||
5. **Par time 3:30 vs the GDD's old flat "6–10 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).
|