[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>
This commit is contained in:
type-two 2026-07-19 20:45:21 +10:00
parent b0abbaf76b
commit b2d9b46c30
4 changed files with 51 additions and 1 deletions

View File

@ -91,6 +91,14 @@ export const BALANCE = {
// 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 },
},
darts: { pool: 48, radius: 0.45 },

View File

@ -87,6 +87,13 @@ const ARCHETYPES = {
color: EMISSIVE.acid,
foe: false,
},
// latcher (Giardia) — chases, CLAMPS to your hull, drains coat until a boost shakes it.
// Amber; a spiky tetrahedron (the trophozoite) so a clinging one reads on the hull.
latcher: {
cfg: B.enemies.latcher,
geo: () => new THREE.TetrahedronGeometry(B.enemies.latcher.radius * 1.3, 0),
color: EMISSIVE.hostile,
},
};
export function createEnemies({ scene, world, bus, rng, assets = null }) {
@ -293,6 +300,33 @@ export function createEnemies({ scene, world, bus, rng, assets = null }) {
bus.emit('flora:tended', { s: e.s, gain: cfg.gain }); // gain undefined for flora => default
}
}
} else if (e.type === 'latcher') {
// Giardia: chase, CLAMP, drain — until a boost shakes it. Boost becomes a DODGE here.
if (e.attached) {
e.phase += dt * 3; // spin on the hull
const off = e.radius + player.radius;
e.s = ps.s; e.x = ps.x + Math.cos(e.phase) * off; e.y = ps.y + Math.sin(e.phase) * off;
e.attachT -= dt; e.auraT -= dt;
if (ps.alive && e.auraT <= 0) { player.damage(cfg.drain, 'latch'); e.auraT = cfg.drainTick; }
if (ps.iframeT > 0 || e.attachT <= 0 || !ps.alive) { // boost i-frames throw it; else it lets go
e.attached = false; e.cooldown = cfg.reattachCd;
e.vs = -6; e.vx = Math.cos(e.phase) * 8; e.vy = Math.sin(e.phase) * 8; // flung clear
}
} else {
e.cooldown = Math.max(0, (e.cooldown || 0) - dt); // can't re-grab instantly
const ds = ps.s - e.s, dx = ps.x - e.x, dy = ps.y - e.y;
const d = Math.hypot(ds, dx, dy) || 1e-5;
if (ps.alive && Math.abs(ds) < cfg.aggroS) {
const k = Math.min(1, cfg.turn * dt);
e.vs += ((ds / d) * cfg.speed - e.vs) * k;
e.vx += ((dx / d) * cfg.speed - e.vx) * k;
e.vy += ((dy / d) * cfg.speed - e.vy) * k;
}
e.s += e.vs * dt; e.x += e.vx * dt; e.y += e.vy * dt;
if (ps.alive && e.cooldown <= 0 && d < cfg.radius + player.radius) {
e.attached = true; e.attachT = cfg.attachMax; e.auraT = 0; // CLAMP
}
}
} else if (e.type === 'phage') {
// The ALLY. A friendly seeker that hunts the nearest FOE (not the player) and rams it —
// a bacteriophage bursts its host. No foe in range => it escorts, easing to a standoff

View File

@ -114,6 +114,7 @@
{ "s": 1680, "type": "checkpoint", "name": "Thoracic Run" },
{ "s": 1700, "type": "hazard", "kind": "reflux_surge", "speed": 16, "from": -60, "span": 260, "warn": 3.0, "lethal": false, "neutralizable": true, "note": "TEACH reflux - the most important event in the level. A burp: 16 u/s against a 14 u/s current, so even a timid player at throttle 1.15 (16.1 u/s) escapes it. Touching it hurts and does not kill. When the real surge arrives at s=3000 the player already knows the verb." },
{ "s": 1900, "type": "spawn", "enemy": "bolus_chunk", "count": 3, "spread": 100 },
{ "s": 1940, "type": "spawn", "enemy": "giardia_latch", "count": 1, "note": "MICROBE PASS (round 2) preview - Giardia belongs to L4 (villi maze) but its grab works in a flow tube, so one rides here to teach the new verb: it clamps to your hull and drains coat until you BOOST it off. Boost is now a dodge. Placed on the open thoracic run where you have room and a charged boost; C relocates it to L4 when that level lands." },
{ "s": 2000, "type": "checkpoint", "name": "Bronchial Narrowing" },
{ "s": 2060, "type": "spawn", "enemy": "candida_bloom", "count": 3, "theta": [0.2, 2.4, 4.6], "note": "Three arcs covered in the level's twistiest tube: each is a surprise on a blind bend. This is the beat that makes curviness a mechanic." },

View File

@ -23,7 +23,7 @@
* 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'];
export const ARCHETYPES = ['floater', 'seeker', 'turret', 'flora', 'spore_pod', 'phage', 'mucus_grazer', 'cdiff', 'latcher'];
/**
* Fields:
@ -117,6 +117,13 @@ export const ENEMIES = Object.freeze({
'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