@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +178 -9
  2. package/corpus/reference/index.json.gz +0 -0
  3. package/corpus/reference/manifest.json +8 -8
  4. package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
  5. package/corpus/sprites/src/sprite-facts.jsonl +401 -0
  6. package/corpus/worlds/index.json.gz +0 -0
  7. package/corpus/worlds/manifest.json +49 -9
  8. package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
  9. package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
  10. package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
  11. package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
  12. package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
  13. package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
  14. package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
  15. package/corpus/worlds/src/mud-hollow.jsonl +82 -0
  16. package/corpus/worlds/src/mud-warren.jsonl +124 -0
  17. package/corpus/worlds/src/spider-fly.jsonl +1 -1
  18. package/data/sprites/animal-icon.toml +11 -5
  19. package/data/sprites/book-icon.toml +9 -5
  20. package/data/sprites/cabinet-icon.toml +10 -5
  21. package/data/sprites/cellar-icon.toml +16 -6
  22. package/data/sprites/container-icon.toml +7 -3
  23. package/data/sprites/desk-icon.toml +8 -5
  24. package/data/sprites/dog-icon.toml +8 -5
  25. package/data/sprites/dog-with-colour-icon.toml +13 -12
  26. package/data/sprites/drawing-room-icon.toml +14 -5
  27. package/data/sprites/egg-icon.toml +6 -3
  28. package/data/sprites/fly-icon.toml +10 -5
  29. package/data/sprites/furniture-icon.toml +5 -3
  30. package/data/sprites/garden-icon.toml +10 -3
  31. package/data/sprites/key-icon.toml +3 -1
  32. package/data/sprites/kitchen-icon.toml +15 -6
  33. package/data/sprites/lamp-icon.toml +9 -4
  34. package/data/sprites/letter-icon.toml +7 -4
  35. package/data/sprites/library-icon.toml +14 -3
  36. package/data/sprites/pan-icon.toml +7 -3
  37. package/data/sprites/person-icon.toml +6 -2
  38. package/data/sprites/poodle-icon.toml +1 -1
  39. package/data/sprites/portable-icon.toml +6 -4
  40. package/data/sprites/portrait-icon.toml +7 -4
  41. package/data/sprites/room-icon.toml +11 -2
  42. package/data/sprites/spider-icon.toml +9 -3
  43. package/data/sprites/study-icon.toml +10 -2
  44. package/package.json +5 -4
  45. package/src/adapters/memory/core.mjs +20 -0
  46. package/src/domain/ask-vocab.mjs +71 -0
  47. package/src/domain/ask.mjs +168 -0
  48. package/src/domain/game-config.mjs +11 -0
  49. package/src/domain/grammar/ace.mjs +40 -4
  50. package/src/domain/grammar/lexicon-core.json +2 -1
  51. package/src/domain/grammar/lexicon.mjs +18 -0
  52. package/src/domain/mud-facts.mjs +15 -0
  53. package/src/domain/reference-pack.mjs +31 -7
  54. package/src/domain/router/drive.mjs +35 -9
  55. package/src/domain/router/registry.mjs +24 -4
  56. package/src/domain/router/resolver.mjs +102 -40
  57. package/src/domain/scene-compose.mjs +117 -0
  58. package/src/domain/spider-fly-world.mjs +37 -1
  59. package/src/domain/sprite-facts.mjs +0 -0
  60. package/src/domain/sprite-request.mjs +156 -0
  61. package/src/domain/sprite-templates.mjs +169 -20
  62. package/src/services/adventure-editor.mjs +8 -14
  63. package/src/services/adventure-viz.mjs +209 -157
  64. package/src/services/adventure.mjs +526 -391
  65. package/src/services/chat-page-viz.mjs +69 -25
  66. package/src/services/chat.mjs +200 -51
  67. package/src/services/code-explorer-viz.mjs +102 -62
  68. package/src/services/extract-facts.mjs +4 -7
  69. package/src/services/ingest-viz.mjs +68 -82
  70. package/src/services/ledger-viz.mjs +136 -67
  71. package/src/services/memory-panel-viz.mjs +62 -0
  72. package/src/services/mud-editor.mjs +10 -15
  73. package/src/services/mud-turn.mjs +6 -6
  74. package/src/services/mud-viz.mjs +1016 -208
  75. package/src/services/p2p-room.mjs +90 -23
  76. package/src/services/plan-pddl.mjs +3 -1
  77. package/src/services/plan-viz.mjs +123 -64
  78. package/src/services/research-viz.mjs +160 -108
  79. package/src/services/spider-fly-turn.mjs +15 -23
  80. package/src/services/spider-fly-viz.mjs +146 -161
  81. package/src/services/spider-fly.mjs +69 -11
  82. package/src/services/sprite-catalog-viz.mjs +414 -240
  83. package/src/services/viz-boot.mjs +71 -0
  84. package/src/services/viz-room-graph.mjs +203 -0
  85. package/src/services/viz-theme.mjs +90 -1
  86. package/src/services/viz-ticker.mjs +22 -0
  87. package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
  88. package/src/surfaces/web/chat-browser-entry.mjs +30 -105
  89. package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
  90. package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
  91. package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
  92. package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
  93. package/src/surfaces/web/memory-stats.mjs +11 -0
  94. package/src/surfaces/web/mud-browser-entry.mjs +71 -29
  95. package/src/surfaces/web/plan-browser-entry.mjs +22 -40
  96. package/src/surfaces/web/research-browser-entry.mjs +26 -41
  97. package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
  98. package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
  99. package/src/surfaces/web/turn-session.mjs +120 -0
  100. package/src/tools/definitions.mjs +30 -0
  101. package/src/tools/handlers/index.mjs +6 -3
  102. package/src/tools/handlers/kit.mjs +19 -2
  103. package/src/tools/handlers/tmct-ask.mjs +11 -6
  104. package/src/tools/handlers/tmct-ingest.mjs +5 -1
  105. package/src/tools/handlers/tmct-related.mjs +4 -4
  106. package/src/tools/handlers/tmct-sprite.mjs +147 -0
  107. package/src/tools/memory-fallthrough.mjs +9 -2
  108. package/src/tools/server.mjs +25 -1
@@ -22,7 +22,8 @@
22
22
  // `.toString()` — exactly ledger-viz.mjs's own `facetCounts`/
23
23
  // `resolveAnsweredTerm` pattern — because they are genuinely UI-only, with no
24
24
  // reason to live inside the engine bundle: `createTicker` (viz-ticker.mjs,
25
- // the shared play/pause/step/reset primitive), `classOfAgentId`,
25
+ // the shared play/pause/step/reset primitive), `agentKindOf` (the shared
26
+ // kind-from-id helper, defined once in spider-fly-world.mjs),
26
27
  // `threadCellsForSpiderPlan` (the silk-thread reconstruction), `nextCorpses`
27
28
  // (the dying-agent bookkeeping behind the dusty-corner corpse pile) and
28
29
  // `facingDegreesFor` (plan-driven sprite orientation) — all kept as real,
@@ -32,11 +33,14 @@
32
33
  // bundle's own real ES exports instead, since (unlike ledger-viz, which
33
34
  // reuses a FIXED shared bundle it can't extend for one page's own needs) this
34
35
  // page ships its own dedicated bundle and can just export what it needs.
35
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
36
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, meterBarHtml } from "./viz-theme.mjs";
36
37
  import { createTicker } from "./viz-ticker.mjs";
37
- import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId } from "../domain/spider-fly-world.mjs";
38
+ import { loadWinkVendor } from "./viz-boot.mjs";
39
+ import { GRID_SIZE, WEB_HOME, WEB_RADIUS, isInWebBlock, cellId, agentKindOf } from "../domain/spider-fly-world.mjs";
38
40
  import { FLY_INITIAL_MASS, EGG_LAY_MASS_THRESHOLD } from "./spider-fly.mjs";
39
- import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
41
+ import { believedFactSentence } from "./spider-fly-turn.mjs";
42
+ import { DEFAULT_GAME_CONFIG, massScaleFor } from "../domain/game-config.mjs";
43
+ import { resolveSpriteRequest } from "../domain/sprite-request.mjs";
40
44
 
41
45
  // A larger cell than this page's first cut (44px) — the board sits in its
42
46
  // own full-width block, so a bigger cell keeps it reading as the page's
@@ -62,13 +66,6 @@ function webCellIds() {
62
66
  return out;
63
67
  }
64
68
 
65
- /** An agent id's class for sprite/HUD purposes — "spider-2" -> "spider",
66
- * "fly-10" -> "fly", "egg-1" -> "egg". Self-contained (no outer refs),
67
- * `.toString()`-splice safe. */
68
- export function classOfAgentId(id) {
69
- return String(id).replace(/-\d+$/, "");
70
- }
71
-
72
69
  /**
73
70
  * Reconstruct the spider's remaining silk-thread path — the sequence of
74
71
  * cell ids from its CURRENT cell (after this tick's one executed step) to
@@ -128,54 +125,6 @@ export function facingDegreesFor(plan, previousDegrees) {
128
125
  return previousDegrees ?? 0;
129
126
  }
130
127
 
131
- /** The instance-level `mgx:feels` emotion for one spider-fly agent this
132
- * tick — a PURE derivation from state spider-fly.mjs's own `runSpiderFlyTick`
133
- * already returns on the agent (`goal`/`mass`), never a new persisted fact,
134
- * the same derive-don't-duplicate posture `hasActiveWebAt` already holds for
135
- * web state (PLAN_GAMES_UPLIFT_V3.md §B.2.4). Operator-confirmed mapping: a
136
- * spider that just ate is happy; a spider carrying an uncaught fly or
137
- * mid-chase (chasing, or co-located and about to catch) is angry (predatory
138
- * focus); a spider avoiding another spider is scared; a spider holding
139
- * position or building a web is calm. A fly evading a believed-visible
140
- * spider is scared, and so is the sharper form of the same fear — just
141
- * caught, or already being carried; a fly with no threat visible (wandering,
142
- * or trapped with none in sight) is calm.
143
- *
144
- * `agent.goal` is spider-fly.mjs's own fixed-template text (`goalLineFor`'s
145
- * literal phrasing) — the only reliable per-tick discriminator this
146
- * function's caller has, and every real goal string that engine can ever
147
- * produce is matched below by its own distinguishing prefix. A goal this
148
- * function doesn't recognize (a freshly hatched/spawned agent's "no goal
149
- * yet", or the transient "X is gone — re-evaluating" scrub once a named
150
- * agent dies) falls through to "calm" — the 6-word vocabulary's own
151
- * deliberate no-strong-emotion baseline (sprite-expressions.mjs's B.2.1
152
- * design), not a guess.
153
- *
154
- * `maxMass` is accepted for parity with this page's own per-class mass-bar
155
- * denominators (`SPIDERFLY.maxSpiderMass`/`maxFlyMass`) but unused today —
156
- * no state in the operator's own confirmed table keys off a mass ratio,
157
- * only off the goal text above. Self-contained (no outer refs),
158
- * `.toString()`-splice safe. */
159
- export function emotionFor(agent, kind, maxMass) {
160
- const goal = agent?.goal || "";
161
- if (kind === "spider") {
162
- if (goal.startsWith("just ate")) return "happy";
163
- if (goal.startsWith("carrying") || goal.startsWith("chasing") || goal.startsWith("co-located with")) return "angry";
164
- if (goal.startsWith("avoiding")) return "scared";
165
- return "calm";
166
- }
167
- if (kind === "fly") {
168
- if (
169
- goal.startsWith("evading")
170
- || goal.startsWith("being carried by")
171
- || goal.startsWith("just caught by")
172
- || goal.startsWith("trapped in an active web")
173
- ) return "scared";
174
- return "calm";
175
- }
176
- return "calm";
177
- }
178
-
179
128
  /**
180
129
  * The updated corpse set for one redraw (§A.2.5 — visual-only, entirely
181
130
  * client-side; the actual starve/eat removal already happened in the
@@ -200,7 +149,7 @@ export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns =
200
149
  if (agents[id] || out[id]) continue;
201
150
  const cell = prevAgents[id]?.cell;
202
151
  if (!cell) continue;
203
- out[id] = { cls: classOfAgentId(id), cell, diedAtTurn: turn };
152
+ out[id] = { cls: agentKindOf(id), cell, diedAtTurn: turn };
204
153
  }
205
154
  return out;
206
155
  }
@@ -265,42 +214,48 @@ ${THEME_TOKENS_CSS}
265
214
  @media (prefers-color-scheme: dark) { :root { --fly: #D9A94B; } }
266
215
  :root[data-theme="dark"] { --fly: #D9A94B; }
267
216
  :root[data-theme="light"] { --fly: #A6791F; }
268
- /* The dashboard chrome (Part C.4.2): a beveled control-panel palette
269
- WRAPPING the dusty-window board scene above, never touching it — every
270
- rule below this block styles .side/.controls-row/.tuning/.status only,
271
- all of which are hidden in ?preview=1 mode alongside .side itself, so
272
- none of this bleeds into the embedded home-page hero. --chrome-face is
273
- the panel's own warm plastic face; --chrome-edge-hi/-lo are its raised-
274
- bevel light/shadow edges; --chrome-brass is the console's metal accent
275
- (borders, active/hover states); --chrome-well/-well-ink are the dark
276
- "LCD" readout a real panel's numeric window would use, deliberately
277
- near-black in both themes (a lit readout reads dark-with-a-glow whether
278
- the room around it is light or dark). --chrome-shadow-raised/-inset
279
- compose the two bevel directions once so every panel/button/well below
280
- just references one of them, never re-deriving the edge colors. */
217
+ /* The console chrome, in this page's own gothic-whimsical register (a
218
+ Charles Addams cartoon: india-ink line work over aged paper, spooky-cute
219
+ rather than scary) WRAPPING the window-corner board scene above, never
220
+ touching it every rule below this block styles
221
+ .side/.controls-row/.tuning/.status only, all of which are hidden in
222
+ ?preview=1 mode alongside .side itself, so none of this bleeds into the
223
+ embedded home-page hero. --chrome-face is the panel's aged-ivory (night
224
+ slate in dark) face, textured by --chrome-hatch, a two-direction ink
225
+ cross-hatch; --chrome-edge-hi/-lo are its raised-bevel light/shadow
226
+ edges; --chrome-accent is the ink-plum accent (borders, active/hover
227
+ states); --chrome-well/-well-ink are the near-black readout well with
228
+ bone-pale numerals, deliberately dark in both themes (a lit readout
229
+ reads dark-with-a-glow whether the room around it is light or dark).
230
+ --chrome-shadow-raised/-inset compose the two bevel directions once so
231
+ every panel/button/well below just references one of them. */
281
232
  :root {
282
- --chrome-face: #DCD3B8; --chrome-face-hi: #EFE8CC; --chrome-edge-hi: #FBF7E6; --chrome-edge-lo: #8B7F5C;
283
- --chrome-brass: #8A6A26; --chrome-brass-soft: rgba(138, 106, 38, .16);
284
- --chrome-well: #201A10; --chrome-well-ink: #E8C876;
233
+ --chrome-face: #E7E1D3; --chrome-face-hi: #F2EDE0; --chrome-edge-hi: #FAF6EA; --chrome-edge-lo: #6E6A5E;
234
+ --chrome-accent: #5C4B66; --chrome-accent-soft: rgba(92, 75, 102, .14);
235
+ --chrome-well: #1A171C; --chrome-well-ink: #E4DDCB;
236
+ --chrome-hatch: repeating-linear-gradient(55deg, rgba(28, 23, 32, .05) 0 1px, transparent 1px 6px), repeating-linear-gradient(-35deg, rgba(28, 23, 32, .04) 0 1px, transparent 1px 9px);
285
237
  --chrome-shadow-raised: inset 0 1px 0 var(--chrome-edge-hi), inset 0 -1px 0 var(--chrome-edge-lo), 0 1px 2px rgba(0, 0, 0, .18);
286
238
  --chrome-shadow-inset: inset 0 1px 3px var(--chrome-edge-lo), inset 0 -1px 0 var(--chrome-edge-hi);
287
239
  }
288
240
  @media (prefers-color-scheme: dark) {
289
- :root { --chrome-face: #2B2E23; --chrome-face-hi: #363A29; --chrome-edge-hi: #4C5138; --chrome-edge-lo: #14150D;
290
- --chrome-brass: #D3AE5C; --chrome-brass-soft: rgba(211, 174, 92, .16);
291
- --chrome-well: #0B0904; --chrome-well-ink: #F0D392; }
241
+ :root { --chrome-face: #262230; --chrome-face-hi: #302B3D; --chrome-edge-hi: #453E57; --chrome-edge-lo: #121016;
242
+ --chrome-accent: #A995BD; --chrome-accent-soft: rgba(169, 149, 189, .16);
243
+ --chrome-well: #0C0A10; --chrome-well-ink: #E0D8C6;
244
+ --chrome-hatch: repeating-linear-gradient(55deg, rgba(240, 235, 220, .04) 0 1px, transparent 1px 6px), repeating-linear-gradient(-35deg, rgba(240, 235, 220, .03) 0 1px, transparent 1px 9px); }
292
245
  }
293
- :root[data-theme="dark"] { --chrome-face: #2B2E23; --chrome-face-hi: #363A29; --chrome-edge-hi: #4C5138; --chrome-edge-lo: #14150D;
294
- --chrome-brass: #D3AE5C; --chrome-brass-soft: rgba(211, 174, 92, .16);
295
- --chrome-well: #0B0904; --chrome-well-ink: #F0D392; }
296
- :root[data-theme="light"] { --chrome-face: #DCD3B8; --chrome-face-hi: #EFE8CC; --chrome-edge-hi: #FBF7E6; --chrome-edge-lo: #8B7F5C;
297
- --chrome-brass: #8A6A26; --chrome-brass-soft: rgba(138, 106, 38, .16);
298
- --chrome-well: #201A10; --chrome-well-ink: #E8C876; }
246
+ :root[data-theme="dark"] { --chrome-face: #262230; --chrome-face-hi: #302B3D; --chrome-edge-hi: #453E57; --chrome-edge-lo: #121016;
247
+ --chrome-accent: #A995BD; --chrome-accent-soft: rgba(169, 149, 189, .16);
248
+ --chrome-well: #0C0A10; --chrome-well-ink: #E0D8C6;
249
+ --chrome-hatch: repeating-linear-gradient(55deg, rgba(240, 235, 220, .04) 0 1px, transparent 1px 6px), repeating-linear-gradient(-35deg, rgba(240, 235, 220, .03) 0 1px, transparent 1px 9px); }
250
+ :root[data-theme="light"] { --chrome-face: #E7E1D3; --chrome-face-hi: #F2EDE0; --chrome-edge-hi: #FAF6EA; --chrome-edge-lo: #6E6A5E;
251
+ --chrome-accent: #5C4B66; --chrome-accent-soft: rgba(92, 75, 102, .14);
252
+ --chrome-well: #1A171C; --chrome-well-ink: #E4DDCB;
253
+ --chrome-hatch: repeating-linear-gradient(55deg, rgba(28, 23, 32, .05) 0 1px, transparent 1px 6px), repeating-linear-gradient(-35deg, rgba(28, 23, 32, .04) 0 1px, transparent 1px 9px); }
299
254
  html { background: var(--bg); }
300
255
  body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
301
256
  .mono { font-family: ${MONO_STACK}; }
302
257
  main { max-width: 1120px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
303
- .eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--chrome-brass); }
258
+ .eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--chrome-accent); }
304
259
  h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
