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>
109 lines
7.7 KiB
JavaScript
109 lines
7.7 KiB
JavaScript
// levels/enemies.js (Lane C) — the enemy CATALOGUE: fiction id → archetype + skin.
|
|
//
|
|
// WHY THIS EXISTS: level JSONs name enemies in fiction ("bolus_chunk"), not in mechanics
|
|
// ("floater"). This is the join table between the two, and nothing more.
|
|
//
|
|
// OWNERSHIP (settled with Lane B's round-1 work, see LANE_C_NOTES.md §→ Lane B):
|
|
// Lane B owns MECHANICS — hp, radius, speed, damage, fire rate, score. They live in
|
|
// combat/balance.js, tuned against B's cannon and B's feel numbers, and they are keyed by
|
|
// ARCHETYPE. This file deliberately does NOT restate them: an hp of "2 shots" here and an
|
|
// hp of 24 there (against damage 12) is the same number twice, which is one number too
|
|
// many. If you want to know what a bolus chunk can take, ask balance.js.
|
|
// Lane C owns COMPOSITION — which fiction appears, how many, where, in what order, and
|
|
// what it is called. That is this file plus the level JSONs.
|
|
//
|
|
// LANE B: resolve a spawn with `getEnemy(ev.enemy)?.archetype` — one line, snippet in
|
|
// LANE_C_NOTES.md §→ Lane B. The three bare archetype names also resolve (passthrough
|
|
// entries at the bottom), so the round-0 stub level and anything already built against it
|
|
// keep working unchanged.
|
|
//
|
|
// Pure data + pure functions: no imports, no THREE. Must stay node-runnable (qa selfcheck).
|
|
|
|
/** The only archetypes Lane B implements in round 1. Selfcheck enforces membership. */
|
|
export const ARCHETYPES = ['floater', 'seeker', 'turret'];
|
|
|
|
/**
|
|
* Fields:
|
|
* archetype 'floater'|'seeker'|'turret' — which of B's behaviors + balance rows drives it
|
|
* biome where it belongs (documentation; '*' = anywhere)
|
|
* tint emissive color. ART_BIBLE law: hostile = hot amber/red, projectiles =
|
|
* white-hot, neutral flora = cyan (turns hostile if shot). Advisory for Lane D.
|
|
* wall true => C must author an explicit `theta`. Lane B already defaults turrets to
|
|
* the wall; this is an AUTHORING rule, not a runtime one — a wall-mounted enemy
|
|
* owns an arc of the cross-section, and which arc is a design decision, never
|
|
* a random one.
|
|
* note the fiction, and why it earns its place in the encounter.
|
|
*/
|
|
export const ENEMIES = Object.freeze({
|
|
// ---- L2 esophagus roster (all three are real esophageal pathology) ----------------
|
|
bolus_chunk: {
|
|
archetype: 'floater', biome: 'esophagus', tint: 0xd98a3a, wall: false,
|
|
note: 'Swallowed food debris tumbling in the swallow. A floater taxes the lane it sits ' +
|
|
'in rather than chasing you, which is exactly what debris should do: it turns the ' +
|
|
'cross-section into a maze without ever raising the stakes. The L2 teaching enemy.',
|
|
},
|
|
eosinophil_swarm: {
|
|
archetype: 'seeker', biome: 'esophagus', tint: 0xff4d3d, wall: false,
|
|
note: 'Eosinophilic esophagitis, weaponised — immune cells that mistake ENDO-1 for the ' +
|
|
'parasite they were built for. Individually trivial; the threat is arithmetic. ' +
|
|
'B tuned the seeker at speed 19 against flow 14 specifically so it catches you ' +
|
|
'from behind: the answer is to shoot or boost, never to outrun. That is the ' +
|
|
'lesson L2 needs before the reflux finale asks you to outrun something.',
|
|
},
|
|
candida_bloom: {
|
|
archetype: 'turret', biome: 'esophagus', tint: 0xffd166, wall: true,
|
|
note: 'Candida esophagitis — white plaque colonies anchored to the mucosa, firing spore ' +
|
|
'clusters. Wall-mounted, so it OWNS an arc of the tube and converts "which side do ' +
|
|
'I fly" into a real decision. B\'s dart turn radius (13.3u) is wider than the tube, ' +
|
|
'so it can never corner you, only punish a straight line — which is why L2 puts ' +
|
|
'blooms on straights and in the squeeze, never on blind bends alone.',
|
|
},
|
|
|
|
// ---- L1 oral cavity (roster stub — L1's encounter pass is round 2+) ---------------
|
|
amylase_droplet: {
|
|
archetype: 'floater', biome: 'oral', tint: 0xe8a13a, wall: false,
|
|
note: 'Saliva enzyme blob with a corrosive contact aura. The tutorial floater — B tuned ' +
|
|
'the floater archetype against exactly this fiction (see balance.js).',
|
|
},
|
|
|
|
// ---- L3 stomach (roster stub — L3's encounter pass is round 2+) -------------------
|
|
pepsin_swarm: {
|
|
archetype: 'seeker', biome: 'stomach', tint: 0xff3b30, wall: false,
|
|
note: 'The seeker in its home biome — B tuned the archetype against this fiction.',
|
|
},
|
|
peyers_turret: {
|
|
archetype: 'turret', biome: 'small_intestine', tint: 0xffe066, wall: true,
|
|
note: 'Antibody turret in a Peyer\'s patch — B tuned the turret archetype against this ' +
|
|
'fiction. Belongs to L4; listed so the catalogue matches balance.js\'s vocabulary.',
|
|
},
|
|
|
|
// ---- passthrough: bare archetype names --------------------------------------------
|
|
// Compatibility so the round-0 stub level's `enemy: 'floater'` style still resolves and
|
|
// Lane B is never broken by this catalogue landing mid-round. Not for use in shipped
|
|
// levels — selfcheck WARNS, because an enemy with no fiction has no place in the body.
|
|
floater: { archetype: 'floater', biome: '*', tint: 0xd98a3a, wall: false, placeholder: true, note: 'Bare archetype passthrough.' },
|
|
seeker: { archetype: 'seeker', biome: '*', tint: 0xff4d3d, wall: false, placeholder: true, note: 'Bare archetype passthrough.' },
|
|
turret: { archetype: 'turret', biome: '*', tint: 0xffd166, wall: true, placeholder: true, note: 'Bare archetype passthrough.' },
|
|
});
|
|
|
|
/** → enemy spec, or null on miss. Consumers must handle null (assets-are-optional law). */
|
|
export function getEnemy(id) {
|
|
return Object.prototype.hasOwnProperty.call(ENEMIES, id) ? ENEMIES[id] : null;
|
|
}
|
|
|
|
/** Pickups Lane C may place. Lane B/E: `pickup {kind}` on the bus when collected. */
|
|
export const PICKUPS = Object.freeze({
|
|
nutrient_orb: { note: 'Score + boost recharge. The breadcrumb: an orb line TEACHES the racing line before it pays anything.' },
|
|
mucin_glob: { note: 'Mucus coat refill. Placed immediately before pressure — the game pays you right before it charges you.' },
|
|
b12_cell: { note: 'Hull repair. Rare: hull is the scarce resource, coat is the renewable one.' },
|
|
antacid_ammo: { note: 'Antacid torpedoes. Dual-use (GDD): B\'s torpedo neutralises an acid zone for 10s in a 14u radius — fired BACKWARD it stalls a reflux surge.' },
|
|
biopsy_sample: { note: 'THE collectible. Exactly 3 per level (selfcheck enforces). Visible-but-costly law: always in plain sight, never on the racing line.' },
|
|
});
|
|
|
|
/** Hazard kinds Lane C places in data. Specs + owners in LANE_C_NOTES.md §Hazard requests. */
|
|
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.' },
|
|
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.' },
|
|
});
|