guts/web/js/combat/balance.js
jing 4ed3433e55 [lane B] Round 2 WIP (session cut off): surf speed-lock, hazards + pickups modules, rho-fraction spawns
Landed before the cut: surf as a speed-lock onto world.crestSpeed(s) with surf.authority
(ruling #1) — riding beats throttle and holding a crest is throttle discipline; tuning.js
header corrected to the real wallRho frame (C's flag); enemies.js reads rho as a FRACTION
of safe radius (C's placement law); balance.js hazard + pickup constants; NEW hazards.js
(reflux_surge / aortic_squeeze / ring_gate, self-scheduling telegraphs, reset(fromS) built
for respawn) and NEW pickups.js (all five kinds, shape-coded).

Cut off before: wiring — nothing imports hazards/pickups yet; the fiction-id -> archetype
resolve (round-2 task #1) does NOT exist despite the enemies.js comment saying index.js
does it; player.kill/shove/refill are called but were never added; checkpoint/respawn and
gate -> level:complete not started; NOTES unwritten. F lands the glue next commit.
Committed by F to protect the shared tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:26:15 +10:00

110 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 },
},
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 },
};