305
260
  button { font: inherit; color: inherit; background: none; cursor: pointer; }
306
261
  button:focus-visible, input:focus-visible, .sprite:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@@ -310,7 +265,7 @@ ${THEME_TOKENS_CSS}
310
265
  needing to sum to a full width. */
311
266
  .stage { display: grid; grid-template-columns: minmax(0, 8fr) minmax(280px, 3fr); gap: 1.2rem; align-items: start; }
312
267
  @media (max-width: 760px) { .stage { grid-template-columns: minmax(0, 1fr); } }
313
- /* A dusty window corner: a soft light glow near the top-left (WEB_HOME
268
+ /* A moonlit window corner: a cool pale glow near the top-left (WEB_HOME
314
269
  already sits near that corner — spider-fly-world.mjs's own header
315
270
  comment), and a faint diagonal weave standing in for dust/silk caught
316
271
  in the light. Decoration only — the 10x10 game grid itself is drawn by
@@ -319,20 +274,26 @@ ${THEME_TOKENS_CSS}
319
274
  margin: 0 auto;
320
275
  position: relative; width: ${BOARD_PX}px; max-width: 100%; aspect-ratio: 1 / 1;
321
276
  background:
322
- radial-gradient(140% 140% at 6% 6%, rgba(255, 241, 199, .55), transparent 52%),
323
- repeating-linear-gradient(115deg, rgba(120, 110, 90, .06) 0 1px, transparent 1px 30px),
277
+ radial-gradient(140% 140% at 6% 6%, rgba(224, 224, 240, .55), transparent 52%),
278
+ repeating-linear-gradient(115deg, rgba(100, 95, 110, .06) 0 1px, transparent 1px 30px),
324
279
  var(--card);
325
280
  border: 1px solid var(--line);
326
281
  box-shadow: inset 0 0 0 6px var(--bg), inset 0 0 0 7px var(--line);
327
282
  }
328
- @media (prefers-color-scheme: dark) { .board-frame { background: radial-gradient(140% 140% at 6% 6%, rgba(130, 112, 60, .32), transparent 52%), repeating-linear-gradient(115deg, rgba(255, 255, 255, .045) 0 1px, transparent 1px 30px), var(--card); } }
329
- :root[data-theme="dark"] .board-frame { background: radial-gradient(140% 140% at 6% 6%, rgba(130, 112, 60, .32), transparent 52%), repeating-linear-gradient(115deg, rgba(255, 255, 255, .045) 0 1px, transparent 1px 30px), var(--card); }
283
+ @media (prefers-color-scheme: dark) { .board-frame { background: radial-gradient(140% 140% at 6% 6%, rgba(126, 120, 158, .30), transparent 52%), repeating-linear-gradient(115deg, rgba(255, 255, 255, .045) 0 1px, transparent 1px 30px), var(--card); } }
284
+ :root[data-theme="dark"] .board-frame { background: radial-gradient(140% 140% at 6% 6%, rgba(126, 120, 158, .30), transparent 52%), repeating-linear-gradient(115deg, rgba(255, 255, 255, .045) 0 1px, transparent 1px 30px), var(--card); }
330
285
  .board-frame canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
286
+ /* The cobweb in the window corner — pure decoration over the moonlit
287
+ glow (never a game element; the ACTIVE web block is still drawn by
288
+ drawBoard() on the canvas). It sits under the sprite layer so agents
289
+ always pass in front of it. */
290
+ .cobweb { position: absolute; top: 7px; left: 7px; width: 36%; height: 36%; color: var(--ink); opacity: .26; pointer-events: none; }
291
+ .cobweb svg { width: 100%; height: 100%; display: block; }
331
292
  .sprite-layer { position: absolute; inset: 0; }
332
293
  .sprite { position: absolute; width: 7.6%; height: 7.6%; transform: translate(-50%, -50%); transition: left .25s ease, top .25s ease; }
333
294
  @media (prefers-reduced-motion: reduce) { .sprite, .sprite-face { transition: none !important; } }
334
295
  .sprite-face { width: 100%; height: 100%; transition: transform .25s ease; }
335
- .sprite-face svg { width: 100%; height: 100%; display: block; }
296
+ .sprite-face svg { width: 100%; height: 100%; display: block; filter: var(--sprite-pop); }
336
297
  .sprite[data-cls="spider"] { color: var(--taught); }
