Pseudopod Lab
A grip / engulf lab — drive a specimen and engulf small prey at short reach; see how the pseudopod's none/poor/decent/great tiers change the grip.
---
.. toc::
Grab, reel, engulf
The pseudopod is grip — phagocytic engulfing of live prey. Here it doesn't just auto-eat: it throws out pseudopod arms that latch onto nearby red prey, reel them in, and engulf them at the mouth. The tier sets how far you can reach, how many prey you can hold at once, and how fast you reel — none can't grip and you starve. (Teal arms = active grips; the dashed ring is your grab range; plankton is off a carnivore's menu.)
This is the *offensive* twin of the in-game predator grapple (Phase D): there a hunter latches onto *you* and drains you, and you break free by moving away, countering with a spike/stinger, or waiting it out. The right panel shows how this specimen's nature skews the mutation roll (cross-diet is rare).
.. exec::docs.adaptation_pseudopod.adaptation_pseudopod :code: false
Source
```python
File: lib/adaptation_lab.py
"""Builder for the movement Adaptation Labs (one isolated, drivable petri dish per movement part). movement_lab(part_id) returns the page component and registers its (part-namespaced) clientside callbacks. Used by the thin docs/adaptation_<part> pages so flagellum/cilia/membrane share one implementation.
Each lab is a *limited* example — no food chain, just: pick a specimen + a tier (none/poor/decent/great), drive it (joystick / WASD), and FEEL the difference, with a path trail + live stats and a per-spawn luck roll. The same tier feels different per specimen because it modulates that specimen's movement archetype. """
import json
from dash import dcc, html, clientside_callback, Input, Output, State import dash_mantine_components as dmc from dash_iconify import DashIconify import dash_gauge as dg
from lib.adaptations import ADAPTATIONS, TIER_LABEL, TIER_COLOR from lib.movement import to_store as _movement_store, SPECIMENS, FEEL from lib.diet import (PART_DIET, SPECIMEN_DIET, DIET_LABEL, DIET_COLOR, DIET_EDIBLE, to_store as _diet_store)
ACCENT = "#3399ff" _ROLE_COLOR = {"speed": "blue", "sense": "grape", "defense": "orange", "attack": "red", "feed": "green", "grip": "teal"} _TIERS = ["none", "poor", "decent", "great"] _DEFAULT_SPEC = {"flagellum": "euglena", "cilia": "paramecium", "membrane": "celegans"}
def _tier_ref_card(part_id, v): """A static reference card: organelle schematic + feel line + stats.""" t = v["tier"] return dmc.Paper( [ dmc.Group([dmc.Badge(TIER_LABEL[t], color=TIER_COLOR[t], variant="light", size="sm"), dmc.Text(v["name"], fw=600, size="sm")], justify="space-between", wrap="nowrap"), html.Div(id={"type": "ml-card", "part": part_id, "tier": t}, className="petri-dish", style={"width": "100%", "aspectRatio": "1 / 1", "borderRadius": "10px", "overflow": "hidden", "margin": "6px 0"}), dmc.Text(FEEL.get(part_id, {}).get(t, v["blurb"]), size="xs", c="dimmed"), dmc.Group([dmc.Text(f"stat {v['mult']:+.1f}", size="xs", fw=600, c="red" if v["mult"] < 0 else ("dimmed" if v["mult"] == 0 else "teal"), style={"fontFamily": "monospace"}), dmc.Text(f"cost {v['cost']:.2f}", size="xs", c="dimmed", style={"fontFamily": "monospace"})], justify="space-between"), ], withBorder=True, radius="md", p="sm", )
def movement_lab(part_id): part = ADAPTATIONS[part_id] P = part_id defspec = _DEFAULT_SPEC.get(P, SPECIMENS[0]["value"])
badges = [dmc.Badge(part["role"], color=_ROLE_COLOR.get(part["role"], "gray"), variant="light", size="sm")] if part.get("diet"): badges.append(dmc.Badge(part["diet"], color="lime", variant="dot", size="sm"))
dish = dmc.Paper( [ dmc.Group( [dmc.Text(part["label"].upper(), fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Group(badges, gap=6)], justify="space-between", mb="xs", ), html.Div( [ html.Div(html.Div(id=f"ml-{P}-arena", style={"position": "absolute", "inset": 0, "zIndex": 1}), className="petri-dish", style={"position": "absolute", "inset": 0, "borderRadius": "50%", "overflow": "hidden"}), html.Div( dg.DashRCJoystick(id=f"ml-{P}-joy", baseRadius=48, controllerRadius=22, directionCountMode="Nine", insideMode=True, throttle=40), style={"position": "absolute", "right": "2px", "bottom": "2px", "zIndex": 6, "padding": "6px", "borderRadius": "50%", "background": "rgba(255,255,255,0.55)", "backdropFilter": "blur(2px)", "boxShadow": "0 2px 10px rgba(15,80,72,0.25)"}, ), ], style={"position": "relative", "aspectRatio": "1 / 1", "width": "100%", "maxWidth": 460, "margin": "0 auto"}, ), dcc.Interval(id=f"ml-{P}-tick", interval=60, n_intervals=0), dcc.Interval(id=f"ml-{P}-init", interval=300, n_intervals=0, max_intervals=1), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 460px", "maxWidth": 540, "background": "linear-gradient(rgb(255 255 255 / 0%) 0%, rgb(243 247 239 / 30%) 100%)", "borderColor": "rgb(51 142 234 / 24%)"}, )
controls = dmc.Stack( [ dmc.Text("DRIVE IT", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Select(id=f"ml-{P}-spec", label="Specimen", value=defspec, allowDeselect=False, data=[{"value": s["value"], "label": s["label"]} for s in SPECIMENS], leftSection=DashIconify(icon="mdi:microscope")), dmc.Stack( [dmc.Text("ADAPTATION TIER", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.SegmentedControl(id=f"ml-{P}-tier", value="decent", fullWidth=True, color="brand", radius="md", size="xs", data=[{"value": t, "label": TIER_LABEL[t]} for t in _TIERS])], gap=4), dmc.Text("", id=f"ml-{P}-feel", size="sm", fw=600, c=ACCENT), dmc.Paper( html.Pre("", id=f"ml-{P}-stats", style={"fontFamily": "monospace", "fontSize": "11px", "margin": 0, "whiteSpace": "pre-wrap", "color": "light-dark(#236bb3, #9ec9ff)"}), withBorder=True, radius="md", p="sm", style={"background": "light-dark(#eef5ff, #16202e)"}, ), dmc.Button("Reroll luck (respawn)", id=f"ml-{P}-respawn", variant="light", color="brand", leftSection=DashIconify(icon="tabler:refresh")), dmc.Text("Drive with the joystick or WASD / arrow keys. The trail shows the movement's " "character; ‘Reroll luck’ re-rolls this spawn's small random variance.", size="xs", c="dimmed"), ], gap="sm", style={"flex": "1 1 240px", "maxWidth": 300}, )
component = dmc.Stack( [ dmc.Paper( [dmc.Text(part["label"] + " — MOVEMENT LAB", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text(part["function"] + " Each tier modulates the specimen's natural movement; " "a small per-spawn luck roll makes every run a little different.", size="sm", c="dimmed")], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)", "background": "linear-gradient(rgb(255 255 255 / 0%) 0%, rgb(243 247 239 / 30%) 100%)"}, ), dmc.Group([dish, controls], align="flex-start", gap="xl", wrap="wrap"), dmc.Paper( [dmc.Text("TIER REFERENCE", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}, mb="xs"), dmc.SimpleGrid(cols={"base": 2, "sm": 4}, spacing="sm", children=[_tier_ref_card(P, v) for v in part["variants"]])], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)"}, ), dcc.Store(id=f"ml-{P}-mv", data=_movement_store()), dcc.Store(id=f"ml-{P}-sink"), ], gap="md", )
# ---- driving loop (part-namespaced via __P__ substitution) -------------- loop_js = """ function(n, nResp, spec, tier, mv, jAngle, jDist) { const nu = window.dash_clientside.no_update; if (!window.CM || !mv) { return [nu, nu]; } const P = '__P__'; if (!window.__mlKeys) { window.__mlKeys = {}; window.addEventListener('keydown', e => { window.__mlKeys[e.key.toLowerCase()] = true; if (e.key === ' ' && document.querySelector('.petri-dish:hover, .petri-dish:focus-within')) { e.preventDefault(); } }); window.addEventListener('keyup', e => { window.__mlKeys[e.key.toLowerCase()] = false; }); } window.__ml = window.__ml || {}; let M = window.__ml[P]; if (!M || M.spec !== spec || M.tier !== tier || M.resp !== (nResp || 0)) { M = window.__ml[P] = {spec: spec, tier: tier, resp: (nResp || 0), variance: window.CM.variance(), state: {x: 0, y: 0, vx: 0, vy: 0, deg: -90, trail: [], cd: 0, _ph: 0}}; } const eff = window.CM.computeStats(mv, spec, P, tier, M.variance); let dx = 0, dy = 0; if (jAngle != null && jDist) { const m = Math.min(1, jDist / 26), a = jAngle * Math.PI / 180; dx += Math.cos(a) * m; dy += -Math.sin(a) * m; } const k = window.__mlKeys || {}; if (k['w'] || k['arrowup']) { dy -= 1; } if (k['s'] || k['arrowdown']) { dy += 1; } if (k['a'] || k['arrowleft']) { dx -= 1; } if (k['d'] || k['arrowright']) { dx += 1; } if (dx || dy) { const mm = Math.hypot(dx, dy); if (mm > 1) { dx /= mm; dy /= mm; } } window.CM.step(M.state, {dx: dx, dy: dy}, eff); const COL = {none: '#9aa7b3', poor: '#fa5252', decent: '#f0a020', great: '#2bb673'}[tier] || '#3399ff'; const sp = (mv.specimens.find(s => s.value === spec) || {}); const host = document.getElementById('ml-__P__-arena'); if (host) { host.innerHTML = window.CM.render(M.state, sp.img || '', COL); } const v = M.variance; const stats = 'ARCHETYPE ' + eff.archLabel + '\\n' + 'TOP SPEED ' + eff.speed.toFixed(1) + '\\n' + 'TURN ' + (eff.turn * 100 | 0) + '%\\n' + 'CONTROL ' + (eff.control * 100 | 0) + '%\\n' + (eff.dart > 0 ? 'DART ' + eff.dart.toFixed(0) + '\\n' : '') + 'LUCK spd ' + v.speed.toFixed(2) + ' \\u00b7 ctl ' + v.ctrl.toFixed(2) + (eff.dart > 0 ? ' \\u00b7 jmp ' + v.jump.toFixed(2) : ''); return [stats, eff.feel]; } """.replace("__P__", P) clientside_callback( loop_js, Output(f"ml-{P}-stats", "children"), Output(f"ml-{P}-feel", "children"), Input(f"ml-{P}-tick", "n_intervals"), Input(f"ml-{P}-respawn", "n_clicks"), Input(f"ml-{P}-spec", "value"), Input(f"ml-{P}-tier", "value"), State(f"ml-{P}-mv", "data"), State(f"ml-{P}-joy", "angle"), State(f"ml-{P}-joy", "distance"), )
# ---- fill the tier reference schematics once on mount ------------------- clientside_callback( """ function(_n) { if (!window.CV) { return window.dash_clientside.no_update; } document.querySelectorAll('[id*="ml-card"]').forEach(host => { let id = null; try { id = JSON.parse(host.id); } catch (e) { return; } if (id.part !== '__P__') { return; } host.innerHTML = (window.CV.renderAdaptPreview || window.CV.renderPart)(id.part, id.tier); }); return ''; } """.replace("__P__", P), Output(f"ml-{P}-sink", "data"), Input(f"ml-{P}-init", "n_intervals"), )
return component
============================ combat labs ====================================
Spike (defense) + Stinger (attack): drive a specimen while red predators home in.
Spike REPELS attackers on contact (tier scales push + damage); Stinger ZAPS the
nearest predator within a tier-scaled venom reach (paralyse → flee; great kills).
_COMBAT_DEFAULT_SPEC = {"spike": "actinosphaerium", "stinger": "cyclops"} _COMBAT_FEEL = { "spike": {"none": "No spines — predators engulf you freely.", "poor": "Brittle bumps — barely slow an attacker.", "decent": "Rigid spines deter most predators on contact.", "great": "Reinforced spines — attackers bounce off and take damage."}, "stinger": {"none": "No barb — you can't fight back.", "poor": "Misfires — short, weak, slow venom.", "decent": "Reliable barb — paralyses an adjacent attacker.", "great": "Ranged high-potency barb — zap and kill before contact."}, }
def combat_lab(part_id): part = ADAPTATIONS[part_id] P = part_id is_def = part["role"] == "defense" defspec = _COMBAT_DEFAULT_SPEC.get(P, SPECIMENS[0]["value"]) badges = [dmc.Badge(part["role"], color="orange" if is_def else "red", variant="light", size="sm")]
dish = dmc.Paper( [dmc.Group([dmc.Text(part["label"].upper(), fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Group(badges, gap=6)], justify="space-between", mb="xs"), html.Div( [html.Div(html.Div(id=f"cl-{P}-arena", style={"position": "absolute", "inset": 0, "zIndex": 1}), className="petri-dish", style={"position": "absolute", "inset": 0, "borderRadius": "50%", "overflow": "hidden"}), html.Div(dg.DashRCJoystick(id=f"cl-{P}-joy", baseRadius=48, controllerRadius=22, directionCountMode="Nine", insideMode=True, throttle=40), style={"position": "absolute", "right": "2px", "bottom": "2px", "zIndex": 6, "padding": "6px", "borderRadius": "50%", "background": "rgba(255,255,255,0.55)", "backdropFilter": "blur(2px)", "boxShadow": "0 2px 10px rgba(15,80,72,0.25)"})], style={"position": "relative", "aspectRatio": "1 / 1", "width": "100%", "maxWidth": 460, "margin": "0 auto"}), dcc.Interval(id=f"cl-{P}-tick", interval=60, n_intervals=0), dcc.Interval(id=f"cl-{P}-init", interval=300, n_intervals=0, max_intervals=1)], p="md", radius="md", withBorder=True, style={"flex": "1 1 460px", "maxWidth": 540, "background": "linear-gradient(rgb(255 255 255 / 0%) 0%, rgb(243 247 239 / 30%) 100%)", "borderColor": "rgb(51 142 234 / 24%)"}, )
controls = dmc.Stack( [dmc.Text("SURVIVE IT", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Select(id=f"cl-{P}-spec", label="Specimen", value=defspec, allowDeselect=False, data=[{"value": s["value"], "label": s["label"]} for s in SPECIMENS], leftSection=DashIconify(icon="mdi:microscope")), dmc.Stack([dmc.Text("ADAPTATION TIER", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.SegmentedControl(id=f"cl-{P}-tier", value="decent", fullWidth=True, color="brand", radius="md", size="xs", data=[{"value": t, "label": TIER_LABEL[t]} for t in _TIERS])], gap=4), dmc.Text("", id=f"cl-{P}-feel", size="sm", fw=600, c=ACCENT), dmc.Paper(html.Pre("", id=f"cl-{P}-stats", style={"fontFamily": "monospace", "fontSize": "11px", "margin": 0, "whiteSpace": "pre-wrap", "color": "light-dark(#236bb3, #9ec9ff)"}), withBorder=True, radius="md", p="sm", style={"background": "light-dark(#eef5ff, #16202e)"}), dmc.Button("Reset predators", id=f"cl-{P}-respawn", variant="light", color="brand", leftSection=DashIconify(icon="tabler:refresh")), dmc.Text(("Red predators hunt you. Spines repel attackers on contact — higher tiers push harder, " "damage them, and keep them fleeing longer." if is_def else "Red predators hunt you. Your barb auto-zaps the nearest attacker in venom range — higher " "tiers reach further, hit harder, and paralyse longer."), size="xs", c="dimmed")], gap="sm", style={"flex": "1 1 250px", "maxWidth": 320}, )
component = dmc.Stack( [dmc.Paper([dmc.Text(part["label"] + (" — DEFENSE LAB" if is_def else " — ATTACK LAB"), fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text(part["function"] + " Drive to dodge; the adaptation does the rest — its TIER sets " "how far and how hard it works.", size="sm", c="dimmed")], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)", "background": "linear-gradient(rgb(255 255 255 / 0%) 0%, rgb(243 247 239 / 30%) 100%)"}), dmc.Group([dish, controls], align="flex-start", gap="xl", wrap="wrap"), dmc.Paper([dmc.Text("TIER REFERENCE", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}, mb="xs"), dmc.SimpleGrid(cols={"base": 2, "sm": 4}, spacing="sm", children=[_tier_ref_card(P, v) for v in part["variants"]])], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)"}), dcc.Store(id=f"cl-{P}-mv", data=_movement_store()), dcc.Store(id=f"cl-{P}-sink")], gap="md", )
loop_js = """ function(n, nResp, spec, tier, mv, jAngle, jDist) { const nu = window.dash_clientside.no_update; if (!window.CM || !window.CV || !mv) { return [nu, nu]; } const P = '__P__', DEF = __DEF__, R = 280; const FEEL = __FEEL__; const REPEL = {none: 0, poor: 10, decent: 26, great: 44}; const ZAP = {none: 0, poor: 46, decent: 92, great: 150}; const FLEE = {none: 0, poor: 18, decent: 34, great: 56}; // spike: tier-scaled flee duration (ticks) const PARA = {none: 0, poor: 22, decent: 46, great: 78}; // stinger: tier-scaled paralysis (ticks) if (!window.__mlKeys) { window.__mlKeys = {}; window.addEventListener('keydown', e => { window.__mlKeys[e.key.toLowerCase()] = true; if (e.key === ' ' && document.querySelector('.petri-dish:hover, .petri-dish:focus-within')) { e.preventDefault(); } }); window.addEventListener('keyup', e => { window.__mlKeys[e.key.toLowerCase()] = false; }); } window.__cl = window.__cl || {}; let M = window.__cl[P]; const newPred = () => { const a = Math.random() * 6.283, d = 200 + Math.random() * 70; return {x: Math.cos(a) * d, y: Math.sin(a) * d, vx: 0, vy: 0, r: 22 + Math.random() * 10, hp: 100, deg: 0, state: 'hunt', cd: 0}; }; if (!M || M.spec !== spec || M.resp !== (nResp || 0)) { M = window.__cl[P] = {spec: spec, resp: (nResp || 0), variance: window.CM.variance(), state: {x: 0, y: 0, vx: 0, vy: 0, deg: -90, r: 34, trail: [], cd: 0, _ph: 0}, preds: [newPred(), newPred(), newPred(), newPred()], repels: 0, kills: 0, hits: 0, flash: 0, flashEng: 0}; } // 'cilia'/'decent' = a navigable movement baseline; the combat parts (spike/stinger) // aren't in mv.partMove, so passing P/tier here would zero the weights and blank the feel. const eff = window.CM.computeStats(mv, spec, 'cilia', 'decent', M.variance); let dx = 0, dy = 0; if (jAngle != null && jDist) { const m = Math.min(1, jDist / 26), a = jAngle * Math.PI / 180; dx += Math.cos(a) * m; dy += -Math.sin(a) * m; } const k = window.__mlKeys || {}; if (k['w'] || k['arrowup']) { dy -= 1; } if (k['s'] || k['arrowdown']) { dy += 1; } if (k['a'] || k['arrowleft']) { dx -= 1; } if (k['d'] || k['arrowright']) { dx += 1; } if (dx || dy) { const mm = Math.hypot(dx, dy); if (mm > 1) { dx /= mm; dy /= mm; } } window.CM.step(M.state, {dx: dx, dy: dy}, eff); const px = M.state.x, py = M.state.y, pr = M.state.r, reach = DEF ? REPEL[tier] : ZAP[tier]; const repelsBefore = M.repels; // detect a repel/zap landing THIS tick (drives the flare flash) let zaps = []; let near = null, nd = 1e9; for (const e of M.preds) { if (e.state !== 'hunt') { continue; } const d = Math.hypot(e.x - px, e.y - py); if (d < nd) { nd = d; near = e; } } for (let i = M.preds.length - 1; i >= 0; i--) { const e = M.preds[i], tox = px - e.x, toy = py - e.y, td = Math.hypot(tox, toy) || 1; if (e.state === 'stun') { e.vx *= 0.5; e.vy *= 0.5; e.cd--; if (e.cd <= 0) { e.state = 'hunt'; } } // paralysed: frozen else if (e.state === 'flee') { e.vx += (-tox / td) * 0.5; e.vy += (-toy / td) * 0.5; e.cd--; if (e.cd <= 0) { e.state = 'hunt'; } } else { e.vx += (tox / td) * 0.16; e.vy += (toy / td) * 0.16; } const sp = Math.hypot(e.vx, e.vy), mx = 2.6; if (sp > mx) { e.vx *= mx / sp; e.vy *= mx / sp; } e.x += e.vx; e.y += e.vy; const dd = Math.hypot(e.x, e.y); if (dd > R - e.r) { const kk = (R - e.r) / dd; e.x *= kk; e.y *= kk; e.vx *= -0.5; e.vy *= -0.5; } if (Math.hypot(e.vx, e.vy) > 0.3) { e.deg = Math.atan2(e.vy, e.vx) * 180 / Math.PI; } if (DEF) { if (td < pr + e.r + reach) { if (reach <= 0) { M.hits++; M.flashEng = 6; } else { e.x = px - (tox / td) * (pr + e.r + reach); e.y = py - (toy / td) * (pr + e.r + reach); e.vx = -tox / td * 3; e.vy = -toy / td * 3; e.state = 'flee'; e.cd = FLEE[tier]; M.repels++; if (tier === 'great') { e.hp -= 34; zaps.push([px, py, e.x, e.y, '#ff6b6b']); } } } } else { if (e === near && reach > 0 && nd < pr + reach && M.state.cd <= 0) { e.state = 'stun'; e.cd = PARA[tier]; M.repels++; e.hp -= (tier === 'great' ? 60 : (tier === 'decent' ? 30 : 14)); zaps.push([px, py, e.x, e.y, '#ffd43b']); M.state.cd = (tier === 'great' ? 10 : 20); } } if (e.hp <= 0) { M.preds.splice(i, 1); M.kills++; } } if (M.state.cd > 0) { M.state.cd--; } while (M.preds.length < 4) { M.preds.push(newPred()); } // --- cosmetic feel state (NO physics change): flare on a fresh repel/zap, stinger charge read, threat ring --- M.state._ph = (M.state._ph || 0) + 0.4; if (M.repels > repelsBefore) { M.flash = 8; } if (M.flash > 0) { M.flash--; } const maxCd = (tier === 'great' ? 10 : 20); const charge = (!DEF && reach > 0) ? Math.max(0, Math.min(1, 1 - (M.state.cd / maxCd))) : 1; const danger = M.preds.some(e => e.state === 'hunt' && Math.hypot(e.x - px, e.y - py) < pr + e.r + reach + 34); let svg = '<svg width="100%" height="100%" viewBox="-300 -300 600 600" preserveAspectRatio="xMidYMid meet" style="overflow:visible">' + '<defs><filter id="cl-pl"><feFlood flood-color="#f03e3e" result="f"/><feComposite in="f" in2="SourceAlpha" operator="in"/></filter></defs>' + '<circle r="280" fill="none" stroke="rgba(51,153,255,0.18)" stroke-width="2"/>'; if (reach > 0) { // reach RADIUS highlight: filled disc + bright dashed ring + radial spokes (sectioned) const RR2 = pr + reach, rc = DEF ? '#f76707' : '#ffd43b'; svg += '<circle cx="' + px.toFixed(1) + '" cy="' + py.toFixed(1) + '" r="' + RR2.toFixed(1) + '" fill="' + (DEF ? 'rgba(247,103,7,0.08)' : 'rgba(255,212,59,0.08)') + '" stroke="' + rc + '" stroke-width="2" stroke-dasharray="7 6" opacity="0.7"/>'; for (let i = 0; i < 8; i++) { const a = i * Math.PI / 4; svg += '<line x1="' + (px + Math.cos(a) * pr).toFixed(1) + '" y1="' + (py + Math.sin(a) * pr).toFixed(1) + '" x2="' + (px + Math.cos(a) * RR2).toFixed(1) + '" y2="' + (py + Math.sin(a) * RR2).toFixed(1) + '" stroke="' + rc + '" stroke-width="1" opacity="0.22"/>'; } } for (const z of zaps) { svg += '<path d="M' + z[0].toFixed(1) + ',' + z[1].toFixed(1) + ' L' + ((z[0]+z[2])/2+8).toFixed(1) + ',' + ((z[1]+z[3])/2-8).toFixed(1) + ' L' + z[2].toFixed(1) + ',' + z[3].toFixed(1) + '" fill="none" stroke="' + z[4] + '" stroke-width="3" stroke-linecap="round"/>'; } for (const e of M.preds) { const dd = ((e.deg % 360) + 360) % 360, flip = (dd > 90 && dd < 270) ? -1 : 1; if (reach > 0 && e.state === 'hunt' && Math.hypot(e.x - px, e.y - py) < 1.4 * (pr + reach)) { svg += '<line x1="' + e.x.toFixed(1) + '" y1="' + e.y.toFixed(1) + '" x2="' + px.toFixed(1) + '" y2="' + py.toFixed(1) + '" stroke="#f03e3e" stroke-width="1" stroke-dasharray="3 5" opacity="0.45"/>'; } svg += '<g transform="translate(' + e.x.toFixed(1) + ',' + e.y.toFixed(1) + ') rotate(' + e.deg.toFixed(1) + ') scale(1,' + flip + ')"' + (e.state !== 'hunt' ? ' opacity="0.7"' : '') + '>' + (e.state === 'flee' ? '<circle r="' + (e.r + 4).toFixed(1) + '" fill="none" stroke="#ffd43b" stroke-width="1.5" opacity="0.6"/>' : '') + (e.state === 'stun' ? '<circle r="' + (e.r + 4).toFixed(1) + '" fill="none" stroke="#9775fa" stroke-width="2" stroke-dasharray="3 4" opacity="0.85"/>' : '') + '<image href="/assets/petri/cyclops.svg" x="' + (-e.r) + '" y="' + (-e.r) + '" width="' + (2*e.r) + '" height="' + (2*e.r) + '" filter="url(#cl-pl)"/></g>'; } // PLAYER = the actual specimen sprite (so changing the Specimen is VISIBLE) with the // adaptation overlaid on top (spike crown / stinger barb), tier-scaled & seated on the rim. const psp = (mv.specimens || []).find(s => s.value === spec) || (mv.specimens || [])[0] || {}; const img = psp.img || '/assets/petri/cyclops.svg'; const ddp = ((M.state.deg % 360) + 360) % 360, flp = (ddp > 90 && ddp < 270) ? -1 : 1; const fk = (M.flash > 0) ? (M.flash / 8) : 0; svg += '<g transform="translate(' + px.toFixed(1) + ',' + py.toFixed(1) + ') rotate(' + M.state.deg.toFixed(1) + ') scale(1,' + flp + ')">' + '<image href="' + img + '" x="' + (-pr) + '" y="' + (-pr) + '" width="' + (2 * pr) + '" height="' + (2 * pr) + '" preserveAspectRatio="xMidYMid meet"/>'; if (tier !== 'none') { if (DEF) { // SPIKE: omnidirectional crown; spines jut out on a repel then settle const sc = (pr / 38) * (1 + 0.18 * fk); svg += '<g transform="scale(' + sc.toFixed(3) + ')"' + (fk > 0 ? ' style="filter:drop-shadow(0 0 3px #ff6b6b)"' : '') + '>' + window.CV.partOverlay('spike', tier) + '</g>'; } else { // STINGER: forward barb + venom tip-glow that brightens with charge const sc = pr / 38, L = ({poor: 16, decent: 28, great: 40})[tier], hx = (36 + L) * sc; svg += '<circle cx="' + hx.toFixed(1) + '" cy="0" r="' + (2 + 5 * charge).toFixed(1) + '" fill="#ffd43b" opacity="' + (0.18 + 0.55 * charge).toFixed(2) + '"/>' + '<g transform="scale(' + sc.toFixed(3) + ')">' + window.CV.partOverlay('stinger', tier) + '</g>'; } } svg += '</g>'; // stinger CHARGE/cooldown ring — screen-anchored (outside the rotate group). Full green circle when ARMED. if (!DEF && reach > 0) { const rr = pr + 7; if (charge >= 0.999) { svg += '<circle cx="' + px.toFixed(1) + '" cy="' + py.toFixed(1) + '" r="' + rr + '" fill="none" stroke="#2bb673" stroke-width="2.5" opacity="0.85"/>'; } else { const a0 = -Math.PI / 2, a1 = a0 + 2 * Math.PI * charge, lg = (a1 - a0) > Math.PI ? 1 : 0; const x0 = px + Math.cos(a0) * rr, y0 = py + Math.sin(a0) * rr, x1 = px + Math.cos(a1) * rr, y1 = py + Math.sin(a1) * rr; svg += '<path d="M' + x0.toFixed(1) + ',' + y0.toFixed(1) + ' A' + rr + ',' + rr + ' 0 ' + lg + ' 1 ' + x1.toFixed(1) + ',' + y1.toFixed(1) + '" fill="none" stroke="#ffd43b" stroke-width="2.5" opacity="0.85" stroke-linecap="round"/>'; } } // pulsing DANGER ring when a hunter is closing if (danger) { const rp = pr + 8 + 3 * Math.sin(M.state._ph); svg += '<circle cx="' + px.toFixed(1) + '" cy="' + py.toFixed(1) + '" r="' + rp.toFixed(1) + '" fill="none" stroke="#f03e3e" stroke-width="2.5" opacity="' + (0.4 + 0.3 * Math.sin(M.state._ph)).toFixed(2) + '"/>'; } // ENGULF wash (defense, no spines): a red flash sells "nothing protecting you = you get eaten" if (DEF && reach <= 0 && M.flashEng > 0) { svg += '<circle r="280" fill="rgba(240,62,62,' + (0.20 * M.flashEng / 6).toFixed(3) + ')"/>'; M.flashEng--; } svg += '</svg>'; const host = document.getElementById('cl-__P__-arena'); if (host) { host.innerHTML = svg; } const stats = (DEF ? 'DEFENSE' : 'ATTACK') + '\\n' + 'TIER ' + tier + '\\n' + 'REACH ' + reach + '\\n' + (DEF ? 'FLEE ' + FLEE[tier] + 't' : 'PARALYSIS ' + PARA[tier] + 't') + '\\n' + (DEF ? 'REPELS ' : 'ZAPS ') + M.repels + '\\n' + 'KILLS ' + M.kills + (DEF && reach <= 0 ? '\\nENGULFED ' + M.hits : '') + (!DEF ? '\\nCHARGE ' + ((100 * charge) | 0) + '%' : '') + '\\nSTATUS ' + (reach <= 0 ? 'EXPOSED' : (danger ? 'THREAT' : 'CLEAR')); return [stats, FEEL[tier] || '']; } """.replace("__P__", P).replace("__DEF__", "true" if is_def else "false") \ .replace("__FEEL__", json.dumps(_COMBAT_FEEL[P])) clientside_callback( loop_js, Output(f"cl-{P}-stats", "children"), Output(f"cl-{P}-feel", "children"), Input(f"cl-{P}-tick", "n_intervals"), Input(f"cl-{P}-respawn", "n_clicks"), Input(f"cl-{P}-spec", "value"), Input(f"cl-{P}-tier", "value"), State(f"cl-{P}-mv", "data"), State(f"cl-{P}-joy", "angle"), State(f"cl-{P}-joy", "distance"), )
clientside_callback( """ function(_n) { if (!window.CV) { return window.dash_clientside.no_update; } document.querySelectorAll('[id*="ml-card"]').forEach(host => { let id = null; try { id = JSON.parse(host.id); } catch (e) { return; } if (id.part !== '__P__') { return; } host.innerHTML = (window.CV.renderAdaptPreview || window.CV.renderPart)(id.part, id.tier); }); return ''; } """.replace("__P__", P), Output(f"cl-{P}-sink", "data"), Input(f"cl-{P}-init", "n_intervals"), ) return component
============================ feeding labs ===================================
_FEED_DEFAULT_SPEC = {"filter": "daphnia", "jaw": "cyclops", "proboscis": "paramecium", "pseudopod": "amoeba"} _FEED_FEEL = { "none": "No mouth — you can't feed at all. Starving.", "poor": "Weak, clogged intake — barely keeps up; you starve in thin food.", "decent": "Reliable feeding at a steady rate.", "great": "High-throughput feeding — maximal intake.", }
def feeding_lab(part_id): """Isolated feed/attack/grip lab: drive a specimen and eat — but only your DIET's food (herbivore=plankton, carnivore=meat, omnivore=both), and only as well as the part's TIER allows. Also shows how your nature SKEWS a mutation roll toward the matching feed part (rare cross-diet).""" part = ADAPTATIONS[part_id] P = part_id diet = PART_DIET.get(P, "carnivore") edible = " + ".join(DIET_EDIBLE[diet]) defspec = _FEED_DEFAULT_SPEC.get(P, SPECIMENS[0]["value"])
badges = [dmc.Badge(part["role"], color="green" if part["role"] == "feed" else ("red" if part["role"] == "attack" else "teal"), variant="light", size="sm"), dmc.Badge(DIET_LABEL[diet], color=DIET_COLOR[diet], variant="dot", size="sm")]
dish = dmc.Paper( [ dmc.Group( [dmc.Text(part["label"].upper(), fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Group(badges, gap=6)], justify="space-between", mb="xs", ), html.Div( [ html.Div(html.Div(id=f"fl-{P}-arena", style={"position": "absolute", "inset": 0, "zIndex": 1}), className="petri-dish", style={"position": "absolute", "inset": 0, "borderRadius": "50%", "overflow": "hidden"}), html.Div( dg.DashRCJoystick(id=f"fl-{P}-joy", baseRadius=48, controllerRadius=22, directionCountMode="Nine", insideMode=True, throttle=40), style={"position": "absolute", "right": "2px", "bottom": "2px", "zIndex": 6, "padding": "6px", "borderRadius": "50%", "background": "rgba(255,255,255,0.55)", "backdropFilter": "blur(2px)", "boxShadow": "0 2px 10px rgba(15,80,72,0.25)"}, ), ], style={"position": "relative", "aspectRatio": "1 / 1", "width": "100%", "maxWidth": 460, "margin": "0 auto"}, ), dcc.Interval(id=f"fl-{P}-tick", interval=60, n_intervals=0), dcc.Interval(id=f"fl-{P}-init", interval=300, n_intervals=0, max_intervals=1), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 460px", "maxWidth": 540, "background": "linear-gradient(rgb(255 255 255 / 0%) 0%, rgb(243 247 239 / 30%) 100%)", "borderColor": "rgb(51 142 234 / 24%)"}, )
def _odd_cell(fp): return html.Div( [html.Div(html.Div(id=f"fl-{P}-bar-{fp}", className="eye-bar"), className="eye-bar-track"), dmc.Text("0%", id=f"fl-{P}-pct-{fp}", size="xs", fw=700, ta="center", style={"fontFamily": "monospace"}), dmc.Badge(ADAPTATIONS[fp]["label"].split()[0], color="brand", variant="light", size="xs", fullWidth=True)], id=f"fl-{P}-odd-{fp}", className="eye-odd", )
controls = dmc.Stack( [ dmc.Text("FEED IT", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Select(id=f"fl-{P}-spec", label="Specimen", value=defspec, allowDeselect=False, data=[{"value": s["value"], "label": s["label"]} for s in SPECIMENS], leftSection=DashIconify(icon="mdi:microscope")), dmc.Stack( [dmc.Text("ADAPTATION TIER", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.SegmentedControl(id=f"fl-{P}-tier", value="decent", fullWidth=True, color="brand", radius="md", size="xs", data=[{"value": t, "label": TIER_LABEL[t]} for t in ["none", "poor", "decent", "great"]])], gap=4), dmc.Text("", id=f"fl-{P}-feel", size="sm", fw=600, c=ACCENT), dmc.Paper( html.Pre("", id=f"fl-{P}-stats", style={"fontFamily": "monospace", "fontSize": "11px", "margin": 0, "whiteSpace": "pre-wrap", "color": "light-dark(#236bb3, #9ec9ff)"}), withBorder=True, radius="md", p="sm", style={"background": "light-dark(#eef5ff, #16202e)"}, ), dmc.Text("This specimen's nature skews which feed part it tends to evolve " "(cross-diet is rare but possible):", size="xs", c="dimmed"), html.Div([_odd_cell("filter"), _odd_cell("jaw"), _odd_cell("proboscis")], className="eye-odds-row"), dmc.Button("Roll a feed part", id=f"fl-{P}-roll", variant="light", color="brand", leftSection=DashIconify(icon="tabler:dice")), dmc.Text("", id=f"fl-{P}-roll-result", size="sm", fw=600), ], gap="sm", style={"flex": "1 1 250px", "maxWidth": 320}, )
component = dmc.Stack( [ dmc.Paper( [dmc.Text(part["label"] + " — FEEDING LAB", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text(part["function"] + ( " Your pseudopod arms GRAB nearby prey, REEL them in and engulf them — higher tiers " "reach farther and hold MORE at once (teal arms = active grips; the dashed ring is your " "grab range; ‘none’ can't grip)." if P == "pseudopod" else f" Diet guard-rail: a {DIET_LABEL[diet].lower()} can only eat " f"{edible}. Drive over your food; the dashed ring is your feeding reach (tier-scaled; " f"‘none’ can't feed). Greyed food isn't on your menu."), size="sm", c="dimmed")], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)", "background": "linear-gradient(rgb(255 255 255 / 0%) 0%, rgb(243 247 239 / 30%) 100%)"}, ), dmc.Group([dish, controls], align="flex-start", gap="xl", wrap="wrap"), dmc.Paper( [dmc.Text("TIER REFERENCE", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}, mb="xs"), dmc.SimpleGrid(cols={"base": 2, "sm": 4}, spacing="sm", children=[_tier_ref_card(P, v) for v in part["variants"]])], withBorder=True, radius="md", p="md", style={"borderColor": "rgb(51 142 234 / 24%)"}, ), dcc.Store(id=f"fl-{P}-mv", data=_movement_store()), dcc.Store(id=f"fl-{P}-diet", data=_diet_store()), dcc.Store(id=f"fl-{P}-sink"), ], gap="md", )
# driving + feeding loop loop_js = """ function(n, spec, tier, mv, jAngle, jDist) { const nu = window.dash_clientside.no_update; if (!window.CM || !window.CF || !mv) { return [nu, nu]; } const P = '__P__', DIET = '__DIET__'; const FFEEL = __FFEEL__; if (!window.__mlKeys) { window.__mlKeys = {}; window.addEventListener('keydown', e => { window.__mlKeys[e.key.toLowerCase()] = true; if (e.key === ' ' && document.querySelector('.petri-dish:hover, .petri-dish:focus-within')) { e.preventDefault(); } }); window.addEventListener('keyup', e => { window.__mlKeys[e.key.toLowerCase()] = false; }); } window.__fl = window.__fl || {}; let M = window.__fl[P]; if (!M || M.spec !== spec) { M = window.__fl[P] = {spec: spec, variance: window.CM.variance(), field: window.CF.newField(), energy: 0, recent: [], state: {x: 0, y: 0, vx: 0, vy: 0, deg: -90, r: 30, trail: [], cd: 0, _ph: 0}}; } const eff = window.CM.computeStats(mv, spec, 'cilia', 'decent', M.variance); // navigable baseline let dx = 0, dy = 0; if (jAngle != null && jDist) { const m = Math.min(1, jDist / 26), a = jAngle * Math.PI / 180; dx += Math.cos(a) * m; dy += -Math.sin(a) * m; } const k = window.__mlKeys || {}; if (k['w'] || k['arrowup']) { dy -= 1; } if (k['s'] || k['arrowdown']) { dy += 1; } if (k['a'] || k['arrowleft']) { dx -= 1; } if (k['d'] || k['arrowright']) { dx += 1; } if (dx || dy) { const mm = Math.hypot(dx, dy); if (mm > 1) { dx /= mm; dy /= mm; } } window.CM.step(M.state, {dx: dx, dy: dy}, eff); // PROBOSCIS feeds with a SINGLE, AIMED, RHYTHMIC strike — NOT the omni-reach // auto-eat — so it skips CF.step's eating (tier 'none') and runs its own // state machine: lock one target inside a forward cone, extend the tube, // grab it, retract, then recharge. You aim by steering (the cell faces its // heading); higher tiers reach farther, aim wider, and recharge faster. const isProb = (P === 'proboscis'), isJaw = (P === 'jaw'), isPseud = (P === 'pseudopod'); let got = window.CF.step(M.state, M.field, DIET, (isProb || isJaw || isPseud) ? 'none' : tier); let tube = ''; if (isProb) { const PROB = {none: 0, poor: 80, decent: 130, great: 180}; // strike reach const PCONE = {none: 0, poor: 20, decent: 28, great: 34}; // aim half-angle (deg) const PCD = {none: 0, poor: 70, decent: 48, great: 32}; // recharge ticks const PEXT = {none: 1, poor: 10, decent: 8, great: 6}; // extend ticks const PRET = 5; // retract ticks const reach = PROB[tier], cone = PCONE[tier], r0 = M.state.r || 30; const cx = M.state.x, cy = M.state.y, head = (M.state.deg || 0) * Math.PI / 180; const pr = M.probe = M.probe || {phase: 'idle', cd: 0, t: 0, len: 0, ang: 0, td: 0, target: null}; if (pr.cd > 0) { pr.cd--; } // IDLE + ready + reach: lock the nearest edible inside the FORWARD cone. if (pr.phase === 'idle' && reach > 0 && pr.cd <= 0) { let best = null, bd = reach + r0; const scan = (arr, kind) => { if (!window.CF.canEat(DIET, kind)) { return; } for (const o of arr) { const dx = o.x - cx, dy = o.y - cy, d = Math.hypot(dx, dy); if (d > r0 && d < bd) { const rel = Math.abs((((Math.atan2(dy, dx) - head) * 180 / Math.PI) % 360 + 540) % 360 - 180); if (rel <= cone) { bd = d; best = {o: o, arr: arr, kind: kind, ang: Math.atan2(dy, dx), d: d}; } } } }; scan(M.field.plankton, 'plankton'); scan(M.field.prey, 'meat'); if (best) { pr.phase = 'extending'; pr.t = 0; pr.ang = best.ang; pr.td = best.d; pr.target = best; } } if (pr.phase === 'extending') { pr.t++; pr.len = r0 + (pr.td - r0) * Math.min(1, pr.t / PEXT[tier]); if (pr.t >= PEXT[tier]) { // committed strike lands — grab it if still there const b = pr.target, ix = b ? b.arr.indexOf(b.o) : -1; if (ix >= 0) { b.arr.splice(ix, 1); got += (b.kind === 'meat' ? 5 : 1) * (window.CF.FEED[tier] || {rate: 1}).rate; } pr.phase = 'retracting'; pr.t = 0; } } else if (pr.phase === 'retracting') { pr.t++; pr.len = r0 + (pr.td - r0) * Math.max(0, 1 - pr.t / PRET); if (pr.t >= PRET) { pr.phase = 'idle'; pr.len = 0; pr.cd = PCD[tier]; pr.target = null; } } // draw: the live tube while striking, else a faint aim guide along heading. if (pr.len > 0) { const bx = cx + Math.cos(pr.ang) * r0, by = cy + Math.sin(pr.ang) * r0; const tx = cx + Math.cos(pr.ang) * pr.len, ty = cy + Math.sin(pr.ang) * pr.len; const w = tier === 'great' ? 6 : (tier === 'decent' ? 5 : 3.5); tube = '<line x1="' + bx.toFixed(1) + '" y1="' + by.toFixed(1) + '" x2="' + tx.toFixed(1) + '" y2="' + ty.toFixed(1) + '" stroke="#3399ff" stroke-width="' + w + '" stroke-linecap="round" opacity="0.95"/>' + '<circle cx="' + tx.toFixed(1) + '" cy="' + ty.toFixed(1) + '" r="8" fill="rgba(51,153,255,0.25)" stroke="#3399ff" stroke-width="2.5"/>'; } else if (reach > 0) { const ready = pr.cd <= 0, R1 = r0 + reach; const col = ready ? 'rgba(51,153,255,0.55)' : 'rgba(130,150,170,0.30)'; const ex = cx + Math.cos(head) * R1, ey = cy + Math.sin(head) * R1; const ep = (s) => { const a = head + s * cone * Math.PI / 180; return [cx + Math.cos(a) * R1, cy + Math.sin(a) * R1]; }; const p1 = ep(1), p2 = ep(-1); tube = '<path d="M' + cx.toFixed(1) + ' ' + cy.toFixed(1) + ' L' + p1[0].toFixed(1) + ' ' + p1[1].toFixed(1) + ' A' + R1.toFixed(1) + ' ' + R1.toFixed(1) + ' 0 0 0 ' + p2[0].toFixed(1) + ' ' + p2[1].toFixed(1) + ' Z" fill="' + (ready ? 'rgba(51,153,255,0.10)' : 'rgba(130,150,170,0.05)') + '" stroke="' + col + '" stroke-width="1.5" stroke-opacity="0.45"/>' + '<line x1="' + cx.toFixed(1) + '" y1="' + cy.toFixed(1) + '" x2="' + ex.toFixed(1) + '" y2="' + ey.toFixed(1) + '" stroke="' + col + '" stroke-width="2" stroke-dasharray="6 7"/>'; } M.probeReach = reach; M.probeCone = cone; } // JAW is a CARNIVORE melee LUNGE-STRIKE — you aim by steering your nose at live // prey; the cell darts forward, the mandibles GAPE then CRUNCH shut, popping the // meat caught in a short FRONT bite arc, then a committed recovery cooldown. // Distinct from the proboscis's long single snipe; skips CF.step's omni-eat. let jaw = ''; if (isJaw) { const JREACH = {none: 0, poor: 14, decent: 24, great: 34}; // bite range past cell edge (px) const JARC = {none: 0, poor: 32, decent: 46, great: 60}; // bite half-angle (deg) const JLUNGE = {none: 0, poor: 4, decent: 6, great: 8}; // forward velocity nudge const JCD = {none: 0, poor: 60, decent: 42, great: 28}; // recovery ticks after a snap const JDMG = {none: 0, poor: 0.6, decent: 1.0, great: 1.4}; // energy multiplier per kill const JMAXR = {none: 0, poor: 12, decent: 15, great: 99}; // max bitable prey radius (prey r is 8-16: poor takes only the smaller half, decent all but the biggest, great any) const JMAX = {none: 0, poor: 1, decent: 2, great: 3}; // prey crunched per snap (AoE cap) const JSLIP = 0.35, JGAPE = 3, JSNAP = 2, FLASH = 6; const reach = JREACH[tier], arc = JARC[tier], r0 = M.state.r || 30; const rate2 = (window.CF.FEED[tier] || {rate: 1}).rate, RB = window.CF.R || 280; const cx = M.state.x, cy = M.state.y, head = (M.state.deg || 0) * Math.PI / 180; const jw = M.jaw = M.jaw || {phase: 'idle', t: 0, cd: 0, hits: 0, lastBite: 0, killX: 0, killY: 0, locked: []}; if (jw.cd > 0) { jw.cd--; } const inArc = (o) => { const dx = o.x - cx, dy = o.y - cy, d = Math.hypot(dx, dy); if (d > r0 + reach + (o.r || 0)) { return false; } const rel = Math.abs((((Math.atan2(dy, dx) - head) * 180 / Math.PI) % 360 + 540) % 360 - 180); return rel <= arc; }; const bitable = (o) => (o.r || 8) <= JMAXR[tier]; // working radius gate (poor takes only smaller prey) // TRIGGER: idle + ready + reach; auto-fires on a bitable on-arc prey, or Space force-fires (may whiff). if (jw.phase === 'idle' && jw.cd <= 0 && reach > 0) { const forced = !!(window.__mlKeys || {})[' ']; // commit to the nearest bitable on-arc prey AT TRIGGER (up to JMAX), like the // proboscis locks its target — so a well-aimed bite lands instead of the prey // drifting out during the gape windup. Space force-fires even with nothing locked. let locked = []; if (window.CF.canEat(DIET, 'meat')) { // carnivore lock: plankton ignored const cand = []; for (const o of M.field.prey) { if (inArc(o) && bitable(o)) { cand.push({o: o, d: Math.hypot(o.x - cx, o.y - cy)}); } } cand.sort((a, b) => a.d - b.d); locked = cand.slice(0, JMAX[tier]).map(c => c.o); } if (locked.length > 0 || forced) { jw.phase = 'gape'; jw.t = 0; jw.hits = 0; jw.locked = locked; // bounded lunge: only nudge forward when well inside the dish, then clamp speed, // so a wall-bounce (vx,vy*=-0.4) can't spin the velocity-follow heading. if (Math.hypot(cx, cy) < RB * 0.82) { M.state.vx += Math.cos(head) * JLUNGE[tier]; M.state.vy += Math.sin(head) * JLUNGE[tier]; const spd = Math.hypot(M.state.vx, M.state.vy), CAP = 14; if (spd > CAP) { M.state.vx *= CAP / spd; M.state.vy *= CAP / spd; } } } } else if (jw.phase === 'gape') { jw.t++; if (jw.t >= JGAPE) { jw.phase = 'snap'; jw.t = 0; } } else if (jw.phase === 'snap') { jw.t++; if (jw.t >= JSNAP) { // the CRUNCH lands on the final snap tick: kill the committed targets let killed = 0; const locked = jw.locked || []; for (let i = 0; i < locked.length && killed < JMAX[tier]; i++) { const o = locked[i], ix = M.field.prey.indexOf(o); // re-derive each splice; guards stale refs if (ix < 0) { continue; } // target already gone if (tier === 'poor' && Math.random() < JSLIP) { continue; } // poor: bite slips off (cd still burns) M.field.prey.splice(ix, 1); killed++; got += 5 * JDMG[tier] * rate2; // per-kill energy (folds poor 0.6x / great 1.4x) jw.killX = o.x; jw.killY = o.y; } jw.hits = killed; jw.locked = []; jw.lastBite = killed > 0 ? FLASH : 0; jw.phase = 'idle'; jw.t = 0; jw.cd = JCD[tier]; // committed recovery (whiffs still cost) } } if (jw.lastBite > 0) { jw.lastBite--; } M.jawReach = reach; M.jawArc = arc; // ---- RENDER: bite-arc guide + animated mandibles + lunge streaks + crunch burst ---- const f = (v) => v.toFixed(1); const hx = cx + Math.cos(head) * r0, hy = cy + Math.sin(head) * r0; // mandible hinge at the leading edge if (reach > 0) { // bite-arc wedge: bright red when ready, dimmed grey during recovery (telegraph) const R1 = r0 + reach, ready = (jw.cd <= 0 && jw.phase === 'idle'); const fill = jw.cd > 0 ? 'rgba(130,150,170,0.05)' : (ready ? 'rgba(240,62,62,0.10)' : 'rgba(240,62,62,0.06)'); const edge = jw.cd > 0 ? 'rgba(130,150,170,0.30)' : 'rgba(240,62,62,0.55)'; const ep = (s) => { const a = head + s * arc * Math.PI / 180; return [cx + Math.cos(a) * R1, cy + Math.sin(a) * R1]; }; const p1 = ep(1), p2 = ep(-1), ex = cx + Math.cos(head) * R1, ey = cy + Math.sin(head) * R1; jaw += '<path d="M' + f(cx) + ' ' + f(cy) + ' L' + f(p1[0]) + ' ' + f(p1[1]) + ' A' + f(R1) + ' ' + f(R1) + ' 0 0 0 ' + f(p2[0]) + ' ' + f(p2[1]) + ' Z" fill="' + fill + '" stroke="none"/>' + '<line x1="' + f(cx) + '" y1="' + f(cy) + '" x2="' + f(ex) + '" y2="' + f(ey) + '" stroke="' + edge + '" stroke-width="2" stroke-dasharray="6 7"/>'; } // animated mandibles: rest slightly parted, GAPE wide during the dart, SLAM to ~0 on the snap. const baseDeg = Math.min(arc, 60); let gaFrac = 0.25; if (jw.phase === 'gape') { gaFrac = 0.25 + 0.75 * (jw.t / JGAPE); } else if (jw.phase === 'snap') { gaFrac = Math.max(0, 1 - jw.t / JSNAP); } const ga = baseDeg * gaFrac * Math.PI / 180; const L = reach * 0.9 + 12, w = arc >= 60 ? 6 : (arc >= 46 ? 4.5 : 3), teeth = arc >= 60 ? 3 : 2; const mandible = (sign) => { const a = head + sign * ga, perp = a + sign * Math.PI / 2; const tipx = hx + Math.cos(a) * L, tipy = hy + Math.sin(a) * L; const mcx = hx + Math.cos(a) * L * 0.5 + Math.cos(perp) * 6, mcy = hy + Math.sin(a) * L * 0.5 + Math.sin(perp) * 6; let m = '<path d="M' + f(hx) + ' ' + f(hy) + ' Q' + f(mcx) + ' ' + f(mcy) + ' ' + f(tipx) + ' ' + f(tipy) + '" fill="none" stroke="#f03e3e" stroke-width="' + w + '" stroke-linecap="round"/>'; for (let i = 1; i <= teeth; i++) { const tt = i / (teeth + 1), bx = hx + Math.cos(a) * L * tt, by = hy + Math.sin(a) * L * tt, tl = 4 + w * 0.4; m += '<line x1="' + f(bx) + '" y1="' + f(by) + '" x2="' + f(bx + Math.cos(perp) * tl) + '" y2="' + f(by + Math.sin(perp) * tl) + '" stroke="#f03e3e" stroke-width="' + (w * 0.7).toFixed(1) + '" stroke-linecap="round"/>'; } return m; }; if (reach > 0) { jaw += mandible(1) + mandible(-1); } // lunge streaks behind the cell during the dart (pure render flourish, no physics). if (jw.phase === 'gape') { for (let i = -1; i <= 1; i++) { const off = i * 8, bx = cx - Math.cos(head) * (r0 + 4) + Math.cos(head + Math.PI / 2) * off, by = cy - Math.sin(head) * (r0 + 4) + Math.sin(head + Math.PI / 2) * off; const tx = bx - Math.cos(head) * 14, ty = by - Math.sin(head) * 14; jaw += '<line x1="' + f(bx) + '" y1="' + f(by) + '" x2="' + f(tx) + '" y2="' + f(ty) + '" stroke="rgba(240,62,62,0.4)" stroke-width="2.5" stroke-linecap="round"/>'; } } // crunch burst (#ffd43b star) + white shock ring at the contact point on a kill. if (jw.lastBite > 0) { const op = jw.lastBite / 6, bx = jw.killX || hx, by = jw.killY || hy; for (let i = 0; i < 6; i++) { const a = i * Math.PI / 3, r1 = 4, r2 = 12 + (6 - jw.lastBite) * 2; jaw += '<line x1="' + f(bx + Math.cos(a) * r1) + '" y1="' + f(by + Math.sin(a) * r1) + '" x2="' + f(bx + Math.cos(a) * r2) + '" y2="' + f(by + Math.sin(a) * r2) + '" stroke="#ffd43b" stroke-width="2.5" stroke-linecap="round" opacity="' + op.toFixed(2) + '"/>'; } const rr = 10 + (6 - jw.lastBite) * 4; jaw += '<circle cx="' + f(bx) + '" cy="' + f(by) + '" r="' + f(rr) + '" fill="none" stroke="rgba(255,255,255,' + (op * 0.7).toFixed(2) + ')" stroke-width="2"/>'; } } // PSEUDOPOD is GRIP — it throws out arms that latch nearby prey, REEL them in and // engulf them at the mouth. Higher tiers reach farther, hold MORE prey at once, and // reel faster. (The DEFENSIVE twin — a predator latching YOU — is the game's Phase D // grapple.) Skips CF.step's omni-eat; intake comes from engulfing the reeled-in prey. let arms = ''; if (isPseud) { const PREACH = {none: 0, poor: 55, decent: 95, great: 140}; // grab reach past the cell edge const PGRIPS = {none: 0, poor: 1, decent: 2, great: 3}; // simultaneous grips const REEL = {none: 0, poor: 0.7, decent: 1.1, great: 1.6}; // reel-in acceleration const reach = PREACH[tier], maxG = PGRIPS[tier], r0 = M.state.r || 30; const cx = M.state.x, cy = M.state.y, rate2 = (window.CF.FEED[tier] || {rate: 1}).rate; const grips = M.grips = (M.grips || []).filter(o => M.field.prey.indexOf(o) >= 0); // drop vanished prey if (reach > 0 && window.CF.canEat(DIET, 'meat')) { while (grips.length < maxG) { // fill free grips with the nearest ungripped prey in reach let best = null, bd = r0 + reach; for (const o of M.field.prey) { if (grips.indexOf(o) >= 0) { continue; } const d = Math.hypot(o.x - cx, o.y - cy); if (d < bd) { bd = d; best = o; } } if (!best) { break; } grips.push(best); } } for (let i = grips.length - 1; i >= 0; i--) { const o = grips[i], dx = cx - o.x, dy = cy - o.y, d = Math.hypot(dx, dy) || 1; o.vx = (o.vx || 0) * 0.78 + (dx / d) * REEL[tier]; // reeled toward the mouth o.vy = (o.vy || 0) * 0.78 + (dy / d) * REEL[tier]; const spp = Math.hypot(o.vx, o.vy); if (spp > 4) { o.vx *= 4 / spp; o.vy *= 4 / spp; } o.r = Math.max(4, o.r - 0.12); // drained → visibly shrinks while held if (d < r0 * 0.7 || o.r <= 4) { // reeled to the mouth (or fully drained) → engulf const ix = M.field.prey.indexOf(o); if (ix >= 0) { M.field.prey.splice(ix, 1); got += 5 * rate2; } grips.splice(i, 1); } } M.pseudReach = reach; M.pseudGrips = maxG; for (const o of grips) { // render a tapered teal arm to each gripped prey + a draining ring const dx = o.x - cx, dy = o.y - cy, ang = Math.atan2(dy, dx); const bx = cx + Math.cos(ang) * r0, by = cy + Math.sin(ang) * r0, perp = ang + Math.PI / 2; const mx = (bx + o.x) / 2 + Math.cos(perp) * 7, my = (by + o.y) / 2 + Math.sin(perp) * 7; const w = tier === 'great' ? 7 : (tier === 'decent' ? 5.5 : 4); arms += '<path d="M' + bx.toFixed(1) + ' ' + by.toFixed(1) + ' Q' + mx.toFixed(1) + ' ' + my.toFixed(1) + ' ' + o.x.toFixed(1) + ' ' + o.y.toFixed(1) + '" fill="none" stroke="#12b886" stroke-width="' + w + '" stroke-linecap="round" opacity="0.9"/>' + '<circle cx="' + o.x.toFixed(1) + '" cy="' + o.y.toFixed(1) + '" r="' + (o.r + 3).toFixed(1) + '" fill="none" stroke="#63e6be" stroke-width="2" stroke-dasharray="3 3" opacity="0.85"/>'; } if (reach > 0 && !grips.length) { // idle: show the grab radius arms += '<circle cx="' + cx.toFixed(1) + '" cy="' + cy.toFixed(1) + '" r="' + (r0 + reach).toFixed(1) + '" fill="rgba(18,184,134,0.05)" stroke="#12b886" stroke-width="1.5" stroke-dasharray="6 7" opacity="0.5"/>'; } } M.energy += got; M.recent.push(got); if (M.recent.length > 30) { M.recent.shift(); } const rate = M.recent.reduce((a, b) => a + b, 0); const sp = (mv.specimens.find(s => s.value === spec) || {}); const host = document.getElementById('fl-__P__-arena'); if (host) { host.innerHTML = window.CF.render(M.state, M.field, sp.img || '', DIET, (isProb || isJaw || isPseud) ? 'none' : tier).replace('</svg>', tube + jaw + arms + '</svg>'); } let stats; if (isProb) { const pb = M.probe || {phase: 'idle', cd: 0}; const strike = pb.phase === 'extending' ? 'STRIKING' : (pb.phase === 'retracting' ? 'retracting' : (pb.cd > 0 ? 'recharge ' + pb.cd : 'ready')); stats = 'DIET ' + DIET + ' (eats __EDIBLE__)\\n' + 'TIER ' + tier + '\\n' + 'REACH ' + (M.probeReach || 0).toFixed(0) + ' (aimed, cone \\u00b1' + (M.probeCone || 0) + '\\u00b0)\\n' + 'STRIKE ' + strike + '\\n' + 'INTAKE/s ' + rate.toFixed(1) + '\\n' + 'ENERGY ' + M.energy.toFixed(0); } else if (isJaw) { const jb = M.jaw || {phase: 'idle', cd: 0, t: 0, hits: 0, lastBite: 0}; const bite = jb.phase === 'gape' ? 'LUNGING' : jb.phase === 'snap' ? 'CHOMP!' : ((jb.lastBite > 0 && jb.hits > 0) ? 'SNAP! +' + jb.hits : (jb.cd > 0 ? 'recharge ' + jb.cd : 'ready')); stats = 'DIET ' + DIET + ' (eats __EDIBLE__)\\n' + 'TIER ' + tier + '\\n' + 'REACH ' + (M.jawReach || 0).toFixed(0) + ' (melee, bite \\u00b1' + (M.jawArc || 0) + '\\u00b0)\\n' + 'BITE ' + bite + '\\n' + 'INTAKE/s ' + rate.toFixed(1) + '\\n' + 'ENERGY ' + M.energy.toFixed(0); } else if (isPseud) { const ng = (M.grips || []).length; stats = 'DIET ' + DIET + ' (eats __EDIBLE__)\\n' + 'TIER ' + tier + '\\n' + 'REACH ' + (M.pseudReach || 0).toFixed(0) + ' (grip, holds ' + (M.pseudGrips || 0) + ')\\n' + 'GRIPPED ' + ng + ' / ' + (M.pseudGrips || 0) + '\\n' + 'INTAKE/s ' + rate.toFixed(1) + '\\n' + 'ENERGY ' + M.energy.toFixed(0); } else { stats = 'DIET ' + DIET + ' (eats __EDIBLE__)\\n' + 'TIER ' + tier + '\\n' + 'REACH ' + window.CF.reach(tier).toFixed(0) + '\\n' + 'INTAKE/s ' + rate.toFixed(1) + '\\n' + 'ENERGY ' + M.energy.toFixed(0); } return [stats, FFEEL[tier] || '']; } """.replace("__P__", P).replace("__DIET__", diet).replace("__EDIBLE__", edible) \ .replace("__FFEEL__", json.dumps(_FEED_FEEL)) clientside_callback( loop_js, Output(f"fl-{P}-stats", "children"), Output(f"fl-{P}-feel", "children"), Input(f"fl-{P}-tick", "n_intervals"), Input(f"fl-{P}-spec", "value"), Input(f"fl-{P}-tier", "value"), State(f"fl-{P}-mv", "data"), State(f"fl-{P}-joy", "angle"), State(f"fl-{P}-joy", "distance"), )
# skewed mutation odds + roll (diet -> which feed part) roll_js = """ function(spec, nRoll, diet) { const nu = window.dash_clientside.no_update; if (!diet) { return nu; } const P = '__P__'; const nature = (diet.specimenDiet || {})[spec] || 'omnivore'; const odds = (diet.skew || {})[nature] || {}; const parts = ['filter', 'jaw', 'proboscis']; parts.forEach(fp => { const pct = Math.round((odds[fp] || 0) * 100); const bar = document.getElementById('fl-' + P + '-bar-' + fp); const lab = document.getElementById('fl-' + P + '-pct-' + fp); const cell = document.getElementById('fl-' + P + '-odd-' + fp); if (bar) { bar.style.height = Math.max(3, pct) + '%'; } if (lab) { lab.textContent = pct + '%'; } if (cell) { cell.classList.remove('eye-win'); } }); const ctx = window.dash_clientside.callback_context; const trig = (ctx.triggered[0] || {}).prop_id || ''; if (trig.indexOf('-roll') < 0) { return nu; } // odds refresh only let r = Math.random(), acc = 0, got = parts[0]; for (const fp of parts) { acc += (odds[fp] || 0); if (r <= acc) { got = fp; break; } } const tiers = ['poor', 'decent', 'great'], tier = tiers[Math.random() * 3 | 0]; const cell = document.getElementById('fl-' + P + '-odd-' + got); if (cell) { cell.classList.add('eye-win'); } const partDiet = (diet.partDiet || {})[got]; const cross = partDiet && partDiet !== nature && nature !== 'omnivore'; const names = {filter: 'Filter mouth', jaw: 'Jaw', proboscis: 'Proboscis'}; return '\\ud83e\\uddec ' + nature + ' \\u2192 ' + names[got] + ' (' + tier + ')' + (cross ? ' \\u2014 a rare cross-diet mutation!' : ''); } """.replace("__P__", P) clientside_callback( roll_js, Output(f"fl-{P}-roll-result", "children"), Input(f"fl-{P}-spec", "value"), Input(f"fl-{P}-roll", "n_clicks"), State(f"fl-{P}-diet", "data"), )
# fill tier reference schematics on mount clientside_callback( """ function(_n) { if (!window.CV) { return window.dash_clientside.no_update; } document.querySelectorAll('[id*="ml-card"]').forEach(host => { let id = null; try { id = JSON.parse(host.id); } catch (e) { return; } if (id.part !== '__P__') { return; } host.innerHTML = (window.CV.renderAdaptPreview || window.CV.renderPart)(id.part, id.tier); }); return ''; } """.replace("__P__", P), Output(f"fl-{P}-sink", "data"), Input(f"fl-{P}-init", "n_intervals"), )
return component ```
:defaultExpanded: false :withExpandedButton: true
---
*Source: /adaptation-pseudopod*
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /adaptation-pseudopod/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt