@polycode-projects/the-mechanical-code-talker 2.8.5 → 2.8.7
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.
- package/package.json +1 -1
- package/src/services/spider-fly-viz.mjs +213 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
|
|
@@ -128,6 +128,54 @@ export function facingDegreesFor(plan, previousDegrees) {
|
|
|
128
128
|
return previousDegrees ?? 0;
|
|
129
129
|
}
|
|
130
130
|
|
|
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
|
+
|
|
131
179
|
/**
|
|
132
180
|
* The updated corpse set for one redraw (§A.2.5 — visual-only, entirely
|
|
133
181
|
* client-side; the actual starve/eat removal already happened in the
|
|
@@ -161,10 +209,14 @@ export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns =
|
|
|
161
209
|
* same `title`/`spriteTemplates` every time; every other piece of state
|
|
162
210
|
* this page shows is computed live in the browser once the sibling bundle
|
|
163
211
|
* loads. `spriteTemplates` is the build step's own read of
|
|
164
|
-
* data/sprites/*.toml (sprite-template-files.mjs)
|
|
165
|
-
* the
|
|
166
|
-
*
|
|
167
|
-
*
|
|
212
|
+
* data/sprites-large/*.toml (sprite-large-template-files.mjs) — the large
|
|
213
|
+
* tier, not the small icon tier every other embedder reads, because it's
|
|
214
|
+
* the only one `spider-with-emotion.toml`/`fly-with-emotion.toml` (the live
|
|
215
|
+
* `mgx:feels` wiring below resolves against) actually exist in — embedded
|
|
216
|
+
* as page data the same reason adventure-viz.mjs's own worldPayload is —
|
|
217
|
+
* the browser bundle stays fs-free. Defaults to `[]` (every agent falls
|
|
218
|
+
* back to the flat SPRITE_REGISTRY, unchanged from before this module
|
|
219
|
+
* existed).
|
|
168
220
|
* `?preview=1` on the page's own URL switches it into the small, auto-
|
|
169
221
|
* playing, non-interactive mode the home page's hero iframe embeds (§11) —
|
|
170
222
|
* one file serves both the hero and the "open full-screen" link, matching
|
|
@@ -203,11 +255,42 @@ ${THEME_TOKENS_CSS}
|
|
|
203
255
|
@media (prefers-color-scheme: dark) { :root { --fly: #D9A94B; } }
|
|
204
256
|
:root[data-theme="dark"] { --fly: #D9A94B; }
|
|
205
257
|
:root[data-theme="light"] { --fly: #A6791F; }
|
|
258
|
+
/* The dashboard chrome (Part C.4.2): a beveled control-panel palette
|
|
259
|
+
WRAPPING the dusty-window board scene above, never touching it — every
|
|
260
|
+
rule below this block styles .side/.controls-row/.tuning/.status only,
|
|
261
|
+
all of which are hidden in ?preview=1 mode alongside .side itself, so
|
|
262
|
+
none of this bleeds into the embedded home-page hero. --chrome-face is
|
|
263
|
+
the panel's own warm plastic face; --chrome-edge-hi/-lo are its raised-
|
|
264
|
+
bevel light/shadow edges; --chrome-brass is the console's metal accent
|
|
265
|
+
(borders, active/hover states); --chrome-well/-well-ink are the dark
|
|
266
|
+
"LCD" readout a real panel's numeric window would use, deliberately
|
|
267
|
+
near-black in both themes (a lit readout reads dark-with-a-glow whether
|
|
268
|
+
the room around it is light or dark). --chrome-shadow-raised/-inset
|
|
269
|
+
compose the two bevel directions once so every panel/button/well below
|
|
270
|
+
just references one of them, never re-deriving the edge colors. */
|
|
271
|
+
:root {
|
|
272
|
+
--chrome-face: #DCD3B8; --chrome-face-hi: #EFE8CC; --chrome-edge-hi: #FBF7E6; --chrome-edge-lo: #8B7F5C;
|
|
273
|
+
--chrome-brass: #8A6A26; --chrome-brass-soft: rgba(138, 106, 38, .16);
|
|
274
|
+
--chrome-well: #201A10; --chrome-well-ink: #E8C876;
|
|
275
|
+
--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);
|
|
276
|
+
--chrome-shadow-inset: inset 0 1px 3px var(--chrome-edge-lo), inset 0 -1px 0 var(--chrome-edge-hi);
|
|
277
|
+
}
|
|
278
|
+
@media (prefers-color-scheme: dark) {
|
|
279
|
+
:root { --chrome-face: #2B2E23; --chrome-face-hi: #363A29; --chrome-edge-hi: #4C5138; --chrome-edge-lo: #14150D;
|
|
280
|
+
--chrome-brass: #D3AE5C; --chrome-brass-soft: rgba(211, 174, 92, .16);
|
|
281
|
+
--chrome-well: #0B0904; --chrome-well-ink: #F0D392; }
|
|
282
|
+
}
|
|
283
|
+
:root[data-theme="dark"] { --chrome-face: #2B2E23; --chrome-face-hi: #363A29; --chrome-edge-hi: #4C5138; --chrome-edge-lo: #14150D;
|
|
284
|
+
--chrome-brass: #D3AE5C; --chrome-brass-soft: rgba(211, 174, 92, .16);
|
|
285
|
+
--chrome-well: #0B0904; --chrome-well-ink: #F0D392; }
|
|
286
|
+
:root[data-theme="light"] { --chrome-face: #DCD3B8; --chrome-face-hi: #EFE8CC; --chrome-edge-hi: #FBF7E6; --chrome-edge-lo: #8B7F5C;
|
|
287
|
+
--chrome-brass: #8A6A26; --chrome-brass-soft: rgba(138, 106, 38, .16);
|
|
288
|
+
--chrome-well: #201A10; --chrome-well-ink: #E8C876; }
|
|
206
289
|
html { background: var(--bg); }
|
|
207
290
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
208
291
|
.mono { font-family: ${MONO_STACK}; }
|
|
209
292
|
main { max-width: 1120px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
|
|
210
|
-
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--
|
|
293
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--chrome-brass); }
|
|
211
294
|
h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
|
|
212
295
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
213
296
|
button:focus-visible, input:focus-visible, .sprite:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
@@ -246,35 +329,54 @@ ${THEME_TOKENS_CSS}
|
|
|
246
329
|
.sprite.corpse .sprite-face { transform: none !important; }
|
|
247
330
|
.thread-tip { position: absolute; transform: translate(-50%, -130%); font-family: ${MONO_STACK}; font-size: .68rem; background: var(--ink); color: var(--bg); padding: .1rem .4rem; border-radius: 3px; pointer-events: none; white-space: nowrap; display: none; }
|
|
248
331
|
.side { display: flex; flex-direction: column; gap: .8rem; min-width: 0; }
|
|
249
|
-
|
|
250
|
-
|
|
332
|
+
/* The console panel shell: a raised beveled plastic face (chrome-shadow-
|
|
333
|
+
raised), never the flat card/hairline the rest of the site uses — the
|
|
334
|
+
nameplate header below bleeds to this same panel's own edges via a
|
|
335
|
+
negative margin equal to this padding, so it reads as one welded unit,
|
|
336
|
+
not a label floating inside a box. */
|
|
337
|
+
.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; }
|
|
338
|
+
.hud h2, .chat h2, .tuning h2 {
|
|
339
|
+
font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 600;
|
|
340
|
+
margin: -.6rem -.75rem .5rem; padding: .42rem .75rem;
|
|
341
|
+
background: var(--chrome-face-hi); border-bottom: 1px solid var(--chrome-edge-lo);
|
|
342
|
+
box-shadow: inset 0 1px 0 var(--chrome-edge-hi);
|
|
343
|
+
color: var(--ink); text-shadow: 0 1px 0 var(--chrome-edge-hi);
|
|
344
|
+
}
|
|
251
345
|
/* Fixed-height, internally-scrolling — a hatch can mint several spiders
|
|
252
346
|
at once now, so the agent count (and a naive card list's own height)
|
|
253
347
|
can jump sharply; the panel must never grow the page underneath it. */
|
|
254
348
|
.hud-list { max-height: 420px; overflow-y: auto; }
|
|
255
|
-
.hud-row { display: flex; flex-direction: column; gap: .1rem; padding: .4rem 0; border-top: 1px solid var(--
|
|
256
|
-
.hud-row:first-of-type { border-top: none; }
|
|
257
|
-
.hud-id { font-family: ${MONO_STACK}; font-size: .74rem; }
|
|
349
|
+
.hud-row { display: flex; flex-direction: column; gap: .1rem; padding: .4rem 0; border-top: 1px solid var(--chrome-edge-lo); box-shadow: inset 0 1px 0 var(--chrome-edge-hi); }
|
|
350
|
+
.hud-row:first-of-type { border-top: none; box-shadow: none; }
|
|
351
|
+
.hud-id { font-family: ${MONO_STACK}; font-size: .74rem; font-weight: 600; }
|
|
258
352
|
.hud-id.spider { color: var(--taught); } .hud-id.fly { color: var(--fly); } .hud-id.egg { color: var(--muted); }
|
|
259
353
|
.hud-goal { font-size: .85rem; }
|
|
260
|
-
.hud-plan, .hud-belief { font-family: ${MONO_STACK}; font-size: .66rem; color: var(--muted); margin-top: .
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
354
|
+
.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); }
|
|
355
|
+
/* A stat-readout track: an inset "LCD" well, filled with a segmented pip
|
|
356
|
+
texture (repeating-linear-gradient) instead of a smooth gradient bar —
|
|
357
|
+
discrete resource units, the same reading-at-a-glance language a 90s
|
|
358
|
+
strategy game's own gold/mana meters use. */
|
|
359
|
+
.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; }
|
|
360
|
+
.mass-fill { height: 100%; background: repeating-linear-gradient(90deg, var(--taught) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
|
|
361
|
+
.mass-fill.fly { background: repeating-linear-gradient(90deg, var(--fly) 0 6px, rgba(0, 0, 0, .25) 6px 7px); }
|
|
264
362
|
.hud-empty { color: var(--muted); font-size: .85rem; }
|
|
265
363
|
.chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 220px; overflow-y: auto; margin-bottom: .5rem; }
|
|
266
364
|
.chatlog:empty { display: none; margin-bottom: 0; }
|
|
267
365
|
.chatlog .u { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
268
366
|
.chatlog .u::before { content: "tmct> "; color: var(--taught); }
|
|
269
367
|
.chatlog .a { font-size: .88rem; line-height: 1.4; white-space: pre-wrap; }
|
|
270
|
-
.chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--
|
|
368
|
+
.chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--chrome-edge-lo); padding-top: .5rem; }
|
|
271
369
|
.chatlog:empty + .chatask { border-top: none; padding-top: 0; }
|
|
272
370
|
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
273
|
-
|
|
371
|
+
/* An inset "command slot" — the same LCD-well treatment the turn counter
|
|
372
|
+
and every live tuning readout below use, so typed input, live stats and
|
|
373
|
+
the turn plaque all read as one instrument panel, not three styles. */
|
|
374
|
+
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--chrome-well); color: var(--chrome-well-ink); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-inset); border-radius: 2px; padding: .32rem .55rem; min-width: 0; box-sizing: border-box; }
|
|
375
|
+
.chatask input::placeholder { color: var(--chrome-edge-hi); opacity: .55; }
|
|
274
376
|
.chatask input:disabled { opacity: .5; }
|
|
275
377
|
.chatpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; }
|
|
276
|
-
.pill { font-family: ${MONO_STACK}; font-size: .68rem; padding: .2rem .6rem; border: 1px solid var(--
|
|
277
|
-
.pill:hover:not(:disabled) { border-color: var(--
|
|
378
|
+
.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; }
|
|
379
|
+
.pill:hover:not(:disabled) { border-color: var(--chrome-brass); }
|
|
278
380
|
.pill:disabled { opacity: .45; cursor: default; }
|
|
279
381
|
.pill[data-role="addr"].active { border-color: var(--taught); color: var(--taught); }
|
|
280
382
|
/* The dynamic deception-pill rail (§A.2.4): a true/false tag shown ONLY
|
|
@@ -282,7 +384,7 @@ ${THEME_TOKENS_CSS}
|
|
|
282
384
|
submitted sentence itself (data-sentence, filled into #chatq on click)
|
|
283
385
|
never carries the tag, so a clicked pill is indistinguishable from a
|
|
284
386
|
hand-typed claim once it's in the input. */
|
|
285
|
-
.dynpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--
|
|
387
|
+
.dynpills { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--chrome-edge-lo); }
|
|
286
388
|
.dynpills:empty { display: none; padding-top: 0; border-top: none; }
|
|
287
389
|
.pill[data-role="dyn-addr"][data-active="1"] { border-color: var(--taught); color: var(--taught); }
|
|
288
390
|
.pill[data-role="dyn-claim"][data-truth="true"] { border-color: var(--taught-t2, var(--taught)); }
|
|
@@ -290,19 +392,50 @@ ${THEME_TOKENS_CSS}
|
|
|
290
392
|
.pill[data-role="dyn-claim"][data-truth="false"] { border-style: dashed; border-color: var(--alert); }
|
|
291
393
|
.pill[data-role="dyn-claim"][data-truth="false"]::before { content: "✕ "; opacity: .6; }
|
|
292
394
|
.tuning-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }
|
|
293
|
-
.tuning-col h3 { font-size: .74rem; margin: 0 0 .4rem; font-weight: 600; }
|
|
395
|
+
.tuning-col h3 { font-size: .74rem; margin: 0 0 .4rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
|
|
294
396
|
.tuning-col.spider h3 { color: var(--taught); } .tuning-col.fly h3 { color: var(--fly); }
|
|
295
397
|
.tuning-col label { display: block; font-size: .68rem; color: var(--muted); margin-bottom: .6rem; }
|
|
296
|
-
|
|
297
|
-
|
|
398
|
+
/* Every live number on this page (this readout, the turn plaque below)
|
|
399
|
+
shares the same brass-on-well "LED" treatment — one readout language,
|
|
400
|
+
not a one-off flourish. */
|
|
401
|
+
.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; }
|
|
402
|
+
.tuning-col input[type="range"] {
|
|
403
|
+
display: block; width: 100%; margin-top: .3rem; height: 6px; border-radius: 3px;
|
|
404
|
+
background: var(--chrome-well); box-shadow: var(--chrome-shadow-inset); accent-color: var(--taught);
|
|
405
|
+
-webkit-appearance: none; appearance: none;
|
|
406
|
+
}
|
|
298
407
|
.tuning-col.fly input[type="range"] { accent-color: var(--fly); }
|
|
408
|
+
/* A lever-look thumb (a molded brass-edged cap) — appearance: none above
|
|
409
|
+
drops the OS's own native slider look in browsers that honor it;
|
|
410
|
+
accent-color just above is the fallback for any that don't. */
|
|
411
|
+
.tuning-col input[type="range"]::-webkit-slider-thumb {
|
|
412
|
+
-webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; margin-top: -4px; cursor: pointer;
|
|
413
|
+
background: var(--chrome-face-hi); border: 1px solid var(--chrome-edge-lo); box-shadow: var(--chrome-shadow-raised);
|
|
414
|
+
}
|
|
415
|
+
.tuning-col.spider input[type="range"]::-webkit-slider-thumb { border-color: var(--taught); }
|
|
416
|
+
.tuning-col.fly input[type="range"]::-webkit-slider-thumb { border-color: var(--fly); }
|
|
417
|
+
.tuning-col input[type="range"]::-moz-range-track { height: 6px; border-radius: 3px; background: var(--chrome-well); box-shadow: var(--chrome-shadow-inset); }
|
|
418
|
+
.tuning-col input[type="range"]::-moz-range-thumb {
|
|
419
|
+
width: 14px; height: 14px; border-radius: 50%; cursor: pointer; border: 1px solid var(--chrome-edge-lo);
|
|
420
|
+
background: var(--chrome-face-hi); box-shadow: var(--chrome-shadow-raised);
|
|
421
|
+
}
|
|
422
|
+
.tuning-col.spider input[type="range"]::-moz-range-thumb { border-color: var(--taught); }
|
|
423
|
+
.tuning-col.fly input[type="range"]::-moz-range-thumb { border-color: var(--fly); }
|
|
299
424
|
.tuning-col input:disabled { opacity: .45; }
|
|
300
425
|
.controls-row { display: flex; align-items: center; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.
|
|
426
|
+
/* Chunky keycaps: raised by default, pressed-in on :active — the one place
|
|
427
|
+
this redesign uses interaction (not animation) to sell "physical
|
|
428
|
+
control panel", and it costs nothing under reduced-motion since neither
|
|
429
|
+
state involves a transition, just an instant bevel swap. */
|
|
430
|
+
.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); }
|
|
431
|
+
.controls-row button:hover:not(:disabled) { border-color: var(--chrome-brass); }
|
|
432
|
+
.controls-row button:active:not(:disabled) { box-shadow: var(--chrome-shadow-inset); }
|
|
433
|
+
.controls-row button:disabled { opacity: .4; cursor: default; box-shadow: none; }
|
|
434
|
+
/* The signature element: a brass-framed digital turn counter, the same
|
|
435
|
+
inset-well readout language as every stat above it — the one thing this
|
|
436
|
+
page is remembered by. */
|
|
437
|
+
.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; }
|
|
438
|
+
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .5rem; padding-left: .5rem; border-left: 2px solid var(--chrome-brass); }
|
|
306
439
|
body.preview .side, body.preview .controls-row, body.preview .status, body.preview .tuning { display: none; }
|
|
307
440
|
body.preview main { padding: 0; max-width: none; }
|
|
308
441
|
body.preview .stage { display: block; }
|
|
@@ -386,6 +519,7 @@ const SPIDERFLY = ${gridData};
|
|
|
386
519
|
const classOfAgentId = ${classOfAgentId.toString()};
|
|
387
520
|
const threadCellsForSpiderPlan = ${threadCellsForSpiderPlan.toString()};
|
|
388
521
|
const facingDegreesFor = ${facingDegreesFor.toString()};
|
|
522
|
+
const emotionFor = ${emotionFor.toString()};
|
|
389
523
|
const nextCorpses = ${nextCorpses.toString()};
|
|
390
524
|
const esc = ${escapeHtml.toString()};
|
|
391
525
|
const el = (id) => document.getElementById(id);
|
|
@@ -447,6 +581,7 @@ const SPIDERFLY = ${gridData};
|
|
|
447
581
|
const goalById = {};
|
|
448
582
|
const spriteEls = {};
|
|
449
583
|
const facingByAgent = {};
|
|
584
|
+
const emotionByAgent = {};
|
|
450
585
|
let corpses = {};
|
|
451
586
|
const corpseEls = {};
|
|
452
587
|
let selectedAddresseeId = null;
|
|
@@ -461,11 +596,32 @@ const SPIDERFLY = ${gridData};
|
|
|
461
596
|
function removeStaleSprites(agents) {
|
|
462
597
|
for (const id of Object.keys(spriteEls)) {
|
|
463
598
|
if (!agents[id]) {
|
|
464
|
-
spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; delete facingByAgent[id];
|
|
599
|
+
spriteEls[id].remove(); delete spriteEls[id]; delete goalById[id]; delete facingByAgent[id]; delete emotionByAgent[id];
|
|
465
600
|
}
|
|
466
601
|
}
|
|
467
602
|
}
|
|
468
603
|
|
|
604
|
+
// maxMassFor/propertyFactsForAgent (emotionFor's own caller-side glue): only
|
|
605
|
+
// spider/fly carry the mgx:feels parameter (data/sprites-large/*-with-
|
|
606
|
+
// emotion.toml exists for those two classes only) — every other class (egg,
|
|
607
|
+
// and any class this page has no template for at all) keeps propertyFacts
|
|
608
|
+
// empty, resolving through its plain template exactly as before this page
|
|
609
|
+
// wired emotion through. maxMassFor mirrors massBarHtml's own per-class
|
|
610
|
+
// denominator below so the HUD's mass bar and the sprite's own expression
|
|
611
|
+
// read the same "how full" scale.
|
|
612
|
+
function maxMassFor(cls) {
|
|
613
|
+
return cls === "spider" ? SPIDERFLY.maxSpiderMass : cls === "fly" ? SPIDERFLY.maxFlyMass : null;
|
|
614
|
+
}
|
|
615
|
+
function propertyFactsForAgent(agent, cls) {
|
|
616
|
+
if (cls !== "spider" && cls !== "fly") return [];
|
|
617
|
+
return [{ predicate: "mgx:feels", object: emotionFor(agent, cls, maxMassFor(cls)) }];
|
|
618
|
+
}
|
|
619
|
+
function resolveSpriteFace(cls, propertyFacts) {
|
|
620
|
+
return window.tmctSpiderFly
|
|
621
|
+
? tmctSpiderFly.resolveSpriteAsset(cls, (session && session.taxonomyRows) || [], propertyFacts, SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
|
|
622
|
+
: "";
|
|
623
|
+
}
|
|
624
|
+
|
|
469
625
|
function togglePov(id) {
|
|
470
626
|
povAgentId = povAgentId === id ? null : id;
|
|
471
627
|
if (!povAgentId) for (const node of Object.values(spriteEls)) node.classList.remove("dimmed");
|
|
@@ -475,27 +631,25 @@ const SPIDERFLY = ${gridData};
|
|
|
475
631
|
if (e.key === "Escape" && povAgentId) togglePov(povAgentId);
|
|
476
632
|
});
|
|
477
633
|
|
|
478
|
-
function ensureSpriteEl(id, cls) {
|
|
634
|
+
function ensureSpriteEl(id, cls, agent) {
|
|
479
635
|
let node = spriteEls[id];
|
|
480
636
|
if (node) return node;
|
|
481
637
|
node = document.createElement("div");
|
|
482
638
|
node.className = "sprite";
|
|
483
639
|
node.dataset.cls = cls;
|
|
484
640
|
// Property-aware resolution (sprite-templates.mjs's resolveSpriteAsset):
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
? tmctSpiderFly.resolveSpriteAsset(cls, (session && session.taxonomyRows) || [], [], SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
|
|
492
|
-
: "";
|
|
641
|
+
// a spider/fly's live mgx:feels emotion (emotionFor, above) resolves it
|
|
642
|
+
// through the matching data/sprites-large/*-with-emotion.toml template;
|
|
643
|
+
// every other class (egg) keeps propertyFacts empty and resolves through
|
|
644
|
+
// its plain class template (or the flat SPRITE_REGISTRY), unchanged.
|
|
645
|
+
const propertyFacts = propertyFactsForAgent(agent, cls);
|
|
646
|
+
emotionByAgent[id] = propertyFacts[0] ? propertyFacts[0].object : null;
|
|
493
647
|
// The sprite SVG lives in its own inner wrapper so plan-driven facing
|
|
494
648
|
// (a CSS rotate on THIS wrapper) never fights the outer .sprite node's
|
|
495
649
|
// own translate(-50%,-50%) positioning transform.
|
|
496
650
|
const face = document.createElement("div");
|
|
497
651
|
face.className = "sprite-face";
|
|
498
|
-
face.innerHTML =
|
|
652
|
+
face.innerHTML = resolveSpriteFace(cls, propertyFacts);
|
|
499
653
|
node.appendChild(face);
|
|
500
654
|
if (!preview) {
|
|
501
655
|
node.tabIndex = 0;
|
|
@@ -515,7 +669,7 @@ const SPIDERFLY = ${gridData};
|
|
|
515
669
|
removeStaleSprites(agents);
|
|
516
670
|
for (const [id, a] of Object.entries(agents)) {
|
|
517
671
|
const cls = classOfAgentId(id);
|
|
518
|
-
const node = ensureSpriteEl(id, cls);
|
|
672
|
+
const node = ensureSpriteEl(id, cls, a);
|
|
519
673
|
const parsed = tmctSpiderFly.parseCellId(a.cell);
|
|
520
674
|
if (!parsed) continue;
|
|
521
675
|
const pct = cellCenterPct(parsed.x, parsed.y);
|
|
@@ -528,7 +682,20 @@ const SPIDERFLY = ${gridData};
|
|
|
528
682
|
// whole point of this page, not a glitch).
|
|
529
683
|
facingByAgent[id] = facingDegreesFor(a.plan, facingByAgent[id]);
|
|
530
684
|
const face = node.querySelector(".sprite-face");
|
|
531
|
-
if (face)
|
|
685
|
+
if (face) {
|
|
686
|
+
face.style.transform = "rotate(" + facingByAgent[id] + "deg)";
|
|
687
|
+
// Emotion changes tick-to-tick (a fly's fear spikes the instant a
|
|
688
|
+
// spider comes into view, a spider's joy fires the instant it eats),
|
|
689
|
+
// so this must re-resolve every redraw, not just at sprite creation —
|
|
690
|
+
// but only actually replace the DOM when the emotion word itself
|
|
691
|
+
// changed since last render, never on every tick regardless.
|
|
692
|
+
const propertyFacts = propertyFactsForAgent(a, cls);
|
|
693
|
+
const emotion = propertyFacts[0] ? propertyFacts[0].object : null;
|
|
694
|
+
if (emotion !== emotionByAgent[id]) {
|
|
695
|
+
emotionByAgent[id] = emotion;
|
|
696
|
+
face.innerHTML = resolveSpriteFace(cls, propertyFacts);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
532
699
|
if (a.goal) goalById[id] = a.goal;
|
|
533
700
|
}
|
|
534
701
|
lastAgents = agents;
|
|
@@ -554,9 +721,12 @@ const SPIDERFLY = ${gridData};
|
|
|
554
721
|
node.setAttribute("aria-hidden", "true");
|
|
555
722
|
const face = document.createElement("div");
|
|
556
723
|
face.className = "sprite-face";
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
724
|
+
// A corpse's expression is frozen — never re-resolved on later
|
|
725
|
+
// redraws (there's no more agent state to derive one from), and
|
|
726
|
+
// never emotion-parameterized at all (a rotting carcass has none),
|
|
727
|
+
// just its class's own plain template under the grayscale/fade the
|
|
728
|
+
// .sprite.corpse CSS rule already applies.
|
|
729
|
+
face.innerHTML = resolveSpriteFace(corpse.cls, []);
|
|
560
730
|
node.appendChild(face);
|
|
561
731
|
spriteLayer.appendChild(node);
|
|
562
732
|
corpseEls[id] = node;
|