diff --git a/web/js/boot.js b/web/js/boot.js index 9671f58..31de9b5 100644 --- a/web/js/boot.js +++ b/web/js/boot.js @@ -104,6 +104,13 @@ if (player) { let paused = false; bus.on('ui:pause', (e) => { paused = !!e?.paused; }); +// The acid sea's level. world/acid.js exposes set(y, dt) and says "C's event pump drives this", +// so an `acid` level event names a target height and the sim eases toward it every step — a +// rising tide is a beat C can author, and because it is eased in step() (not frame()) it stays +// deterministic for stepped sims. No sea on this level => the event is inert. +let acidTarget = null; +bus.on('level:event', (ev) => { if (ev?.type === 'acid') acidTarget = ev.height ?? 0; }); + // --- level-event pump (owned by boot per round-2 ruling): emits C's events as the // player crosses their s. Each event fires ONCE per run — respawn does not rewind the pump // (no double-spawns, collected pickups stay collected); hazards re-arm via combat.reset(), @@ -232,6 +239,7 @@ function step(dt) { pumpTo(player.state.s); combat.update(dt); updateRun(dt); + if (acidTarget !== null && world.acid) world.acid.set(acidTarget, dt); // the tide eases world.update(dt, player.state.s); } else { updateCamera(dt); // ?fly=1 noclip / title drift diff --git a/web/js/flight/player.js b/web/js/flight/player.js index 0c5f659..a172568 100644 --- a/web/js/flight/player.js +++ b/web/js/flight/player.js @@ -209,7 +209,16 @@ export function createPlayer({ scene, world, bus, rng, flags = {}, assets = null // coat: biome ambient drain, then delayed regen st.sinceDamage += dt; - drain((biome.coatDrain ?? 0) * dt); + // pH: inside an acid sea's span, ambient drain is scaled by SUBMERSION (Lane A's own B-hook, + // world/acid.js depthAt > 0 = under the surface). Gated on the spec's s-span and not on + // depthAt alone, because depthAt returns a bare -1 outside the span and that is + // indistinguishable from "1 unit above the surface" — reading it as the latter would quietly + // scale the drain on every level that has no sea at all. + const sea = world.acid; + const inSea = !!sea && st.s >= sea.spec.from && st.s <= sea.spec.to; + st.submerged = inSea && sea.depthAt(_pos) > 0; + const phMult = !inSea ? 1 : (st.submerged ? T.coat.acidMult : T.coat.surfaceMult); + drain((biome.coatDrain ?? 0) * phMult * dt); if (st.sinceDamage > T.coat.regenDelay) st.coat = Math.min(T.coat.max, st.coat + T.coat.regen * dt); diff --git a/web/js/flight/tuning.js b/web/js/flight/tuning.js index f393378..2c48f60 100644 --- a/web/js/flight/tuning.js +++ b/web/js/flight/tuning.js @@ -121,6 +121,16 @@ export const TUNING = { regen: 8, // /s regenDelay: 2.5, // s after the last damage before regen resumes // ambient drain is the biome's: world.biomeAt(s).coatDrain (esophagus = 0.5/s) + // + // --- the pH mechanic (L3) --------------------------------------------------------- + // On a level with an acid sea (`level.acid`), the biome's drain is scaled by WHETHER YOU + // ARE IN IT — world.acid.depthAt(pos) > 0 means submerged. This is the GDD's "pH as + // environmental damage; mucus shallows are safe lanes", and it makes L3's promise literal: + // position is the resource. Against the 8/s regen, stomach 2.2 x 5 = 11/s bleeds even at + // rest and is punishing mid-fight (regen is off for 2.5s after any hit); above the surface + // 2.2 x 0.15 = 0.33/s is effectively safe air. A level with no acid plane is untouched. + acidMult: 5.0, + surfaceMult: 0.15, }, hull: { max: 100 }, diff --git a/web/js/levels/L3_stomach.json b/web/js/levels/L3_stomach.json index bf511e1..85edcf2 100644 --- a/web/js/levels/L3_stomach.json +++ b/web/js/levels/L3_stomach.json @@ -3,63 +3,75 @@ "id": "L3_stomach", "name": "Stomach", "tagline": "The acid sea", - "seed": 20260703, - "par": { "time": 540, "score": 15000, "samples": 3 }, + "seed": 20260702, + "par": { "time": 540, "score": 5500, "samples": 3 }, "next": "L4_small_intestine", "design": { "owner": "Lane C", - "status": "SKELETON (round 1) - shape only: biomes, lengths, arenas, boss placement, gate. Encounters, mucus-shallow lanes, churn cyclones and the pH field are round 2+. Do not read the (near-empty) events list as a design.", - "identity": "The coat level. Ambient pH drains the mucus coat continuously, so position IS the resource: mucus shallows along the greater curvature pause the drain, open acid does not. Ends on the Pyloric Guardian.", + "status": "AUTHORED (round 3). The skeleton waited on the pH field and on a boss framework; both now exist, so the encounters, the acid sea and the Guardian are built.", + "identity": "The coat level. Ambient pH drains the mucus coat continuously, so position IS the resource: the sea has a SURFACE, and flying above it is the safe lane. Ends on the Pyloric Guardian.", "anatomy": "Cardia -> fundus -> body (greater/lesser curvature) -> antrum -> pyloric antrum -> pylorus. Real order, real proportions.", "biome_note": "biome = tissue/look family (Lane A's registry), segments[].name = the anatomy. There is no 'cardia' or 'pylorus' biome and there should not be. The cardia deliberately keeps the ESOPHAGUS palette so that the acid sea REVEALS itself: you arrive still in teal, and the world turns orange the moment the fundus opens out. Spending a biome id on the cardia would throw that reveal away.", "scale": "Real stomach is ~25cm long, ~15cm wide when full; arena radius 180 units = 36cm. The one place the 1 unit : 1 mm fiction nearly holds - the stomach is genuinely a room.", - "open_question": "The body of the stomach is not a sphere and arenas[] currently only expresses spheres. Either A's arena primitive grows to a capsule/blob union, or L3 chains overlapping spheres. Flagged to Lane A in LANE_C_NOTES.md §-> Lane A - do not treat arenas[1] as final geometry." + "the_ph_mechanic": "`acid` (below) is Lane A's round-2 acid plane, finally load-bearing. B reads world.acid.depthAt(): submerged multiplies the biome's 2.2/s coat drain by 5 (11/s, which outruns the 8/s regen even at rest and is brutal mid-fight, since regen is off for 2.5s after any hit); above the surface it is multiplied by 0.15 and you are effectively safe. That is the GDD's 'mucus shallows are safe lanes' expressed as DEPTH rather than as wall-hugging - better, because it reads instantly (you can SEE the surface) and because it makes the room's vertical axis matter in the one level that is a room. Every collectible worth having is under it.", + "the_tide": "Three `acid` events raise the sea across the level (-14 -> -6 -> +2). The safe lane thins as you go, and by the antrum the surface is above the centreline, so the last stretch has no safe air at all. The player learns the mechanic in a room where the answer is easy and re-meets it where the answer is gone.", + "no_flora_here": "L3 is the only level with no commensals and no biofilm gate, and that is anatomically exact: stomach acid sterilises, which is precisely why H. pylori is famous for surviving it. Mechanically it means BIOME STANDING cannot move here - the reputation loop goes quiet for nine minutes and comes back in L4, which makes L4's bifido colonies read as relief rather than as furniture.", + "open_question": "The body of the stomach is not a sphere and arenas[] currently only expresses spheres. Either A's arena primitive grows to a capsule/blob union, or L3 chains overlapping spheres. Flagged to Lane A in LANE_C_NOTES.md §-> Lane A - do not treat arenas[1] as final geometry.", + "deferred": "Churn cyclones (GDD) are not here: a vortex that flings the player is a FORCE, and no hazard in the tree applies one. It wants the same Lane A/B work as the Guardian's arena churn and should land with it.", + "checkpoint_density": "Twenty-one checkpoints, because flow is 3 u/s through the fundus and body and the 30s death-cost law works out to ~100 units there. A level where the room is the hazard has to be generous about respawns, or the acid stops being a resource and becomes a punishment." + }, + "acid": { + "from": 700, + "to": 2200, + "height": -14, + "biome": "stomach", + "note": "The sea itself. Starts low (-14): in a 40-45 unit room that leaves most of the lumen as safe air, so the fundus can TEACH the mechanic before the body tests it. Raised by the `acid` events at s1150 / s1750." }, "segments": [ { "biome": "esophagus", "name": "Cardia", "length": 200, - "radius": { "base": 12, "wobble": 0.2 }, + "radius": { "base": 12, "wobble": 0.15 }, "curviness": 0.3, "flow": 8, - "note": "You arrive through the cardiac sphincter with the reflux surge still behind you. Short, tube-mode, decompression after L2's finale. Still wearing the esophagus palette (see design.biome_note) - the orange hits when the fundus opens, not before." + "note": "You arrive through the lower esophageal sphincter still wearing L2's teal, still moving at L2's pace. 200 units of the previous level, so that the fundus can take it away." }, { "biome": "stomach", "name": "Fundus", "length": 600, - "radius": { "base": 40, "wobble": 0.25 }, + "radius": { "base": 40, "wobble": 0.2 }, "curviness": 0.2, "flow": 3, - "note": "The gas dome at the top of the stomach. First taste of 6DOF-in-acid, low density: a breather that happens to be lethal if you loiter in the open." + "note": "The reveal: the tube stops being a tube. Radius triples, flow drops to a third, the palette goes amber and the acid surface is suddenly THERE below you. The whole segment is a teaching room - the sea is low, the ceiling is huge, and nothing here can kill you quickly." }, { "biome": "stomach", "name": "Body", "length": 900, - "radius": { "base": 45, "wobble": 0.3 }, - "curviness": 0.2, + "radius": { "base": 45, "wobble": 0.25 }, + "curviness": 0.25, "flow": 3, - "note": "THE ACID SEA - the level's centrepiece and the biggest room in the game so far. Churn cyclones, pepsin swarms, and the mucus shallows hugging the greater curvature." + "note": "The acid sea proper, and the biggest room in the game. The tide rises halfway through, so the safe air you learned to use in the fundus is being taken away while you are still in it." }, { "biome": "stomach", "name": "Antrum", "length": 500, - "radius": { "base": 25, "wobble": 0.25 }, - "curviness": 0.35, + "radius": { "base": 25, "wobble": 0.2 }, + "curviness": 0.3, "flow": 6, - "note": "The stomach starts squeezing down toward the exit. Room shrinks, current returns, pressure builds into the boss." + "note": "The room closes and the current picks up - the stomach beginning to squeeze its contents toward the pylorus, with you in them. The second tide event lands here and puts the surface ABOVE the centreline: no safe air, the coat just burns, and the only answer left is speed." }, { "biome": "stomach", "name": "Pyloric Antrum", "length": 300, "radius": { "base": 14, "wobble": 0.15 }, - "curviness": 0.25, + "curviness": 0.2, "flow": 10, - "note": "The Guardian's lair (see arenas[2]). The churn here is a current that pushes you at the walls for the whole fight." + "note": "Out of the acid at last and into a tight fast funnel - and the Guardian is in it. C's law puts the checkpoint at 2260, twenty units before the lair, so a wipe costs the fight and not the sea." } ], "arenas": [ @@ -69,8 +81,54 @@ ], "events": [ { "s": 20, "type": "checkpoint", "name": "Cardia" }, + { "s": 60, "type": "pickup", "kind": "nutrient_orb", "count": 3, "spread": 40, "note": "The last easy thing. An orb line in the cardia at L2's pace, 140 units before the room opens." }, + { "s": 120, "type": "spawn", "enemy": "bezoar_chunk", "count": 2, "spread": 50, "note": "Debris in the sphincter - the familiar floater, so the first 200 units of L3 ask nothing new. The level's whole opening is a setup for the reveal." }, + { "s": 180, "type": "checkpoint", "name": "Fundic Reveal", "note": "Placed 20 units before the fundus so the checkpoint fires while the player is still in teal. When they respawn here, they get the reveal again." }, + + { "s": 240, "type": "pickup", "kind": "mucin_glob", "count": 2, "spread": 40, "note": "TEACH THE COAT ECONOMY, immediately on arrival. The bar starts draining the moment the biome changes; the game hands you two refills in the same breath so the lesson is 'this is survivable and it is about position', not 'you are already dying'." }, + { "s": 280, "type": "checkpoint", "name": "Fundus" }, + { "s": 340, "type": "spawn", "enemy": "pepsin_swarm", "count": 3, "spread": 60, "note": "TEACH pepsin: the L2 seeker in its home biome, three of them, in the largest room they will ever have. Nothing else is happening - the point is to learn that a seeker in 6DOF can come from below." }, + { "s": 380, "type": "checkpoint", "name": "Fundic Dome" }, + { "s": 430, "type": "pickup", "kind": "biopsy_sample", "count": 1, "theta": 4.71, "rho": 0.55, "note": "SAMPLE 1/3, and the mechanic's first bill: theta 4.71 is straight DOWN, which in this level means under the surface. It costs a dip into 11/s drain in the safest room in the level - the cheapest possible version of the trade the whole stomach is about." }, + { "s": 480, "type": "checkpoint", "name": "Greater Curvature" }, + { "s": 540, "type": "spawn", "enemy": "bezoar_chunk", "count": 4, "spread": 110, "note": "Debris drifting on the sea. In a 40-unit room these are not a threat, they are TERRAIN - the first thing that makes the player pick a line through a room rather than around an obstacle." }, + { "s": 580, "type": "checkpoint", "name": "Fundic Floor" }, + { "s": 640, "type": "pickup", "kind": "antacid_ammo", "count": 2, "spread": 30, "note": "Torpedoes 1700 units before the Guardian, and the level never says why. Its P2 has armoured nodes that ONLY antacid strips, so a player who spent these on pepsin swarms meets that wall with an empty tube. The lesson is available and never taught - the same contract L2's s2300 ammo honours." }, + { "s": 680, "type": "checkpoint", "name": "Fundic Exit" }, + { "s": 740, "type": "spawn", "enemy": "hpylori_driller", "count": 1, "note": "TEACH H. pylori, alone, in the dome: the one microbe that lives here. It corkscrews so it is hard to pin, and it does NOT dissolve on contact - it grinds and keeps coming. First enemy in the game that must be OUT-DAMAGED rather than dodged, taught with a whole arena to retreat into." }, + { "s": 780, "type": "checkpoint", "name": "Incisura" }, + + { "s": 840, "type": "spawn", "enemy": "pepsin_swarm", "count": 5, "spread": 90 }, + { "s": 880, "type": "checkpoint", "name": "Body" }, + { "s": 940, "type": "pickup", "kind": "mucin_glob", "count": 2, "spread": 50 }, + { "s": 980, "type": "checkpoint", "name": "Gastric Body" }, + { "s": 1040, "type": "spawn", "enemy": "bezoar_chunk", "count": 5, "spread": 140 }, + { "s": 1080, "type": "checkpoint", "name": "The Sea" }, + { "s": 1150, "type": "acid", "height": -6, "note": "THE FIRST TIDE. The sea climbs 8 units while the player is in the middle of the biggest room in the game. Nothing announces it but the water - the safe air simply starts running out, and a player who has been skimming the surface finds themselves in it." }, + { "s": 1180, "type": "checkpoint", "name": "Rising" }, + { "s": 1240, "type": "spawn", "enemy": "hpylori_driller", "count": 2, "spread": 60, "note": "TEST H. pylori: two of them, in a rising sea, where retreating downward is no longer free. The drill and the tide are the same problem from two directions." }, + { "s": 1280, "type": "checkpoint", "name": "Mid Sea" }, + { "s": 1330, "type": "pickup", "kind": "biopsy_sample", "count": 1, "theta": 1.57, "rho": 0.8, "note": "SAMPLE 2/3, out at the wall of the widest room in the game. Not deep, but FAR - the cost is the round trip across open water with drillers in it, and every second of that trip is paid in coat." }, + { "s": 1380, "type": "checkpoint", "name": "Lesser Curvature" }, + { "s": 1440, "type": "spawn", "enemy": "pepsin_swarm", "count": 6, "spread": 100, "note": "The biggest swarm in the level, at the far end of the sea, where the player's coat is at its worst. Pepsin dissolves on contact - it is a damage race the player wins by shooting straight, and loses by drifting low." }, + { "s": 1480, "type": "checkpoint", "name": "Sea Exit" }, + { "s": 1540, "type": "pickup", "kind": "mucin_glob", "count": 2, "spread": 40, "note": "The last real top-up before the antrum. Take both." }, + { "s": 1580, "type": "checkpoint", "name": "Antral Approach" }, + { "s": 1640, "type": "spawn", "enemy": "bezoar_chunk", "count": 4, "spread": 100 }, + { "s": 1680, "type": "checkpoint", "name": "Pre-Antrum" }, + + { "s": 1750, "type": "acid", "height": 2, "note": "THE SECOND TIDE, and the level's cruellest idea: the surface goes ABOVE the centreline as the room narrows to 25. There is no safe air in the antrum. The mechanic the fundus taught is simply withdrawn, and the only remaining answer is to stop optimising position and RUN - which is exactly what the antrum's flow 6 and the pyloric antrum's flow 10 are there to let you do." }, + { "s": 1780, "type": "checkpoint", "name": "Antrum" }, + { "s": 1840, "type": "spawn", "enemy": "pepsin_swarm", "count": 4, "spread": 70 }, + { "s": 1900, "type": "pickup", "kind": "antacid_ammo", "count": 2, "spread": 25, "note": "Second and last ammo drop. A player who understood the first one is now carrying four torpedoes into a fight that needs two." }, + { "s": 1980, "type": "checkpoint", "name": "Antral Run" }, + { "s": 2040, "type": "spawn", "enemy": "hpylori_driller", "count": 2, "spread": 50, "note": "Drillers in a 25-unit tube with no safe depth: the grind, in a corridor, while the coat burns. The hardest 100 units in the level and the last thing before the run-in." }, + { "s": 2120, "type": "pickup", "kind": "biopsy_sample", "count": 1, "theta": 3.14, "rho": 0.9, "note": "SAMPLE 3/3, wall-pinned in the antrum at flow 6 with no safe air anywhere. The cruellest of the three: there is no clever position left, only the decision to spend coat you need for the Guardian." }, + { "s": 2180, "type": "checkpoint", "name": "Pyloric Antrum" }, + { "s": 2200, "type": "pickup", "kind": "mucin_glob", "count": 3, "spread": 30, "note": "Three globs on the racing line, out of the acid at last, sixty units before the boss checkpoint. This is the level apologising - and arming you for a fight whose entire design is a race against this bar." }, + { "s": 2260, "type": "checkpoint", "name": "Pyloric Antrum", "note": "Checkpoint before the boss - the law, and the reason a Guardian wipe costs seconds and not minutes." }, { "s": 2350, "type": "boss", "id": "pyloric_guardian", "note": "Full phase spec (phases / telegraphs / windows / failure pressure) in LANE_C_NOTES.md §Boss specs." }, - { "s": 2480, "type": "gate", "id": "pylorus", "to": "L4_small_intestine" } + { "s": 2480, "type": "gate", "id": "pylorus", "to": "L4_small_intestine", "note": "The pylorus opens and the stomach passes you on like chyme. You leave the acid behind; your coat does not forget it." } ] } diff --git a/web/js/levels/enemies.js b/web/js/levels/enemies.js index ea9aa5b..23f83cd 100644 --- a/web/js/levels/enemies.js +++ b/web/js/levels/enemies.js @@ -145,6 +145,14 @@ export const ENEMIES = Object.freeze({ 'pure payoff. A bright cyan gem, the special one of the commensal family.', }, + bezoar_chunk: { + archetype: 'floater', biome: 'stomach', tint: 0xd98a3a, wall: false, + note: 'A fragment of bezoar - the trapped, half-digested mass a real stomach can accrete ' + + 'around indigestible material. L3\'s traffic: in a 40-unit room these stop being ' + + 'obstacles and become TERRAIN, the thing that makes you choose a line through a room ' + + 'rather than around a hazard.', + }, + // ---- L4 small intestine roster (the villi maze) ------------------------------------ // All three map to archetypes Lane B already ships — L4 is authored from existing behaviour, // no new systems. The fiction is anatomically placed: Giardia colonises the duodenum/jejunum diff --git a/web/js/levels/index.js b/web/js/levels/index.js index cee2435..354af44 100644 --- a/web/js/levels/index.js +++ b/web/js/levels/index.js @@ -529,6 +529,10 @@ export async function validate(level, { expectId = null } = {}) { if (e.warn != null && e.warn < 2 && e.lethal !== false) W(`${at}: warn ${e.warn}s is under the 2s telegraph law for lethal hazards`); break; } + case 'acid': + // acid-sea level change (world/acid.js set()). `height` is a world-space Y. + if (typeof e.height !== 'number') E(`${at}: acid event needs a numeric height`); + break; case 'biofilm': // reputation cash-out (combat/index.js biofilmGate). Optional need (0..100)/allies/coat. if (e.need != null && (e.need < 0 || e.need > 100)) E(`${at}: biofilm need ${e.need} out of 0..100`);