guts/web/js/levels/enemies.js
type-two b2d9b46c30 [lane B+C] Giardia: the latch, and boost becomes a dodge
The first foe with an ATTACH state machine, and the first that gives boost a defensive job.
Giardia chases like a seeker, CLAMPS its ventral suction disc onto your hull, and drains coat
while it rides — until a boost's i-frames throw it (or it lets go after attachMax, then can't
re-grab for reattachCd). The counterplay is timing a boost, not out-running it.

- enemies.js: `latcher` archetype — a two-state (chase / attached) branch. Attached, it rides
  player.pos + a spin offset and drains `drain`/tick; ps.iframeT > 0 (boost) detaches and flings
  it clear. Amber tetrahedron (the trophozoite), so a clinging one reads on the hull.
- levels/enemies.js: ARCHETYPES += latcher; catalogue `giardia_latch` (biome small_intestine).
- L2: one Giardia previewed on the open thoracic run (s1940) — it belongs to L4's villi maze,
  but the grab works in a flow tube, so it teaches the boost-dodge verb now.

Verified: it chased + clamped, drained coat 100->84 while attached, and a simulated boost
(iframeT) shook it off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:45:21 +10:00

157 lines
11 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 archetypes Lane B implements. Selfcheck enforces membership.
* Round-2 microbe pass added `flora` (the first friend), `spore_pod` (a distinct-mesh foe),
* `phage` (the first ally — a friendly seeker that hunts foes), and `mucus_grazer` (a commensal
* that trades your coat for faster standing). */
export const ARCHETYPES = ['floater', 'seeker', 'turret', 'flora', 'spore_pod', 'phage', 'mucus_grazer', 'cdiff', 'latcher'];
/**
* 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.',
},
// ---- round-2 microbe pass: the first FRIEND, and a distinct-mesh foe ---------------
lacto_drifter: {
archetype: 'flora', biome: 'esophagus', tint: 0x39e6ff, wall: false,
note: 'Lactobacillus — a friendly commensal that acidifies the neighbourhood and out-' +
'competes pathogens. Cyan (ART_BIBLE: flora reads friendly). Brush its aura and it ' +
'sheds mucus coat; SHOOT it and you have harmed the biome (flora:harmed) — the seed ' +
'of the BIOME STANDING reputation loop. The first entity that rewards NOT firing.',
},
yeast_pod: {
archetype: 'spore_pod', biome: 'esophagus', tint: 0xffb347, wall: false,
note: 'Candida budding-yeast cluster — a drifting colony with a corrosive contact aura. ' +
'Mechanically a floater, but its own archetype so it carries a distinct blobby ' +
'silhouette: the first foe authored to be skinned by a Trellis mesh.',
},
phage: {
archetype: 'phage', biome: '*', tint: 0x33ffbe, wall: false,
note: 'Bacteriophage — the most abundant biological entity in you; hunts one bacterial ' +
'species and bursts it. THE ALLY: a friendly seeker that chases your FOES, not you ' +
'(teal, not the cyan "don\'t shoot" flora). Budded by high BIOME STANDING — the payoff ' +
'for tending flora instead of spraying it. Placeable by C, but mainly biome-spawned.',
},
akkermansia: {
archetype: 'mucus_grazer', biome: '*', tint: 0x39e6ff, wall: false,
note: 'Akkermansia muciniphila — lives in your mucus layer and EATS it, yet is a marker of ' +
'a lean healthy gut. The cleverest node in the reputation loop: brushing it COSTS a ' +
'little coat but fills BIOME STANDING fast (gain 12 vs flora\'s 5) — spend coat to earn ' +
'allies quicker. Cyan (a commensal), amoeba-blob shape so the coat cost reads as a ' +
'different bug, not a betrayal by the healing rod.',
},
cdiff_bloom: {
archetype: 'cdiff', biome: 'large_intestine', tint: 0xff7a2a, wall: false,
note: 'Clostridioides difficile — waits for antibiotics to clear the neighbours, then blooms ' +
'and paints the colon in pseudomembrane, leaving armoured SPORES. In-game a floater ' +
'with a corrosive aura whose corpse ISN\'T clean: killing it drops a spore_cloud that ' +
'keeps hurting the ground it died on (shoot it to disperse, or fly around). The colon\'s ' +
'definitive foe; belongs to L5, registered so C can place it when L5 gets its pass.',
},
giardia_latch: {
archetype: 'latcher', biome: 'small_intestine', tint: 0xff5a2a, wall: false,
note: 'Giardia duodenalis — the "smiley-face" trophozoite that clamps its ventral suction ' +
'disc onto the gut wall and carpets it, starving you (malabsorption). In-game it chases, ' +
'CLAMPS to your hull, and drains coat until you BOOST it off — the first foe that gives ' +
'boost a defensive job. Belongs to L4 (the villi maze); registered for its encounter pass.',
},
// ---- 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.' },
});