From aae1ebdcfd8a7beec496d8972bfda4dc1178be68 Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 19 Jul 2026 20:30:17 +1000 Subject: [PATCH] [lane B+C] Akkermansia: spend coat to summon allies sooner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cleverest node in the reputation loop — a commensal that eats your mucus. Brushing it COSTS coat but fills BIOME STANDING fast (gain 12 vs flora's 5): the "spend coat for a quicker phage ally" trade against flora's safe heal-and-slow. - enemies.js: flora's branch generalised to a shared COMMENSAL branch, sign-flipped by data (cfg.coat gifts, cfg.coatCost nibbles); flora:tended now carries `gain`. New `mucus_grazer` archetype — cyan (still flora, readability law), but a dodecahedron amoeba-blob so the coat cost reads as a different bug, not a betrayal by the healing rod. - combat/index.js: standing gain comes from the event's `gain`. - levels: ARCHETYPES += mucus_grazer; catalogue `akkermansia`; L2 teaches it at s210, right after the flora reef so the same-colour / opposite-trade contrast lands. Verified: brushing it dropped coat 100->88 while standing hit 36% in 3 tends (4x flora's rate); distinct blob vs the flora capsule. Co-Authored-By: Claude Opus 4.8 --- web/js/combat/balance.js | 8 ++++++++ web/js/combat/enemies.js | 24 ++++++++++++++++++------ web/js/combat/index.js | 2 +- web/js/levels/L2_esophagus.json | 1 + web/js/levels/enemies.js | 13 +++++++++++-- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/web/js/combat/balance.js b/web/js/combat/balance.js index 18a619d..44960ab 100644 --- a/web/js/combat/balance.js +++ b/web/js/combat/balance.js @@ -55,9 +55,17 @@ export const BALANCE = { // no combo (enemies.js kill()): it sits in the enemy pool so it is shootable, but shooting // it is the reputation trap, never a reward. `coat` kept small + no parking in a flow tube = // a trickle, not a fountain. hp low: a stray pellet shouldn't wipe a colony you meant to spare. + // No `gain` => flora:tended defaults to the small +5 standing in combat/index.js. flora: { hp: 18, radius: 2.0, score: 0, drift: 1.2, bob: 0.4, auraRadius: 3.6, coat: 6, auraTick: 0.3, pool: 24 }, + // mucus_grazer (Akkermansia) — a commensal that eats your mucin. Brushing it COSTS `coatCost` + // per tick but signals gut health hardest (`gain` 12 vs flora's 5): the "spend coat for + // faster allies" trade. Shares flora's drift+aura branch, sign-flipped by data. score 0, + // shootable (harming it is still flora:harmed). The clever risk/reward of the reputation loop. + mucus_grazer: { hp: 20, radius: 2.2, score: 0, drift: 1.0, bob: 0.35, + auraRadius: 4.0, coatCost: 4, gain: 12, auraTick: 0.3, pool: 16 }, + // spore_pod — a foe budding-yeast/Candida cluster. Mechanically the floater (drift + corrosive // aura) with its OWN pool so its blobby mesh reads distinct from tumbling food debris. The // first entity authored to be skinned by a Trellis mesh (assets/models/spore_pod.glb). diff --git a/web/js/combat/enemies.js b/web/js/combat/enemies.js index f5b2b3f..6ce0b3d 100644 --- a/web/js/combat/enemies.js +++ b/web/js/combat/enemies.js @@ -63,6 +63,15 @@ const ARCHETYPES = { color: EMISSIVE.ally, foe: false, }, + // mucus_grazer (Akkermansia) — a commensal that NIBBLES your coat (it eats mucin) but signals + // gut health hardest. Cyan (still flora, readability law), but a distinct amoeba-blob silhouette + // so the coat cost reads as "a different bug", not a betrayal by the healing rod. + mucus_grazer: { + cfg: B.enemies.mucus_grazer, + geo: () => new THREE.DodecahedronGeometry(B.enemies.mucus_grazer.radius, 0), + color: EMISSIVE.neutral, + foe: false, + }, }; export function createEnemies({ scene, world, bus, rng, assets = null }) { @@ -230,10 +239,12 @@ export function createEnemies({ scene, world, bus, rng, assets = null }) { e.auraT = cfg.auraTick; } } - } else if (e.type === 'flora') { - // The FRIEND. Drifts like a floater but its aura HEALS: brush past and it sheds a - // little mucus coat (the "mobile mucin lane"). Never damages you. It's still in the - // enemy pool, so you CAN shoot it — and that is the reputation trap, handled in kill(). + } else if (e.type === 'flora' || e.type === 'mucus_grazer') { + // COMMENSALS. Drift like a floater; brushing the aura raises BIOME STANDING (flora:tended) + // — that is the reputation you bank. The coat effect is data-driven and sign-flipped: + // flora sheds `coat` (a mobile mucin lane) + small standing (default +5). + // mucus_grazer nibbles `coatCost` (Akkermansia eats mucin) + big standing (`gain`). + // Neither ever "attacks" you; both stay shootable, which is the reputation trap (kill()). 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; @@ -243,9 +254,10 @@ export function createEnemies({ scene, world, bus, rng, assets = null }) { 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.refill({ coat: cfg.coat }); // friendly: gives coat instead of taking it + if (cfg.coat) player.refill({ coat: cfg.coat }); + if (cfg.coatCost) player.damage(cfg.coatCost, 'graze'); e.auraT = cfg.auraTick; - bus.emit('flora:tended', { s: e.s }); // for the (future) BIOME STANDING meter + bus.emit('flora:tended', { s: e.s, gain: cfg.gain }); // gain undefined for flora => default } } } else if (e.type === 'phage') { diff --git a/web/js/combat/index.js b/web/js/combat/index.js index a435c79..7ca3096 100644 --- a/web/js/combat/index.js +++ b/web/js/combat/index.js @@ -81,7 +81,7 @@ export function createCombat({ scene, world, bus, rng, flags = {}, assets = null // first pass — C/playtest tunes the feel. let standing = 0, phageBudCd = 0; const setStanding = (v) => { standing = Math.max(0, Math.min(100, v)); bus.emit('standing', { value: standing }); }; - const offTended = bus.on('flora:tended', () => { setStanding(standing + 5); budPhage(); }); + const offTended = bus.on('flora:tended', (e) => { setStanding(standing + (e?.gain ?? 5)); budPhage(); }); const offHarmed = bus.on('flora:harmed', () => setStanding(standing - 35)); function budPhage() { if (standing < 100 || phageBudCd > 0) return; // a FULL bar buds an ally diff --git a/web/js/levels/L2_esophagus.json b/web/js/levels/L2_esophagus.json index 2d58b25..b2aeb8d 100644 --- a/web/js/levels/L2_esophagus.json +++ b/web/js/levels/L2_esophagus.json @@ -86,6 +86,7 @@ { "s": 20, "type": "checkpoint", "name": "Upper Esophageal Sphincter" }, { "s": 120, "type": "pickup", "kind": "nutrient_orb", "count": 3, "spread": 40, "note": "Orb lines teach the racing line before they score anything." }, { "s": 180, "type": "spawn", "enemy": "lacto_drifter", "count": 4, "spread": 90, "rho": 0.7, "note": "MICROBE PASS (round 2) - the first FRIEND, taught first, in the calmest stretch. A Lactobacillus reef drifting off the racing line (cyan = friendly, ART_BIBLE). Brush it and it sheds mucus coat; SHOOT it and you have harmed the biome (flora:harmed) - the seed of the BIOME STANDING loop. Teaches 'don't shoot cyan' before any level charges for it." }, + { "s": 210, "type": "spawn", "enemy": "akkermansia", "count": 2, "spread": 30, "rho": 0.5, "note": "MICROBE PASS (round 2) - the CHOICE node, taught right after flora so the contrast lands: same cyan family, opposite trade. Brushing Akkermansia nibbles a little coat but fills BIOME STANDING fast (gain 12) - spend coat to summon a phage ally sooner. Near the racing line so the choice is unavoidable." }, { "s": 240, "type": "spawn", "enemy": "bolus_chunk", "count": 1, "note": "TEACH bolus: one chunk, slow, unmissable, harmless if you simply steer." }, { "s": 300, "type": "spawn", "enemy": "yeast_pod", "count": 1, "rho": 0.6, "note": "MICROBE PASS (round 2) - the spore_pod foe's teach beat: one drifting Candida cluster, off-line, harmless if you steer. A distinct blobby silhouette (Trellis-skinned), not the food-debris floater." }, { "s": 340, "type": "pickup", "kind": "nutrient_orb", "count": 3, "spread": 40 }, diff --git a/web/js/levels/enemies.js b/web/js/levels/enemies.js index 821ab90..0a0c087 100644 --- a/web/js/levels/enemies.js +++ b/web/js/levels/enemies.js @@ -21,8 +21,9 @@ /** The archetypes Lane B implements. Selfcheck enforces membership. * Round-2 microbe pass added `flora` (the first friend), `spore_pod` (a distinct-mesh foe), - * and `phage` (the first ally — a friendly seeker that hunts foes). */ -export const ARCHETYPES = ['floater', 'seeker', 'turret', 'flora', 'spore_pod', 'phage']; + * `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']; /** * Fields: @@ -100,6 +101,14 @@ export const ENEMIES = Object.freeze({ '(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.', + }, // ---- passthrough: bare archetype names -------------------------------------------- // Compatibility so the round-0 stub level's `enemy: 'floater'` style still resolves and