L1 has been a 2-event skeleton since round 1, waiting (in its own status field) on "Lane B's arena-mode controller". That exists now, so C's round2_plan is built as written: molar crush-cycles, the saliva tide, amylase droplets, the three samples exactly where C put them (behind the molars / under the tongue / a tonsillar crypt), and the epiglottis exam. 24 events. The teaching order is the level: move (orbs, no current) -> LOOK (the arena opens and you can stop, which no other level allows) -> shoot (one droplet, then two) -> coat (the molar ring grazes you, mucin is right there) -> RESTRAINT -> flow returns (the tide, the funnel) -> the exam. Every verb alone, in the calmest room in the game, before anything is asked of it. Two hazards are deliberately re-used rather than invented: the molar crush IS a ring_gate and the saliva tide IS a non-lethal surge, so L2's gates and its reflux finale become callbacks instead of new ideas. THE MAST CELL (LANE_C_NOTES §Boss specs 5) sits at the exact midpoint. A sentinel immune cell: cyan, no attack, no chase, inert aura (auraDps 0 — it shares the floater's drift branch and its aura does literally nothing). Fly past and the encounter just ends. Shoot it and it degranulates — droplets close in from the walls (which is how "the cavity swells shut" is expressed, because a runtime tube radius is not in Lane A's contract), the coat bleeds, and past half health the airway closes on a 25s timer. You always get through. The only question is what you get through. It needed a new boss `kind: 'sentinel'` with its own update path, because forcing a thing with no rings and no windows through the phase machine would have been worse than a branch. Damage is the failure state, so every number describes a consequence rather than a fight. Verified, all three player choices: SPARE -> "spared", coat untouched at 100, karma +25, score 0. SHOOT -> "degranulated", coat 100->56. KILL -> "anaphylaxis-survived", coat 100->6. Score is 0 in every case: shooting it buys literally nothing, so no medal can ever reward the mistake. Two bugs the testing paid for: `def.phases[0]` crashed on a boss that has no phases, and the first degranulation drain (5/s) was UNDER the player's 8/s coat regen — measured coat going 80 -> 100 during a histamine cascade, i.e. the punishment was healing you. Now 14 and 22, which are net -6/s and -14/s against regen. L1 is no longer a skeleton: its checkpoint-gap and zero-samples warnings are gone, and the campaign now opens on a real tutorial instead of an empty mouth with a gate at the end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
195 lines
12 KiB
JavaScript
195 lines
12 KiB
JavaScript
// combat/balance.js (Lane B) — every combat number. Sibling of flight/tuning.js: that file
|
||
// decides how ENDO-1 handles, this one decides what the fight is worth.
|
||
//
|
||
// Sized against the stub esophagus (flow 14 u/s, playable disc radius ~7..10 u) and against
|
||
// the player's own numbers: disc top speed 13 u/s, coat 100 with 8/s regen after 2.5 s.
|
||
|
||
export const BALANCE = {
|
||
// Lysozyme cannon: infinite ammo, heat-limited (GDD). Heat is the whole design — the gun
|
||
// is free, so the cost has to be time. Sustained fire nets +25 heat/s (11 shots/s × 5.5,
|
||
// less 30/s cooling) => ~4 s of held fire before lockout, then ~2 s to recover to resumeAt.
|
||
// Burst and you never see the meter; hold and you're disarmed exactly when it's busiest.
|
||
cannon: {
|
||
rof: 11, // shots/s
|
||
speed: 95, // u/s in world space (closing speed ~75 with player flow)
|
||
life: 1.15, // s => ~109 u range, comfortably past the fog
|
||
damage: 12,
|
||
radius: 0.35,
|
||
heatPerShot: 5.5,
|
||
coolRate: 30, // /s
|
||
overheatAt: 100,
|
||
resumeAt: 40, // hysteresis — no stutter-firing on the edge of lockout
|
||
spread: 0.004, // rad, seeded — a hair of life, not a handicap
|
||
pool: 96, // rof × life = ~13 in flight; 96 covers spikes with room
|
||
},
|
||
|
||
// Antacid torpedo: the dual-use secondary (GDD). Also neutralizes an acid zone for ~10 s
|
||
// via the level:neutralize bus event — A/C consume it. Ammo-limited, pickup-fed (round 2).
|
||
torpedo: {
|
||
rof: 0.8, speed: 42, life: 3.2, damage: 45, splash: 9, radius: 0.7,
|
||
ammo: 6, ammoMax: 6,
|
||
neutralizeRadius: 14, neutralizeDuration: 10,
|
||
pool: 8,
|
||
},
|
||
|
||
enemies: {
|
||
// amylase droplet — slow floater with a corrosive contact aura. A soft obstacle: it
|
||
// doesn't chase, it taxes the lane it sits in. Aura ticks discretely (see enemies.js).
|
||
floater: { hp: 24, radius: 2.2, score: 100, drift: 1.6, bob: 0.5,
|
||
auraRadius: 4.2, auraDps: 18, auraTick: 0.2, pool: 24 },
|
||
|
||
// pepsin — small fast seeker, dissolves on contact. Chases in spline space (s,x,y), so
|
||
// a pursuit costs three floats. speed 19 > player flow 14: it WILL catch you from
|
||
// behind, and the answer is to shoot it or boost, not to outrun it at throttle 1.4.
|
||
seeker: { hp: 14, radius: 1.0, score: 150, speed: 19, turn: 2.4, contact: 16,
|
||
aggroS: 60, pool: 32 },
|
||
|
||
// antibody turret (Peyer's patch) — wall-mounted, fires homing darts. Dart turn radius
|
||
// is speed/turn = 24/1.8 = 13.3 u, wider than the tube itself: it can never corner you,
|
||
// only punish a straight line. Dodgeable by design (charter).
|
||
turret: { hp: 40, radius: 1.6, score: 250, fireEvery: 1.9, rangeS: 55,
|
||
dartSpeed: 24, dartTurn: 1.8, dartLife: 4.5, dartDamage: 14, pool: 12 },
|
||
|
||
// flora — the first FRIEND. A commensal (Lactobacillus &c) that drifts like a floater but
|
||
// HELPS: brush its aura and it sheds `coat` mucus per tick instead of taking it. score 0 and
|
||
// no combo (enemies.js kill()): it sits in the enemy pool so it is shootable, but shooting
|
||
// it is the reputation trap, never a reward. `coat` kept small + no parking in a flow tube =
|
||
// a trickle, not a fountain. hp low: a stray pellet shouldn't wipe a colony you meant to spare.
|
||
// No `gain` => flora:tended defaults to the small +5 standing in combat/index.js.
|
||
flora: { hp: 18, radius: 2.0, score: 0, drift: 1.2, bob: 0.4,
|
||
auraRadius: 3.6, coat: 6, auraTick: 0.3, pool: 24 },
|
||
|
||
// mucus_grazer (Akkermansia) — a commensal that eats your mucin. Brushing it COSTS `coatCost`
|
||
// per tick but signals gut health hardest (`gain` 12 vs flora's 5): the "spend coat for
|
||
// faster allies" trade. Shares flora's drift+aura branch, sign-flipped by data. score 0,
|
||
// shootable (harming it is still flora:harmed). The clever risk/reward of the reputation loop.
|
||
mucus_grazer: { hp: 20, radius: 2.2, score: 0, drift: 1.0, bob: 0.35,
|
||
auraRadius: 4.0, coatCost: 4, gain: 12, auraTick: 0.3, pool: 16 },
|
||
|
||
// spore_pod — a foe budding-yeast/Candida cluster. Mechanically the floater (drift + corrosive
|
||
// aura) with its OWN pool so its blobby mesh reads distinct from tumbling food debris. The
|
||
// first entity authored to be skinned by a Trellis mesh (assets/models/spore_pod.glb).
|
||
spore_pod: { hp: 30, radius: 2.4, score: 120, drift: 1.4, bob: 0.5,
|
||
auraRadius: 4.4, auraDps: 20, auraTick: 0.2, pool: 16 },
|
||
|
||
// phage — the ALLY (bacteriophage). A friendly seeker that hunts the nearest FOE and rams
|
||
// it (`punch`), bursting after `bursts` hits (a phage lyses its host and dies). FAST (26 >
|
||
// every foe) so it actually catches things; FRAGILE (hp 8) so a stray pellet or a foe pops
|
||
// it. score 0 — an ally never scores. Budded by high BIOME STANDING (combat/index.js), the
|
||
// payoff for tending flora. escort* = where it loiters when no foe is in `huntS` range.
|
||
phage: { hp: 8, radius: 1.1, score: 0, speed: 26, turn: 3.2, punch: 20, bursts: 2,
|
||
huntS: 70, escortLead: 10, escortR: 5, pool: 8 },
|
||
|
||
// cdiff (C. difficile) — a foe floater (drift + corrosive aura) whose corpse ISN'T clean:
|
||
// on death it leaves a spore_cloud (enemies.js kill()). Killing it costs you the ground it
|
||
// died on. A colon (L5) enemy; registered now, placed when L5 gets its encounter pass.
|
||
cdiff: { hp: 26, radius: 2.3, score: 140, drift: 1.2, bob: 0.45,
|
||
auraRadius: 4.0, auraDps: 14, auraTick: 0.2, pool: 12 },
|
||
|
||
// spore_cloud — C. diff's parting gift. A STATIC corrosive zone (acid-green, readability law)
|
||
// that damages on contact and expires after `life`s. Not scored (foe:false), but shootable:
|
||
// low hp so you CAN clear it with the cannon — spend heat to disperse it, or fly around.
|
||
spore_cloud: { hp: 14, radius: 3.2, score: 0, auraRadius: 4.5, auraDps: 22, auraTick: 0.25,
|
||
life: 5, pool: 12 },
|
||
|
||
// latcher (Giardia) — a foe that CHASES like a seeker, then CLAMPS onto your hull (its
|
||
// ventral suction disc) and drains `drain` coat/tick while it rides. The new verb is
|
||
// "shake it off": a boost's i-frames throw it, else it lets go after `attachMax`s, then
|
||
// can't re-grab for `reattachCd`s. speed 20 > flow so it catches you; the counterplay is
|
||
// timing a boost, not out-running it.
|
||
latcher: { hp: 16, radius: 1.4, score: 160, speed: 20, turn: 2.6, aggroS: 55,
|
||
drain: 8, drainTick: 0.3, attachMax: 3.5, reattachCd: 1.5, pool: 12 },
|
||
|
||
// virion (norovirus) — the ARITHMETIC swarm: tiny, fast, individually trivial (hp 6, one
|
||
// pellet), lethal only in numbers. Shares the seeker behaviour (chase + dissolve on contact)
|
||
// but its own tiny-radius pool of 40, so a norovirus cloud reads as a swarm, not a squad.
|
||
virion: { hp: 6, radius: 0.7, score: 60, speed: 22, turn: 2.8, contact: 8, aggroS: 55, pool: 40 },
|
||
|
||
// driller (H. pylori) — the one microbe that thrives in the stomach. A TANKY corkscrewer:
|
||
// chases like a seeker but spirals (spin/corkscrew) so it's hard to pin, and it does NOT
|
||
// dissolve on contact — it grinds you and keeps coming. hp 50 = you must commit real fire.
|
||
driller: { hp: 50, radius: 1.6, score: 220, speed: 16, turn: 2.0, contact: 18, aggroS: 60,
|
||
spin: 4, corkscrew: 6, pool: 8 },
|
||
|
||
// mucosal_node — the Pyloric Guardian's rim nodes (LANE_C_NOTES §Boss specs). Fixed: no
|
||
// movement branch in enemies.update, which is exactly right for a node bolted to an iris.
|
||
// It is combat/boss.js that makes them a boss — the phase cycle flips `invuln` so they are
|
||
// only shootable while the sphincter relaxes. This `hp` is only the default — boss.js sets
|
||
// it per phase (150 for the rim rings, 60 for P3's core, which is C's "x5 hits").
|
||
// pool 12, not 9: a fight spawns 4 + 4 + 1 rings, and the retired ones only leave the pool
|
||
// when enemies.reap() next runs. Sizing to the exact total would make phase 3 depend on
|
||
// reap timing, and a boss that silently spawns no core is the worst bug in the file.
|
||
mucosal_node: { hp: 150, radius: 2.6, score: 400, pool: 12 },
|
||
|
||
// mast_cell — L1's sentinel (LANE_C_NOTES §Boss specs 5). A patrolling immune cell with NO
|
||
// attack: auraDps 0 means it shares the floater's drift branch and its aura does nothing at
|
||
// all. hp 400 is not a health bar, it is a THRESHOLD — high enough that killing it takes
|
||
// deliberate sustained fire, so the cascade is never something a player did by accident with
|
||
// one stray pellet. score 0 and foe:false: there is no reward here, in any sense.
|
||
mast_cell: { hp: 400, radius: 3.2, score: 0, drift: 1.1, bob: 0.3,
|
||
auraRadius: 4.0, auraDps: 0, auraTick: 0.5, pool: 2 },
|
||
|
||
// beacon (Faecalibacterium) — the reward-state commensal. The most anti-inflammatory bug in
|
||
// you; a screen of these IS a healthy gut. Shares the commensal branch: heals AND gives the
|
||
// biggest standing (gain 25) with no coat cost. Rare (pool 10) — the pure payoff node.
|
||
beacon: { hp: 22, radius: 2.0, score: 0, drift: 0.9, bob: 0.5,
|
||
auraRadius: 4.2, coat: 8, gain: 25, auraTick: 0.3, pool: 10 },
|
||
},
|
||
|
||
darts: { pool: 48, radius: 0.45 },
|
||
|
||
// --- hazards (round 2) -------------------------------------------------------------
|
||
// C authors WHERE and HOW MUCH in the level JSON (speed/period/amplitude/span/warn); these
|
||
// are the mechanical constants those params drive. C's params always win where they overlap.
|
||
hazards: {
|
||
// reflux_surge — the rear chaser. C's finale: speed 21 vs flow 20, spawning 40 u behind
|
||
// and chasing 600 u. Lethal on contact by C's authoring (`lethal: true`); the s=1700
|
||
// teaching burp is `lethal: false` and only grazes. Antacid stalls it (`neutralizable`).
|
||
surge: {
|
||
graze: 26, // coat damage/tick for the NON-lethal teaching version
|
||
grazeTick: 0.35, // s between graze ticks while inside it
|
||
stallFactor: 0.0, // speed multiplier while neutralised — 0 = fully parked.
|
||
// C's design leans on this: "fired backward it stalls the surge
|
||
// ~10 s" (their finale maths assumes a real stop, not a slow).
|
||
proximityRange: 220, // u within which we emit hazard:proximity (E's rear indicator)
|
||
catchRadius: 3.0, // u of s-overlap that counts as being caught
|
||
},
|
||
// aortic_squeeze — DIRECTIONAL by design (C): the arch presses from `theta` only and the
|
||
// opposite arc is the answer. Non-lethal, and C's teach-then-test ledger depends on that
|
||
// staying true (LANE_C_NOTES: "if B makes squeeze contact lethal, this breaks — tell me").
|
||
squeeze: {
|
||
graze: 14, // coat damage per contact tick
|
||
grazeTick: 0.3,
|
||
shove: 9, // u/s pushed off the bulging wall — the hazard MOVES you
|
||
arc: 1.9, // rad of the cross-section the bulge occupies (~110°, one side)
|
||
},
|
||
// ring_gate — a POINT, therefore timeable: the player modulates throttle to arrive on an
|
||
// open beat. Closed contact is expensive but never lethal (C: it is a skill check, and
|
||
// a flow-locked player cannot stop to wait).
|
||
gate: {
|
||
hit: 30, // coat damage for arriving on a closed beat
|
||
shove: 6, // u/s shove toward the centreline (the iris squeezes you through)
|
||
irisMin: 0.12, // fraction of the lumen still open when fully "sealed" — never a
|
||
// true wall: a flow-locked player physically cannot stop, so a
|
||
// real seal would be an unavoidable toll rather than a skill check
|
||
},
|
||
},
|
||
|
||
// --- pickups (round 2) -------------------------------------------------------------
|
||
// C owns placement + counts (level JSON); B owns what each one is WORTH. C's round-2 task
|
||
// is to hand over an economy — until it lands these are B's proposal, documented in NOTES.
|
||
pickups: {
|
||
radius: 1.5, // generous collect radius: pickups are a reward, not a dexterity test
|
||
spin: 1.4, // rad/s idle rotation
|
||
nutrient_orb: { score: 50, boost: 0.6, note: 'score + boost recharge (shaves cooldown)' },
|
||
mucin_glob: { coat: 35 },
|
||
b12_cell: { hull: 30 },
|
||
antacid_ammo: { ammo: 1 },
|
||
biopsy_sample: { score: 500, sample: 1 },
|
||
},
|
||
|
||
// Chained kills feed the combo meter (GDD: score × style multiplier; E's music layer
|
||
// listens). Window is generous enough to cross a fold, tight enough to mean something.
|
||
combo: { window: 2.5 },
|
||
};
|