337
298
  .sprite[data-cls="fly"] { color: var(--fly); }
338
299
  .sprite[data-cls="egg"] { color: var(--muted); }
@@ -350,7 +311,7 @@ ${THEME_TOKENS_CSS}
350
311
  nameplate header below bleeds to this same panel's own edges via a
351
312
  negative margin equal to this padding, so it reads as one welded unit,
352
313
  not a label floating inside a box. */
353
- .hud, .chat, .tuning { background: var(--chrome-face); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-raised); border-radius: 2px; padding: .6rem .75rem; }
314
+ .hud, .chat, .tuning { background: var(--chrome-hatch), var(--chrome-face); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-raised); border-radius: 2px; padding: .6rem .75rem; }
354
315
  /* Both the controls strip above the board and the tuning strip below it
355
316
  match the board's own width (not the wider stage-left column they sit
356
317
  in), so all three line up as one visual stack. */
@@ -380,12 +341,12 @@ ${THEME_TOKENS_CSS}
380
341
  .hud-row { display: flex; align-items: flex-start; gap: .6rem; padding: .4rem 0; border-top: 1px solid var(--chrome-edge-lo); box-shadow: inset 0 1px 0 var(--chrome-edge-hi); }
381
342
  .hud-row:first-of-type { border-top: none; box-shadow: none; }
382
343
  .hud-row.clickable { cursor: pointer; }
383
- .hud-row.clickable:hover, .hud-row.clickable:focus-visible { background: var(--chrome-brass-soft); }
344
+ .hud-row.clickable:hover, .hud-row.clickable:focus-visible { background: var(--chrome-accent-soft); }
384
345
  .hud-main { display: flex; flex-direction: column; gap: .1rem; flex: 1 1 auto; min-width: 0; }
385
346
  .hud-id { font-family: ${MONO_STACK}; font-size: .74rem; font-weight: 600; }
386
347
  .hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
387
348
  .hud-goal { font-size: .85rem; }
388
- .hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .25rem; line-height: 1.4; padding-left: .5rem; border-left: 2px solid var(--chrome-brass); }
349
+ .hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .25rem; line-height: 1.4; padding-left: .5rem; border-left: 2px solid var(--chrome-accent); }
389
350
  /* the click-expand facts panel (§28): beside the clicked spider/fly's own
390
351
  row, never a separate popover or a second panel elsewhere on the page —
391
352
  the same believedCellOf/beliefSnapshotFor read path spider-fly.mjs
@@ -397,9 +358,9 @@ ${THEME_TOKENS_CSS}
397
358
  texture (repeating-linear-gradient) instead of a smooth gradient bar —
398
359
  discrete resource units, the same reading-at-a-glance language a 90s
399
360
  strategy game's own gold/mana meters use. */
