[lane B+C] The Tapeworm and the churn: both deferrals closed, campaign ready for playtest
The two things I deferred out loud, built — and the second one turned out to be the primitive
the first was waiting on.
THE CHURN (the force primitive). Everything in hazards.js pushed the player RADIALLY; nothing
applied a tangential one, which is why both L3's cyclones and the Guardian's arena churn got
deferred. `churn` is now a hazard kind: a tangential impulse that REVERSES on a period, so it
cannot be leaned into, only re-read. Measured: 4.32 u/s of push, direction flipping.
- L3 s1100: cyclones spanning the sea, deliberately overlapping the first tide — the room
starts spinning you at the exact moment the safe air starts shrinking.
- The Pyloric Guardian carries its own (`def.churn` + per-phase `churnMul`), so C's spec is
finally honoured in full: "pushes you at the walls", x1.5 in P2, harder in P3. Measured
2.7 u/s on the player mid-fight. The boss.js header that said this was missing is corrected.
THE TAPEWORM (LANE_C_NOTES §2), the campaign's third pressure: a DISTANCE race. One new idea in
the schedule — `flee` — advances the boss's own s and drags its ring along at fixed offsets, so
6 proglottids string out 70 units down the tube ahead of you instead of sitting in a
cross-section. Fall 110 units behind and it is gone. Verified both ways: fall behind ->
"escaped" after 16.8s having seen only P1; keep up -> all three phases -> win. C's line was "a
chase you can lose without dying", and that is now literally the outcome table.
Placed at L4 s5460, immediately before the biofilm cash-out, so the chase and the reward land
together. L4's "no_miniboss_yet" note is replaced rather than left lying.
PRE-PLAYTEST SWEEP: 6/6 levels valid, all load with ZERO console errors, every level carries
exactly 3 biopsy samples, five bosses across the campaign (mast_cell / pyloric_guardian /
tapeworm / the_blockage / appendix_sovereign), 16,820 units of authored canal, and the chain
resolves L1 -> L2 -> L3 -> L4 -> L5 -> end with LS_appendix rejoining at L5.
Everything in the GDD's roster now exists except the numbers being right, and that is the part
only humans can tell us.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a3784c0371
commit
9f6e91fd8f
@ -14,9 +14,11 @@
|
||||
// - so this file is a state machine and nothing else. The second and third bosses should be
|
||||
// able to reuse it without touching enemies.js again.
|
||||
//
|
||||
// The arena churn (C: "pushes you at the walls", x1.5 in P2, reverses as P3's tell) is NOT here.
|
||||
// It wants a force on the player that only arena mode can express, and it is the one part of the
|
||||
// spec that is a feel change rather than a schedule. Noted in NOTES for the round that tunes it.
|
||||
// Two things beyond the schedule, both added once the primitives existed:
|
||||
// churn — a force on the player (C: "pushes you at the walls"). hazards.js owns the level-side
|
||||
// `churn` kind; a boss can carry its own via `def.churn` + per-phase `churnMul`.
|
||||
// flee — the boss's own s advances and its ring rides along, so the encounter becomes a
|
||||
// DISTANCE race you can lose without dying. That is the Tapeworm's whole pressure.
|
||||
|
||||
import { BALANCE as B } from './balance.js';
|
||||
|
||||
@ -42,10 +44,16 @@ const BOSSES = {
|
||||
telegraph: 2.0, // C: "a deep gulp 2 s before every open"
|
||||
node: 'mucosal_node',
|
||||
rho: 0.82, // the iris rim
|
||||
// C's arena churn, which the first Guardian commit deferred because nothing in the tree
|
||||
// applied a force to the player. hazards.js has a `churn` kind now, so this is the same
|
||||
// idea owned by the boss instead of the level: `out` shoves you AT THE WALLS (C's words),
|
||||
// `spin` drags you around the cross-section, and the whole thing reverses on `period` so it
|
||||
// cannot simply be leaned against. `churnMul` per phase — C asks for x1.5 in P2.
|
||||
churn: { out: 9, spin: 12, period: 8 },
|
||||
phases: [
|
||||
{ id: 1, open: 2.5, period: 8.0, nodes: 4, hp: 150, armored: 0, vents: 3, ventEvery: 3.0 },
|
||||
{ id: 2, open: 1.8, period: 5.0, nodes: 4, hp: 150, armored: 2, vents: 3, ventEvery: 2.4 },
|
||||
{ id: 3, open: 1.2, period: 4.0, nodes: 1, hp: 60, armored: 0, vents: 3, ventEvery: 2.0, doom: 25 },
|
||||
{ id: 2, open: 1.8, period: 5.0, nodes: 4, hp: 150, armored: 2, vents: 3, ventEvery: 2.4, churnMul: 1.5 },
|
||||
{ id: 3, open: 1.2, period: 4.0, nodes: 1, hp: 60, armored: 0, vents: 3, ventEvery: 2.0, doom: 25, churnMul: 1.8 },
|
||||
],
|
||||
},
|
||||
|
||||
@ -78,6 +86,28 @@ const BOSSES = {
|
||||
],
|
||||
},
|
||||
|
||||
// LANE_C_NOTES §2. The distance race — "it does not fight you, it RUNS, and the canal is its
|
||||
// body". The pressure is losing it, not dying to it, which needed one new idea in the schedule:
|
||||
// `flee`. The boss's own s advances every frame and its segments ride along at fixed offsets,
|
||||
// so the ring is strung out down the tube ahead of you. Fall `loseAt` units behind and it is
|
||||
// gone — the encounter ends, you live, and you have lost it. C: "a chase you can lose without
|
||||
// dying." Its speed sits just under a boosting player's, so keeping up is a throttle
|
||||
// commitment that costs you the room to dodge with.
|
||||
tapeworm: {
|
||||
telegraph: 1.2,
|
||||
node: 'mucosal_node', // proglottid weak points (the genital pore, which is real)
|
||||
rho: 0.55,
|
||||
flee: { speed: 17, loseAt: 110, spread: 14 },
|
||||
phases: [
|
||||
// P1 The Tail — 6 proglottids, each vulnerable only as its pore rolls into view.
|
||||
{ id: 1, open: 1.8, period: 4.0, nodes: 6, hp: 70, armored: 0, vents: 0, ventEvery: 9 },
|
||||
// P2 Constriction — it coils into 3 rotating rings; the weak points are the joints.
|
||||
{ id: 2, open: 1.5, period: 3.6, nodes: 3, hp: 110, armored: 1, vents: 2, ventEvery: 2.6 },
|
||||
// P3 The Scolex — it turns and charges. Suckers exposed only as it lunges.
|
||||
{ id: 3, open: 1.4, period: 4.0, nodes: 1, hp: 150, armored: 0, vents: 3, ventEvery: 1.9 },
|
||||
],
|
||||
},
|
||||
|
||||
// LANE_C_NOTES §5. The restraint boss, and the one that is not shaped like a boss at all —
|
||||
// hence `kind: 'sentinel'` and its own update path. There is no ring, no window and no hp bar
|
||||
// you are meant to empty: there is a cell on patrol, and a question. Damage is the FAILURE
|
||||
@ -138,6 +168,9 @@ export function createBoss({ bus, rng, player, enemies, flags = {}, getKarma = n
|
||||
const theta = (i / phase.nodes) * Math.PI * 2 + rand() * 0.5;
|
||||
const e = enemies.spawn(f.def.node, { s: f.s, theta, rho: f.def.rho });
|
||||
if (!e) continue;
|
||||
// A fleeing boss is strung out DOWN THE TUBE, not packed into one cross-section: each
|
||||
// segment keeps its own offset so the worm reads as a body you chase along, not a ring.
|
||||
if (f.def.flee) { e.dS = i * f.def.flee.spread; e.s = f.s + e.dS; }
|
||||
e.hp = phase.hp; // per-phase: rim nodes are tanky, the core is 5 hits
|
||||
e.invuln = true; // the iris starts shut
|
||||
e.armor = i < phase.armored; // C's P2: two of the four are armoured
|
||||
@ -333,6 +366,28 @@ export function createBoss({ bus, rng, player, enemies, flags = {}, getKarma = n
|
||||
if (f.doomT <= 0) { player?.kill?.('sealed'); finish(f, 'sealed'); return; }
|
||||
}
|
||||
|
||||
const ps = player?.state;
|
||||
|
||||
// CHURN — a boss that stirs its own room. `out` presses you toward the wall, `spin` drags you
|
||||
// around it, and both reverse on `period` so the answer keeps moving. Per-phase `churnMul`.
|
||||
if (f.def.churn && ps?.alive) {
|
||||
const c = f.def.churn, mul = f.phase.churnMul ?? 1;
|
||||
f.churnT = (f.churnT ?? 0) + dt;
|
||||
const dir = Math.sin((f.churnT * Math.PI * 2) / c.period) >= 0 ? 1 : -1;
|
||||
const rho = Math.hypot(ps.x, ps.y) || 1e-5;
|
||||
const ux = ps.x / rho, uy = ps.y / rho; // outward radial
|
||||
player.shove((ux * c.out + -uy * c.spin * dir) * mul * dt,
|
||||
(uy * c.out + ux * c.spin * dir) * mul * dt);
|
||||
}
|
||||
|
||||
// FLEE — the Tapeworm. It runs, its segments ride along at their spawn offsets, and losing it
|
||||
// is a real outcome: you live, the encounter ends, and it got away with the level's miniboss.
|
||||
if (f.def.flee && ps) {
|
||||
f.s += f.def.flee.speed * dt;
|
||||
for (const e of f.nodes) if (e.alive) e.s = f.s + (e.dS ?? 0);
|
||||
if (f.s - ps.s > f.def.flee.loseAt) { finish(f, 'escaped'); return; }
|
||||
}
|
||||
|
||||
// The cycle. `open` is the whole fight: nodes are only shootable while the sphincter relaxes.
|
||||
const openFor = f.phase.open * windowScale;
|
||||
f.t += dt;
|
||||
|
||||
@ -126,6 +126,7 @@ export function createHazards({ scene, world, bus, rng, player }) {
|
||||
if (h.kind === 'reflux_surge') updateSurge(h, dt, ps);
|
||||
else if (h.kind === 'aortic_squeeze') updateSqueeze(h, dt, ps);
|
||||
else if (h.kind === 'ring_gate') updateGate(h, dt, ps);
|
||||
else if (h.kind === 'churn') updateChurn(h, dt, ps);
|
||||
}
|
||||
|
||||
render();
|
||||
@ -190,6 +191,29 @@ export function createHazards({ scene, world, bus, rng, player }) {
|
||||
player.shove(-(ps.x / rho) * H.squeeze.shove, -(ps.y / rho) * H.squeeze.shove);
|
||||
}
|
||||
|
||||
// --- churn: a VORTEX, and the force primitive the tree was missing ------------------------
|
||||
// GDD L3 "churn cyclones (environmental vortex, flings you)". Everything else in hazards.js
|
||||
// pushes you RADIALLY — toward or away from a wall. A cyclone is the other axis: a TANGENTIAL
|
||||
// impulse that spins you around the cross-section, so holding a line costs you input rather
|
||||
// than coat. That is why it deserved a kind of its own instead of another squeeze.
|
||||
//
|
||||
// It REVERSES on `period`, which is not decoration: a force you can lean into permanently is
|
||||
// just a changed centre, while one that flips is a thing you have to keep reading. (It is also
|
||||
// the tell C's Blockage spec asks for — "the churn reverses for 1.2s" — available to any boss
|
||||
// that wants it, see boss.js.)
|
||||
function updateChurn(h, dt, ps) {
|
||||
const ev = h.ev;
|
||||
if (ps.s < h.s || ps.s > h.s + (ev.span ?? 300)) return;
|
||||
h.phase = (h.phase ?? 0) + dt;
|
||||
const dir = Math.sin((h.phase * Math.PI * 2) / (ev.period ?? 6)) >= 0 ? 1 : -1;
|
||||
const rho = Math.hypot(ps.x, ps.y);
|
||||
if (rho < 1e-4) return;
|
||||
// tangential unit vector in the disc: perpendicular to the radial direction
|
||||
const tx = -ps.y / rho, ty = ps.x / rho;
|
||||
const k = (ev.strength ?? 26) * dir * dt;
|
||||
player.shove(tx * k, ty * k);
|
||||
}
|
||||
|
||||
// --- ring gate: a POINT, therefore timeable ----------------------------------------------
|
||||
const gateOpen = (h, t) => ((t % (h.ev.period ?? 3.0)) < (h.ev.open ?? 1.5));
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"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.",
|
||||
"the_churn": "Cyclones at s1100, spanning the sea. hazards.js now has a `churn` kind - a TANGENTIAL force that reverses on a period - so the GDD's vortex is real rather than deferred. It overlaps the first tide deliberately: the room starts spinning you at the exact moment the safe air starts shrinking.",
|
||||
"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": {
|
||||
@ -104,6 +104,7 @@
|
||||
{ "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": 1100, "type": "hazard", "kind": "churn", "span": 420, "strength": 24, "period": 7, "warn": 2.5, "note": "THE CHURN CYCLONES (GDD), finally buildable now that hazards.js has a force. A tangential vortex across the widest part of the sea that REVERSES every 3.5s: it cannot be leaned into, only re-read, and it spins you toward the surface or away from it while the tide is rising. The stomach mixing its contents, with you as contents." },
|
||||
{ "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." },
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"curve": "breather -> teach -> the maze -> pressure -> the lymphoid gauntlet -> SPIKE -> cash-out + valve. No single spike as sharp as L2's hiatus: L4's difficulty is CUMULATIVE. It wins by attrition, which is what an endurance level is for.",
|
||||
"speed_identity": "flow 8-10 vs L2's 12-20. This is deliberate and it is the whole level: at 9 u/s you have time to look, and the fog (0.050, densest in the game outside the colon) plus curviness 0.65-0.70 means looking is exactly what you must do. L2 punishes bad reflexes; L4 punishes bad reading.",
|
||||
"teach_then_test": "chyme (b1 solo -> b3 fields in the maze) / giardia (b2 solo in open water -> b6 three at once in the tightest tube) / segmentation ring (b2 open 70% -> b4 53% -> b6 43%) / peyers turret (b2 one on a clear sightline -> b5 three arcs at once, twice) / neutrophil (b2 three in the open -> b7 five on the run-in).",
|
||||
"no_miniboss_yet": "The GDD's Tapeworm chase belongs at this level's end. There is no boss framework in the tree yet (the Pyloric Guardian is the first one being built), so L4 currently ends at the ileocecal valve gate with the biofilm cash-out as its finale. When the boss lane lands, the Tapeworm goes in at ~5500 and this note comes out."
|
||||
"the_miniboss": "The Tapeworm runs at s5460, immediately before the biofilm cash-out - so the chase and the reward land together: catch it and the biome opens for you sixty units later. It is the campaign's only encounter you can LOSE without dying (it escapes at 110 units of gap), which is C's third pressure and the reason L4 does not simply end on a gate."
|
||||
},
|
||||
"segments": [
|
||||
{
|
||||
@ -146,6 +146,7 @@
|
||||
{ "s": 5340, "type": "checkpoint", "name": "Ileocecal Run-in" },
|
||||
|
||||
{ "s": 5420, "type": "spawn", "enemy": "neutrophil_hunter", "count": 5, "spread": 90, "note": "The immune system's last word before the valve." },
|
||||
{ "s": 5460, "type": "boss", "id": "tapeworm", "note": "THE MINIBOSS, at the s this level's own notes reserved for it. Taenia solium, metres of it, running ahead of you down the terminal ileum - it does not fight, it RUNS, and if it gets 110 units clear it is gone and you have lost it without ever being in danger. C's third pressure: a chase you can lose without dying. Spec: LANE_C_NOTES §Boss specs 2." },
|
||||
{ "s": 5500, "type": "biofilm", "need": 60, "allies": 3, "coat": 40, "note": "THE CASH-OUT. Ten minutes of deciding whether to spray the cyan resolves here: high BIOME STANDING and the ileocecal biofilm parts - three phage escorts and a coat refill for the run at the valve. Low, and it stays sealed and you finish alone. This is the reputation loop's first appearance at the END of a level rather than the middle of one." },
|
||||
{ "s": 5600, "type": "checkpoint", "name": "Ileocecal Approach" },
|
||||
{ "s": 5700, "type": "spawn", "enemy": "chyme_clump", "count": 3, "spread": 80 },
|
||||
|
||||
@ -212,5 +212,6 @@ export const PICKUPS = Object.freeze({
|
||||
export const HAZARDS = Object.freeze({
|
||||
reflux_surge: { needs: ['speed'], optional: ['from', 'span', 'lethal', 'neutralizable'], owner: 'B', note: 'Acid wall chasing from behind at `speed`. `from` = spawn offset relative to the trigger s (negative = behind the player). `lethal: false` => the non-lethal teaching version. `neutralizable` => B\'s antacid torpedo stalls it (balance.js: neutralizeDuration 10s).' },
|
||||
aortic_squeeze: { needs: ['period', 'amplitude', 'theta'], optional: ['span'], owner: 'A', note: 'Zone hazard: the aortic arch pulses against ONE SIDE of the tube. The wall at `theta` bulges inward by `amplitude` * radius on `period`; the opposite arc stays clear and is the answer. Directional on purpose — a zone that sealed symmetrically would be an untimeable toll in a flow-locked tube (you cannot stop). Wall contact = graze damage, not death.' },
|
||||
churn: { needs: ['span'], optional: ['strength', 'period', 'warn'], owner: 'B', note: 'Zone hazard: a churn cyclone (GDD L3). Applies a TANGENTIAL impulse that spins the ship around the cross-section and REVERSES every `period`/2 seconds, so it cannot be leaned into - it has to be re-read. The one force primitive in the tree; everything else pushes radially. `strength` is u/s of shove per second (default 26).' },
|
||||
ring_gate: { needs: ['period', 'open'], optional: [], owner: 'A', note: 'Point hazard: a peristaltic ring seals the lumen, opening for `open` seconds every `period`. Timeable precisely BECAUSE it is a point — the player modulates throttle on approach to arrive on an open beat.' },
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user