diff --git a/web/js/combat/balance.js b/web/js/combat/balance.js index 44960ab..e401b04 100644 --- a/web/js/combat/balance.js +++ b/web/js/combat/balance.js @@ -79,6 +79,18 @@ export const BALANCE = { // 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 }, }, darts: { pool: 48, radius: 0.45 }, diff --git a/web/js/combat/enemies.js b/web/js/combat/enemies.js index 6ce0b3d..2ab1f18 100644 --- a/web/js/combat/enemies.js +++ b/web/js/combat/enemies.js @@ -72,6 +72,21 @@ const ARCHETYPES = { color: EMISSIVE.neutral, foe: false, }, + // cdiff (C. difficile) — a foe floater whose corpse leaves a spore_cloud (kill()). Amber, + // an angular octahedron so it reads distinct from the round yeast cluster. + cdiff: { + cfg: B.enemies.cdiff, + geo: () => new THREE.OctahedronGeometry(B.enemies.cdiff.radius, 0), + color: EMISSIVE.hostile, + }, + // spore_cloud — the lingering corrosive zone C. diff drops on death. Acid-green (readability + // law: acid/corrosive), foe:false so the phage ignores it and clearing it never scores. + spore_cloud: { + cfg: B.enemies.spore_cloud, + geo: () => new THREE.IcosahedronGeometry(B.enemies.spore_cloud.radius, 1), + color: EMISSIVE.acid, + foe: false, + }, }; export function createEnemies({ scene, world, bus, rng, assets = null }) { @@ -148,7 +163,7 @@ export function createEnemies({ scene, world, bus, rng, assets = null }) { hp: cfg.hp, radius: cfg.radius, alive: true, pos: new THREE.Vector3(), flash: 0, phase: rand() * Math.PI * 2, cooldown: rand() * (cfg.fireEvery ?? 1), // desync turret volleys - auraT: 0, + auraT: 0, life: cfg.life ?? 0, // life: only spore_cloud counts down }; live.push(e); pool.slots.push(e); @@ -178,6 +193,10 @@ export function createEnemies({ scene, world, bus, rng, assets = null }) { onDeath?.(e); return; } + if (e.type === 'cdiff') { // the corpse keeps hurting: leave a spore cloud + const sp = spawn('spore_cloud', { s: e.s }); + if (sp) { sp.x = e.x; sp.y = e.y; } // at the death spot, not a random arc + } comboN++; comboT = B.combo.window; bus.emit('enemy:die', { id: e.id, type: e.type, s: e.s, score: e.cfg.score, kind }); bus.emit('combo', { n: comboN }); @@ -221,9 +240,23 @@ export function createEnemies({ scene, world, bus, rng, assets = null }) { e.flash = Math.max(0, e.flash - dt * 6); const cfg = e.cfg; - if (e.type === 'floater' || e.type === 'spore_pod') { - // drifts and bobs; taxes the lane it occupies rather than chasing. spore_pod shares - // this behaviour exactly — it's a floater with its own pool/mesh (a yeast cluster). + if (e.type === 'spore_cloud') { + // A lingering corrosive spore cloud (C. diff's parting gift). Static — it does NOT move; + // it just damages on contact and expires after `life`. Killing the C. diff isn't clean. + e.life -= dt; + if (e.life <= 0) { kill(e, 'expire'); continue; } + if (ps.alive) { + const d = Math.hypot(e.s - ps.s, e.x - ps.x, e.y - ps.y); + e.auraT -= dt; + if (d < cfg.auraRadius + player.radius && e.auraT <= 0) { + player.damage(cfg.auraDps * cfg.auraTick, 'spore'); + e.auraT = cfg.auraTick; + } + } + } else if (e.type === 'floater' || e.type === 'spore_pod' || e.type === 'cdiff') { + // drifts and bobs; taxes the lane it occupies rather than chasing. spore_pod (yeast) and + // cdiff (C. difficile) share this exactly — floaters with their own pool/mesh. cdiff's + // twist is only at death (kill() drops a spore_cloud), never in how it moves. e.phase += dt * cfg.bob; e.s += Math.sin(e.phase) * cfg.drift * dt; e.x += Math.cos(e.phase * 0.7) * cfg.drift * dt; diff --git a/web/js/levels/enemies.js b/web/js/levels/enemies.js index 0a0c087..16e5228 100644 --- a/web/js/levels/enemies.js +++ b/web/js/levels/enemies.js @@ -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']; +export const ARCHETYPES = ['floater', 'seeker', 'turret', 'flora', 'spore_pod', 'phage', 'mucus_grazer', 'cdiff']; /** * Fields: @@ -109,6 +109,14 @@ export const ENEMIES = Object.freeze({ '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.', + }, // ---- passthrough: bare archetype names -------------------------------------------- // Compatibility so the round-0 stub level's `enemy: 'floater'` style still resolves and