400
- .mass-track { height: 7px; margin-top: .35rem; background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 1px; overflow: hidden; }
401
- .mass-fill { height: 100%; background: repeating-linear-gradient(90deg, var(--taught) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
402
- .mass-fill.fly { background: repeating-linear-gradient(90deg, var(--fly) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
361
+ .meter-track { height: 7px; margin-top: .35rem; background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 1px; overflow: hidden; }
362
+ .meter-fill { height: 100%; background: repeating-linear-gradient(90deg, var(--taught) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
363
+ .meter-fill.fly { background: repeating-linear-gradient(90deg, var(--fly) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
403
364
  .hud-empty { color: var(--muted); font-size: .85rem; }
404
365
  .chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 220px; overflow-y: auto; margin-bottom: .5rem; }
405
366
  .chatlog:empty { display: none; margin-bottom: 0; }
@@ -417,7 +378,7 @@ ${THEME_TOKENS_CSS}
417
378
  .chatask input:disabled { opacity: .5; }
418
379
  .chatpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; }
419
380
  .pill { font-family: ${MONO_STACK}; font-size: .68rem; padding: .2rem .6rem; border: 1px solid var(--chrome-edge-lo); border-radius: 99px; background: var(--chrome-face); color: var(--ink); box-shadow: inset 0 1px 0 var(--chrome-edge-hi); white-space: nowrap; }
420
- .pill:hover:not(:disabled) { border-color: var(--chrome-brass); }
381
+ .pill:hover:not(:disabled) { border-color: var(--chrome-accent); }
421
382
  .pill:disabled { opacity: .45; cursor: default; }
422
383
  .pill[data-role="addr"].active { border-color: var(--taught); color: var(--taught); }
423
384
  /* The dynamic deception-pill rail (§A.2.4): a true/false tag shown ONLY
@@ -437,7 +398,7 @@ ${THEME_TOKENS_CSS}
437
398
  .tuning-col.spider h3 { color: var(--taught); } .tuning-col.fly h3 { color: var(--fly); }
438
399
  .tuning-col label { display: block; font-size: .68rem; color: var(--muted); margin-bottom: .6rem; }
439
400
  /* Every live number on this page (this readout, the turn plaque below)
440
- shares the same brass-on-well "LED" treatment — one readout language,
401
+ shares the same bone-on-ink readout treatment — one readout language,
441
402
  not a one-off flourish. */
442
403
  .tuning-col .tuning-val { font-family: ${MONO_STACK}; font-variant-numeric: tabular-nums; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: 0 .35rem; float: right; }
443
404
  .tuning-col input[type="range"] {
@@ -446,7 +407,7 @@ ${THEME_TOKENS_CSS}
446
407
  -webkit-appearance: none; appearance: none;
447
408
  }
448
409
  .tuning-col.fly input[type="range"] { accent-color: var(--fly); }
449
- /* A lever-look thumb (a molded brass-edged cap) — appearance: none above
410
+ /* A lever-look thumb (a molded, ink-edged cap) — appearance: none above
450
411
  drops the OS's own native slider look in browsers that honor it;
451
412
  accent-color just above is the fallback for any that don't. */
452
413
  .tuning-col input[type="range"]::-webkit-slider-thumb {
@@ -469,14 +430,14 @@ ${THEME_TOKENS_CSS}
469
430
  control panel", and it costs nothing under reduced-motion since neither
470
431
  state involves a transition, just an instant bevel swap. */
471
432
  .controls-row button { font-family: ${MONO_STACK}; font-size: .78rem; letter-spacing: .03em; text-transform: uppercase; padding: .4rem .85rem; border-radius: 2px; border: 1px solid var(--chrome-edge-lo); background: var(--chrome-face); box-shadow: var(--chrome-shadow-raised); color: var(--ink); }
472
- .controls-row button:hover:not(:disabled) { border-color: var(--chrome-brass); }
433
+ .controls-row button:hover:not(:disabled) { border-color: var(--chrome-accent); }
473
434
  .controls-row button:active:not(:disabled) { box-shadow: var(--chrome-shadow-inset); }
474
435
  .controls-row button:disabled { opacity: .4; cursor: default; box-shadow: none; }
475
- /* The signature element: a brass-framed digital turn counter, the same
436
+ /* The turn counter plaque: an accent-framed readout, the same
476
437
  inset-well readout language as every stat above it — the one thing this
477
438
  page is remembered by. */
478
- .controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .82rem; letter-spacing: .05em; text-transform: uppercase; font-variant-numeric: tabular-nums; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-brass); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .3rem .7rem; }
479
- .status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .5rem; padding-left: .5rem; border-left: 2px solid var(--chrome-brass); }
439
+ .controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .82rem; letter-spacing: .05em; text-transform: uppercase; font-variant-numeric: tabular-nums; color: var(--chrome-well-ink); background: var(--chrome-well); border: 1px solid var(--chrome-accent); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .3rem .7rem; }
440
+ .status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .5rem; padding-left: .5rem; border-left: 2px solid var(--chrome-accent); }
480
441
  body.preview .side, body.preview .controls-panel, body.preview .tuning { display: none; }
481
442
  body.preview main { padding: 0; max-width: none; }
482
443
  body.preview .stage, body.preview .stage-left { display: block; }
@@ -507,11 +468,23 @@ ${THEME_TOKENS_CSS}
507
468
  <div class="board-frame" id="boardFrame">
508
469
  <canvas id="board" width="${BOARD_PX}" height="${BOARD_PX}" aria-label="the 10x10 board"></canvas>
509
470
  <canvas id="pov" width="${BOARD_PX}" height="${BOARD_PX}" aria-hidden="true"></canvas>
471
+ <div class="cobweb" aria-hidden="true"><svg viewBox="0 0 100 100" fill="none" stroke="currentColor" stroke-linecap="round">
472
+ <g stroke-width="0.7">
473
+ <path d="M0 0 L73.8 5.2"/><path d="M0 0 L68.6 27.7"/><path d="M0 0 L52.3 52.3"/><path d="M0 0 L27.7 68.6"/><path d="M0 0 L5.2 73.8"/>
474
+ </g>
475
+ <g stroke-width="0.55">
476
+ <path d="M20 1.4 Q16.8 3.9 18.5 7.5 Q14.3 9.5 14.1 14.1 Q9.5 14.3 7.5 18.5 Q3.9 16.8 1.4 20"/>
477
+ <path d="M33.9 2.4 Q28.4 6.6 31.5 12.7 Q24.4 16.1 24 24 Q16.1 24.4 12.7 31.5 Q6.6 28.4 2.4 33.9"/>
478
+ <path d="M49.9 3.5 Q41.9 9.7 46.4 18.7 Q35.9 23.7 35.4 35.4 Q23.7 35.9 18.7 46.4 Q9.7 41.9 3.5 49.9"/>
479
+ <path d="M67.8 4.8 Q57 13.2 63 25.5 Q48.8 32.3 48.1 48.1 Q32.3 48.8 25.5 63 Q13.2 57 4.8 67.8"/>
480
+ </g>
481
+ <path d="M48.1 48.1 Q49.5 54 48.6 59.5" stroke-width="0.5"/>
482
+ </svg></div>
510
483
  <div class="sprite-layer" id="spriteLayer"></div>
511
484
  <div class="thread-tip" id="threadTip"></div>
512
485
  </div>
513
486
  <div class="tuning" id="tuning">
514
- <h2>live tuning &mdash; mass loss, spawn rate, vision, per class</h2>
487
+ <h2>live tuning &middot; mass loss, spawn rate, vision, per class</h2>
515
488
  <div class="tuning-grid">
516
489
  <div class="tuning-col spider">
517
490
  <h3>spider</h3>
@@ -567,12 +540,25 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
567
540
  (function () {
568
541
  "use strict";
569
542
  const createTicker = ${createTicker.toString()};
570
- const classOfAgentId = ${classOfAgentId.toString()};
543
+ const loadWinkVendor = ${loadWinkVendor.toString()};
544
+ const agentKindOf = ${agentKindOf.toString()};
571
545
  const threadCellsForSpiderPlan = ${threadCellsForSpiderPlan.toString()};
572
546
  const facingDegreesFor = ${facingDegreesFor.toString()};
573
- const emotionFor = ${emotionFor.toString()};
547
+ const resolveSpriteRequest = ${resolveSpriteRequest.toString()};
574
548
  const nextCorpses = ${nextCorpses.toString()};
575
- const esc = ${escapeHtml.toString()};
549
+ const massScaleFor = ${massScaleFor.toString()};
550
+ const believedFactSentence = ${believedFactSentence.toString()};
551
+ const escapeHtml = ${escapeHtml.toString()};
552
+ const meterBarHtml = ${meterBarHtml.toString()};
553
+ const esc = escapeHtml;
554
+ // The lemma/POS tier every sibling dock loads: this game's OWN commands
555
+ // (tick, address, stop, see) are closed regexes that never need it, but a
556
+ // line that doesn't match one of those still falls through to the
557
+ // ordinary chat/ask/teach lanes (spiderFlyTurn returns null), and those
558
+ // lanes read exactly as much better with wink loaded as they do on every
559
+ // other page. Fired eagerly here so it is likely settled well before a
560
+ // visitor's first ordinary (non-game) question.
561
+ loadWinkVendor({ register: (factory) => tmctSpiderFly.registerWinkModel(factory) })();
576
562
  const el = (id) => document.getElementById(id);
577
563
  const boardFrame = el("boardFrame");
578
564
  const boardCanvas = el("board");
@@ -636,7 +622,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
636
622
  const goalById = {};
637
623
  const spriteEls = {};
638
624
  const facingByAgent = {};
639
- const emotionByAgent = {};
625
+ const moodByAgent = {};
640
626
  let corpses = {};
641
627
  const corpseEls = {};
642
628
  let selectedAddresseeId = null;
@@ -651,30 +637,37 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
651
637
  function removeStaleSprites(agents) {
652
638
  for (const id of Object.keys(spriteEls)) {
653
639
  if (!agents[id]) {
654
- spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; delete facingByAgent[id]; delete emotionByAgent[id];
640
+ spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; delete facingByAgent[id]; delete moodByAgent[id];
655
641
  }
656
642
  }
657
643
  }
658
644
 
659
- // maxMassFor/propertyFactsForAgent (emotionFor's own caller-side glue): only
660
- // spider/fly carry the mgx:feels parameter (data/sprites-large/*-with-
661
- // emotion.toml exists for those two classes only) every other class (egg,
662
- // and any class this page has no template for at all) keeps propertyFacts
663
- // empty, resolving through its plain template exactly as before this page
664
- // wired emotion through. maxMassFor mirrors massBarHtml's own per-class
665
- // denominator below so the HUD's mass bar and the sprite's own expression
666
- // read the same "how full" scale.
667
- function maxMassFor(cls) {
668
- return cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
669
- }
670
- function propertyFactsForAgent(agent, cls) {
671
- if (cls !== "spider" && cls !== "fly") return [];
672
- return [{ predicate: "mgx:feels", object: emotionFor(agent, cls, maxMassFor(cls)) }];
645
+ // The engine writes each agent's mood as a real mgx:feels fact every turn
646
+ // and hands the same word back on the agent, so this page reads it rather
647
+ // than deriving a mood of its own. Only spider/fly carry the mgx:feels
648
+ // parameter (data/sprites-large/*-with-emotion.toml exists for those two
649
+ // classes only) every other class (egg, and any class this page has no
650
+ // template for at all) asks for no expression at all, resolving through its
651
+ // plain template. A snapshot-sourced agent carries no mood (a chat-driven
652
+ // tick returns text, not the structured tick shape), so the last mood the
653
+ // engine reported stands, exactly as goalById already holds the last goal.
654
+ // The starting board has no reported mood at all, and calm is the mood the
655
+ // engine itself writes for a just-minted agent, so that is the baseline.
656
+ function expressionForAgent(id, agent, cls) {
657
+ if (cls !== "spider" && cls !== "fly") return null;
658
+ return (agent && agent.mood) || moodByAgent[id] || "calm";
673
659
  }
674
- function resolveSpriteFace(cls, propertyFacts) {
675
- return window.tmctSpiderFly
676
- ? tmctSpiderFly.resolveSpriteAsset(cls, (session && session.taxonomyRows) || [], propertyFacts, SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
677
- : "";
660
+ // One sprite request, in the same { class, expression } shape tmct_sprite
661
+ // takes, answered by the same pure resolver the tool wraps (sprite-request.mjs,
662
+ // spliced in above) the page holds the state and nothing else.
663
+ function resolveSpriteFace(spriteRequest) {
664
+ if (!window.tmctSpiderFly) return "";
665
+ return resolveSpriteRequest(spriteRequest, {
666
+ factRows: (session && session.taxonomyRows) || [],
667
+ templates: SPIDERFLY.spriteTemplates,
668
+ spriteRegistry: tmctSpiderFly.SPRITE_REGISTRY,
669
+ resolveSpriteAsset: tmctSpiderFly.resolveSpriteAsset,
670
+ }).svg || "";
678
671
  }
679
672
 
680
673
  function togglePov(id) {
@@ -693,18 +686,18 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
693
686
  node.className = "sprite";
694
687
  node.dataset.cls = cls;
695
688
  // Property-aware resolution (sprite-templates.mjs's resolveSpriteAsset):
696
- // a spider/fly's live mgx:feels emotion (emotionFor, above) resolves it
697
- // through the matching data/sprites-large/*-with-emotion.toml template;
698
- // every other class (egg) keeps propertyFacts empty and resolves through
699
- // its plain class template (or the flat SPRITE_REGISTRY), unchanged.
700
- const propertyFacts = propertyFactsForAgent(agent, cls);
701
- emotionByAgent[id] = propertyFacts[0] ? propertyFacts[0].object : null;
689
+ // a spider/fly's live mgx:feels mood resolves it through the matching
690
+ // data/sprites-large/*-with-emotion.toml template; every other class
691
+ // (egg) asks for no expression and resolves through its plain class
692
+ // template (or the flat SPRITE_REGISTRY), unchanged.
693
+ const mood = expressionForAgent(id, agent, cls);
694
+ moodByAgent[id] = mood;
702
695
  // The sprite SVG lives in its own inner wrapper so plan-driven facing
703
696
  // (a CSS rotate on THIS wrapper) never fights the outer .sprite node's
704
697
  // own translate(-50%,-50%) positioning transform.
705
698
  const face = document.createElement("div");
706
699
  face.className = "sprite-face";
707
- face.innerHTML = resolveSpriteFace(cls, propertyFacts);
700
+ face.innerHTML = resolveSpriteFace({ class: cls, expression: mood });
708
701
  node.appendChild(face);
709
702
  if (!preview) {
710
703
  node.tabIndex = 0;
@@ -723,7 +716,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
723
716
  function applyAgents(agents) {
724
717
  removeStaleSprites(agents);
725
718
  for (const [id, a] of Object.entries(agents)) {
726
- const cls = classOfAgentId(id);
719
+ const cls = agentKindOf(id);
727
720
  const node = ensureSpriteEl(id, cls, a);
728
721
  const parsed = tmctSpiderFly.parseCellId(a.cell);
729
722
  if (!parsed) continue;
@@ -739,16 +732,15 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
739
732
  const face = node.querySelector(".sprite-face");
740
733
  if (face) {
741
734
  face.style.transform = "rotate(" + facingByAgent[id] + "deg)";
742
- // Emotion changes tick-to-tick (a fly's fear spikes the instant a
735
+ // Mood changes tick-to-tick (a fly's fear spikes the instant a
743
736
  // spider comes into view, a spider's joy fires the instant it eats),
744
737
  // so this must re-resolve every redraw, not just at sprite creation —
745
- // but only actually replace the DOM when the emotion word itself
738
+ // but only actually replace the DOM when the mood word itself
746
739
  // changed since last render, never on every tick regardless.
747
- const propertyFacts = propertyFactsForAgent(a, cls);
748
- const emotion = propertyFacts[0] ? propertyFacts[0].object : null;
749
- if (emotion !== emotionByAgent[id]) {
750
- emotionByAgent[id] = emotion;
751
- face.innerHTML = resolveSpriteFace(cls, propertyFacts);
740
+ const mood = expressionForAgent(id, a, cls);
741
+ if (mood !== moodByAgent[id]) {
742
+ moodByAgent[id] = mood;
743
+ face.innerHTML = resolveSpriteFace({ class: cls, expression: mood });
752
744
  }
753
745
  }
754
746
  if (a.goal) goalById[id] = a.goal;
@@ -781,7 +773,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
781
773
  // never emotion-parameterized at all (a rotting carcass has none),
782
774
  // just its class's own plain template under the grayscale/fade the
783
775
  // .sprite.corpse CSS rule already applies.
784
- face.innerHTML = resolveSpriteFace(corpse.cls, []);
776
+ face.innerHTML = resolveSpriteFace({ class: corpse.cls });
785
777
  node.appendChild(face);
786
778
  spriteLayer.appendChild(node);
787
779
  corpseEls[id] = node;
@@ -794,13 +786,6 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
794
786
  }
795
787
  }
796
788
 
797
- function massBarHtml(cls, mass) {
798
- const maxMass = cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
799
- if (typeof mass !== "number" || !maxMass) return "";
800
- const pct = Math.max(0, Math.min(100, (mass / maxMass) * 100));
801
- return '<div class="mass-track"><div class="mass-fill ' + esc(cls) + '" style="width:' + pct + '%"></div></div>';
802
- }
803
-
804
789
  // The last-created plan as a plain arrow chain ("west → west"), or
805
790
  // "holding." when the agent's plan this tick is empty — mirrors exactly
806
791
  // what drove this tick's sprite facing (facingDegreesFor reads the same
@@ -826,15 +811,15 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
826
811
 
827
812
  // The full text rendering behind the click-expand panel: one sentence per
828
813
  // other candidate the observer currently has a belief about, or "has not
829
- // been observed" for a candidate it doesn't — the same belief map
830
- // beliefLineHtml already compresses into a single line, spelled out in
831
- // full beside the clicked agent instead of abbreviated above it.
814
+ // been observed" for a candidate it doesn't — believedFactSentence is the
815
+ // SAME function spider-fly-turn.mjs's own chat lane renders "X sees: ..."
816
+ // from, so the chat phrasing and this panel can never drift apart.
817
+ // beliefLineHtml already compresses the same belief map into a single line
818
+ // above; this spells it out in full beside the clicked agent.
832
819
  function observedFactsHtml(observerId, belief) {
833
820
  const entries = Object.entries(belief || {});
834
821
  const lines = entries.length
835
- ? entries.map(([id, cell]) => '<div class="hud-detail-line">'
836
- + (cell ? esc(id) + " is at " + esc(cell) + "." : esc(id) + " has not been observed.")
837
- + "</div>").join("")
822
+ ? entries.map(([id, cell]) => '<div class="hud-detail-line">' + esc(believedFactSentence(id, cell)) + "</div>").join("")
838
823
  : '<div class="hud-detail-line">nothing else is on the board yet.</div>';
839
824
  return '<div class="hud-detail"><div class="hud-detail-title">' + esc(observerId) + " observes</div>" + lines + "</div>";
840
825
  }
@@ -843,13 +828,13 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
843
828
  const ids = Object.keys(lastAgents).sort();
844
829
  if (!ids.length) { hudEl.innerHTML = '<div class="hud-empty">no agents on the board.</div>'; return; }
845
830
  hudEl.innerHTML = ids.map((id) => {
846
- const cls = classOfAgentId(id);
831
+ const cls = agentKindOf(id);
847
832
  const a = lastAgents[id];
848
833
  const clickable = cls === "spider" || cls === "fly";
849
834
  const expanded = clickable && expandedAgents.has(id);
850
835
  const main = '<div class="hud-main"><span class="hud-id ' + esc(cls) + '">' + esc(id) + '</span>'
851
836
  + '<span class="hud-goal">' + esc(goalById[id] || "watching\\u2026") + "</span>"
852
- + massBarHtml(cls, a.mass)
837
+ + meterBarHtml(cls, a.mass, massScaleFor(cls, { maxSpiderMass: SPIDERFLY.maxSpiderMass, maxFlyMass: SPIDERFLY.maxFlyMass }))
853
838
  + planLineHtml(a.plan)
854
839
  + beliefLineHtml(a.belief)
855
840
  + "</div>";
@@ -939,7 +924,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
939
924
  // engine's own shipped default before the session has finished
940
925
  // booting) — a POV toggle always reflects whatever vision range this
941
926
  // class currently actually has, not a fixed constant.
942
- const radius = classOfAgentId(povAgentId) === "spider"
927
+ const radius = agentKindOf(povAgentId) === "spider"
943
928
  ? (liveConfig.spiderVisionRadius ?? tmctSpiderFly.DEFAULT_VISION_RADIUS)
944
929
  : (liveConfig.flyVisionRadius ?? tmctSpiderFly.DEFAULT_VISION_RADIUS);
945
930
  const visible = new Set(tmctSpiderFly.visibleCells(p.x, p.y, radius));