@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.6

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.
@@ -25,7 +25,7 @@
25
25
  // renderIngestHtml() is pure: no I/O, deterministic output for identical
26
26
  // input. scripts/build-demo-site.mjs calls it directly and writes the result
27
27
  // to public/ingest.html, after ingest-browser.bundle.js already exists.
28
- import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml } from "./viz-theme.mjs";
28
+ import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
29
29
  import {
30
30
  bandLabelFor,
31
31
  statsSummaryLine,
@@ -75,7 +75,8 @@ ${THEME_TOKENS_CSS}
75
75
  header.topbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .7rem 1.1rem; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
76
76
  .brand { display: flex; flex-direction: column; gap: .1rem; }
77
77
  .eyebrow { font-family: ${MONO_STACK}; font-size: .78rem; letter-spacing: .08em; color: var(--muted); }
78
- .subtitle { font-size: .82rem; color: var(--muted); }
78
+ ${EYEBROW_LINKS_CSS}
79
+ .subtitle { margin: 0; font-size: .82rem; font-weight: 400; color: var(--muted); }
79
80
 
80
81
  /* the live memory count, in the topbar rather than the status line: it is
81
82
  the one number that says what this page's memory actually holds, and the
@@ -181,8 +182,8 @@ ${THEME_TOKENS_CSS}
181
182
  <div class="ingestCol">
182
183
  <header class="topbar">
183
184
  <div class="brand">
184
- <span class="eyebrow">the-mechanical-code-talker</span>
185
- <span class="subtitle">ingest &mdash; paste or drop text. It keeps the facts it can ground and skips the rest.</span>
185
+ <span class="eyebrow">${demoEyebrowHtml("ingest", "ingest")}</span>
186
+ <h1 class="subtitle">ingest &mdash; paste or drop text. It keeps the facts it can ground and skips the rest.</h1>
186
187
  </div>
187
188
  <div class="topbar-right">
188
189
  <span class="fact-pill" id="factPill" aria-live="polite"
@@ -15,7 +15,7 @@
15
15
  // doesn't do: it reads the checked-in chat-dock engine bundle.
16
16
 
17
17
  import { loadMemory, readFactRows, findContradictions, normFactTerm } from "../adapters/memory/core.mjs";
18
- import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson, countLabel, TOKENS } from "./viz-theme.mjs";
18
+ import { THEME_TOKENS_CSS, MONO_STACK, escapeHtml, embedJson, countLabel, TOKENS, demoEyebrowHtml, EYEBROW_LINKS_CSS } from "./viz-theme.mjs";
19
19
  import { createTicker, prefersReducedMotion } from "./viz-ticker.mjs";
20
20
  import { bfsLevels } from "../domain/planning.mjs";
21
21
  import { pluralOf } from "../domain/inflect.mjs";
@@ -644,7 +644,9 @@ ${DASH_DARK_CHROME_CSS}
644
644
  body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${DASH_SANS_STACK}; font-size: 15px; line-height: 1.5; }
645
645
  .mono { font-family: ${MONO_STACK}; }
646
646
  main { max-width: 1200px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
647
+ .visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
647
648
  .eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4em 1.2em; margin-bottom: .9rem; }
649
+ ${EYEBROW_LINKS_CSS}
648
650
  button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
649
651
  button:focus-visible, input:focus-visible { outline: 2px solid var(--corpus); outline-offset: 2px; border-radius: 4px; }
650
652
  .topbar { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: .5rem .7rem; margin-bottom: 1.1rem; }
@@ -789,7 +791,8 @@ ${DASH_DARK_CHROME_CSS}
789
791
  </head>
790
792
  <body>
791
793
  <main>
792
- <div class="eyebrow"><span>tmct &middot; memory ledger</span><span id="counts"></span></div>
794
+ <h1 class="visually-hidden">memory ledger</h1>
795
+ <div class="eyebrow"><span>${demoEyebrowHtml("ledger", "memory ledger")}</span><span id="counts"></span></div>
793
796
  ${dashboardHtml(stats)}
794
797
  <div class="topbar">
795
798
  <nav class="crumbs" id="crumbs" aria-label="Focus trail"></nav>
@@ -76,7 +76,7 @@
76
76
  // actually executed, never a race between panes.
77
77
  import {
78
78
  THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel,
79
- wordBeforeCursor, rowsForWorld, appendLogLine,
79
+ wordBeforeCursor, rowsForWorld, appendLogLine, demoEyebrowHtml, EYEBROW_LINKS_CSS,
80
80
  } from "./viz-theme.mjs";
81
81
  import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
82
82
  import { directedGridLayout, roomGraphSvg, levelsOf, EXIT_DELTA } from "./viz-room-graph.mjs";
@@ -392,7 +392,7 @@ ${MUD_SHARE_SKIN}
392
392
  <body>
393
393
  <main>
394
394
  <header class="mud-topbar">
395
- <h1 class="eyebrow">tmct &middot; mud</h1>
395
+ <h1 class="eyebrow">${demoEyebrowHtml("mud", "mud")}</h1>
396
396
  <div class="mud-topbar-actions">
397
397
  <button type="button" class="state-pill" id="statePill" aria-expanded="false" aria-controls="netPanel"
398
398
  title="whether this burrow is shared with anyone"><i class="state-dot"></i><span id="statePillWord">not shared</span></button>
@@ -411,7 +411,7 @@ ${scenarioList.length > 1 ? ` <select id="scenarioSelect" class="deck-sel
411
411
  ${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " selected" : ""}>${escapeHtml(s.label || scenarioLabel(s.worldPayload?.name))}</option>`).join("\n")}
412
412
  </select>` : ""}
413
413
  <button type="button" id="editModeBtn" aria-pressed="false">edit</button>
414
- <label class="deck-teach" title="With this on, a sentence like &quot;Candle is in the study.&quot; writes a fact into the world instead of running as a command.">
414
+ <label class="deck-teach" title="With this on, a sentence like &quot;Pebble lies in the garden.&quot; writes a fact into the world instead of running as a command.">
415
415
  <input type="checkbox" id="teachToggle">
416
416
  teach
417
417
  </label>
@@ -590,6 +590,7 @@ const MUD_STYLE = `
590
590
  .mono { font-family: ${MONO_STACK}; }
591
591
  main { max-width: 1280px; margin: 0 auto; padding: 1.1rem 1.2rem 2.4rem; }
592
592
  .eyebrow { font-family: ${MONO_STACK}; font-weight: 500; font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: var(--parchment); opacity: .9; margin: 0 0 .8rem; }
593
+ ${EYEBROW_LINKS_CSS}
593
594
 
594
595
  /* ---- the header: brand on the left, the sharing chrome on the right ----
595
596
  The same arrangement chat.html's topbar holds — sharing is page chrome,
@@ -42,12 +42,10 @@
42
42
  // agent or item, never the mid-lerp position. This is what an e2e
43
43
  // assertion reads, and what a later screenshot ready-check reads too
44
44
  // (see test-e2e/pages-mudiii.test.mjs's own header).
45
+ // .flashCell(cellId) / .showRoute(cells) / .clearRoute() — the clicked
46
+ // cell and the route the followed agent is walking to it, drawn along
47
+ // the cells the world's own exit search returned.
45
48
  // .ready() — whether boot() has finished at least once.
46
- // These three calls are not yet wired into mudiii-viz.mjs's own
47
- // boot()/applyTickResult()/camera handlers — that file is owned by the viz
48
- // track, not this one; the coordinator's own report names the exact call
49
- // sites needed, mirroring how window.mudiiiHandleSceneClick was already
50
- // added for the reverse direction.
51
49
  //
52
50
  // Reused from mudiii-viz.mjs rather than re-derived, off its own frozen
53
51
  // exports: `roleOfAgentId`, `cellToWorld`, `cellFromGroundPoint`,
@@ -366,6 +364,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
366
364
  // removal (source "ecology") apart from applyTick's own diff no-op
367
365
  // (source "diff") without guessing from a vanished mesh alone.
368
366
  var removalLog = [];
367
+ var routeLine = null, routeCells = [];
368
+ var flashMesh = null, flashUntil = 0;
369
+ var FLASH_MS = 600;
370
+ // The floor a one-shot flourish is held for when its own clip is shorter,
371
+ // long enough that a bite reads as a bite at the deck's fastest tick.
372
+ var ONE_SHOT_HOLD_MIN_MS = 450;
373
+ var tickRungs = {};
369
374
  var cameraState = { mode: "overhead", selectedId: null };
370
375
  var cameraTween = null, lookAtTween = null;
371
376
  var lastFrameTs = null;
@@ -557,10 +562,8 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
557
562
  }
558
563
  }
559
564
 
560
- // ---- items: crumbs and morsels, the committed hay bale at two target
561
- // heights (food-crumb 0.16, food-morsel 0.36 in data/mudiii-assets.json),
562
- // matching the on-ground footprint the primitive spheres they replace were
563
- // already tuned to. ---------------------------------------------------------
565
+ // ---- items: crumbs and morsels, both the committed hay bale, each at
566
+ // whatever targetHeight its own data/mudiii-assets.json row names. ----------
564
567
  function itemAssetKeyFor(kind) { return kind === "morsel" ? "food-morsel" : "food-crumb"; }
565
568
 
566
569
  function animateScaleTo(object3D, target, now) {
@@ -635,21 +638,56 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
635
638
  // correctly clone a SkinnedMesh, and at this roster size — a handful of
636
639
  // foxes and goblins — the extra memory of a separate load per agent is
637
640
  // trivial next to that whole class of bug). ---------------------------------
641
+ // The id, drawn to a canvas and hung above the agent as a sprite. Parenting
642
+ // it to the agent's own group means it rides the movement tween with no
643
+ // per-frame bookkeeping, and a sprite always faces the camera, so there is
644
+ // no facing maths either. sizeAttenuation off holds it at a constant size on
645
+ // screen — an id that shrinks to nothing as the camera pulls back is worse
646
+ // than no id at all — which is why the scale below is in screen fractions
647
+ // rather than world units.
648
+ var LABEL_SCREEN_WIDTH = 0.13;
649
+ function makeAgentLabel(id, height) {
650
+ var canvas = document.createElement("canvas");
651
+ canvas.width = 256;
652
+ canvas.height = 64;
653
+ var ctx = canvas.getContext("2d");
654
+ ctx.font = "bold 38px ui-monospace, SFMono-Regular, Menlo, monospace";
655
+ ctx.textAlign = "center";
656
+ ctx.textBaseline = "middle";
657
+ ctx.lineWidth = 8;
658
+ ctx.lineJoin = "round";
659
+ ctx.strokeStyle = "rgba(20,16,10,.85)";
660
+ ctx.strokeText(id, 128, 34);
661
+ ctx.fillStyle = "#F3ECDD";
662
+ ctx.fillText(id, 128, 34);
663
+ var material = new THREE.SpriteMaterial({
664
+ map: new THREE.CanvasTexture(canvas), transparent: true, sizeAttenuation: false, depthWrite: false,
665
+ });
666
+ var sprite = new THREE.Sprite(material);
667
+ sprite.scale.set(LABEL_SCREEN_WIDTH, LABEL_SCREEN_WIDTH / 4, 1);
668
+ sprite.position.y = height + 0.3;
669
+ sprite.name = "label-" + id;
670
+ return sprite;
671
+ }
672
+
638
673
  function ensureAgent(id, agent) {
639
674
  if (agentGroups[id]) return agentGroups[id];
640
675
  var kind = roleOfAgentId(id);
641
676
  var entry = {
642
677
  group: new THREE.Group(), tween: null, cell: null, facing: agent.facing, role: agent.role,
643
678
  kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null, oneShotAction: null,
679
+ oneShotUntil: 0, model: null,
644
680
  };
645
681
  entry.group.visible = false;
646
682
  scene.add(entry.group);
647
683
  agentGroups[id] = entry;
648
684
  var asset = manifestByKind[kind];
685
+ entry.group.add(makeAgentLabel(id, asset ? asset.targetHeight : 1));
649
686
  if (asset) {
650
687
  loadGlbRaw(modelUrlFor(asset.destPath)).then(function (gltf) {
651
688
  normalizeToHeight(gltf.scene, asset.targetHeight);
652
689
  entry.group.add(gltf.scene);
690
+ entry.model = gltf.scene;
653
691
  entry.group.visible = true;
654
692
  entry.clipMap = asset.clips || {};
655
693
  entry.mixer = new THREE.AnimationMixer(gltf.scene);
@@ -671,6 +709,12 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
671
709
  // keep blending into every clip after it, forever). Fade it out before
672
710
  // any of the guards below can skip the rest of this call.
673
711
  if (entry.oneShotAction) {
712
+ // A flourish holds for its own clip length, floored, before anything may
713
+ // fade it out. At the deck's 220ms default the next tick otherwise
714
+ // landed during the wind-up and a bite read as a twitch. This holds the
715
+ // ANIMATION only: the tick that arrived has already moved the agent, so
716
+ // the simulation never waits on a flourish.
717
+ if (performance.now() < entry.oneShotUntil) return;
674
718
  entry.oneShotAction.fadeOut(0.15);
675
719
  entry.oneShotAction = null;
676
720
  }
@@ -702,6 +746,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
702
746
  action.fadeIn(0.15).play();
703
747
  if (prev) prev.fadeOut(0.15);
704
748
  entry.oneShotAction = action;
749
+ var clip = typeof action.getClip === "function" ? action.getClip() : null;
750
+ entry.oneShotUntil = performance.now()
751
+ + Math.max(ONE_SHOT_HOLD_MIN_MS, clip && clip.duration ? clip.duration * 1000 : 0);
705
752
  entry.currentClip = null;
706
753
  }
707
754
 
@@ -731,7 +778,10 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
731
778
  }
732
779
  if (entry.clipMap) {
733
780
  var moving = singleHop;
734
- playClip(entry, clipForAction(agent.role, currentActionFor(id, lastAgentsById, moving), entry.clipMap));
781
+ // A hand-driven step is a walk, whatever the agent believes: it did not
782
+ // choose to chase or flee, the visitor chose for it.
783
+ var action = moving && tickRungs[id] === "driven" ? "driven" : currentActionFor(id, lastAgentsById, moving);
784
+ playClip(entry, clipForAction(agent.role, action, entry.clipMap));
735
785
  }
736
786
  }
737
787
 
@@ -766,6 +816,61 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
766
816
  }
767
817
  }
768
818
 
819
+ // ---- the clicked cell, and the route to it --------------------------------
820
+ // The line follows the cells the world's own exit search returned, never a
821
+ // straight segment from agent to target: a straight one cuts through
822
+ // buildings and promises a walk the board would refuse.
823
+ function clearRoute() {
824
+ routeCells = [];
825
+ if (!routeLine) return;
826
+ if (routeLine.parent) routeLine.parent.remove(routeLine);
827
+ routeLine.geometry.dispose();
828
+ routeLine.material.dispose();
829
+ routeLine = null;
830
+ }
831
+
832
+ function showRoute(cells) {
833
+ clearRoute();
834
+ if (!scene || !cells || cells.length < 2) return;
835
+ var points = [];
836
+ for (var i = 0; i < cells.length; i += 1) {
837
+ var world = cellToWorld(cells[i], GRID_SIZE, CELL_SIZE);
838
+ if (!world) return;
839
+ points.push(new THREE.Vector3(world.x, 0.09, world.z));
840
+ }
841
+ routeLine = new THREE.Line(
842
+ new THREE.BufferGeometry().setFromPoints(points),
843
+ new THREE.LineBasicMaterial({ color: 0xd98a2b }),
844
+ );
845
+ routeLine.name = "route";
846
+ scene.add(routeLine);
847
+ routeCells = cells.slice();
848
+ }
849
+
850
+ function clearFlash() {
851
+ if (!flashMesh) return;
852
+ if (flashMesh.parent) flashMesh.parent.remove(flashMesh);
853
+ flashMesh.geometry.dispose();
854
+ flashMesh.material.dispose();
855
+ flashMesh = null;
856
+ }
857
+
858
+ function flashCell(cell) {
859
+ if (!scene) return;
860
+ var world = cellToWorld(cell, GRID_SIZE, CELL_SIZE);
861
+ if (!world) return;
862
+ clearFlash();
863
+ flashMesh = new THREE.Mesh(
864
+ new THREE.PlaneGeometry(CELL_SIZE, CELL_SIZE),
865
+ new THREE.MeshBasicMaterial({ color: 0xd98a2b, transparent: true, opacity: 0.75 }),
866
+ );
867
+ flashMesh.name = "cell-flash";
868
+ flashMesh.rotation.x = -Math.PI / 2;
869
+ flashMesh.position.set(world.x, 0.03, world.z);
870
+ scene.add(flashMesh);
871
+ flashUntil = performance.now() + FLASH_MS;
872
+ }
873
+
769
874
  // ---- camera ---------------------------------------------------------------
770
875
  function agentSnapshotFor(id) {
771
876
  var entry = agentGroups[id];
@@ -817,6 +922,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
817
922
  itemMeshes = {};
818
923
  lastAgentsById = {};
819
924
  lastItemsById = {};
925
+ tickRungs = {};
926
+ clearRoute();
927
+ clearFlash();
820
928
  removalLog = [];
821
929
  manifestByKind = buildManifestByKind(input && input.assetManifest);
822
930
  await placeProps((input && input.propPlacements) || []);
@@ -829,6 +937,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
829
937
  var now = performance.now();
830
938
  var agents = (tick && tick.agents) || {};
831
939
  var items = (tick && tick.items) || {};
940
+ tickRungs = (tick && tick.rungs) || {};
832
941
  for (var id in agents) if (Object.prototype.hasOwnProperty.call(agents, id)) applyAgentTick(id, agents[id], now);
833
942
  for (var itemId in items) if (Object.prototype.hasOwnProperty.call(items, itemId)) applyItemTick(itemId, items[itemId], now);
834
943
  // Ecology first: an eaten agent/item is already gone from this tick's own
@@ -859,6 +968,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
859
968
  }
860
969
  if (entry.mixer) entry.mixer.update(deltaSec);
861
970
  }
971
+ if (flashMesh) {
972
+ var flashLeft = flashUntil - performance.now();
973
+ if (flashLeft <= 0) clearFlash();
974
+ else flashMesh.material.opacity = 0.75 * (flashLeft / FLASH_MS);
975
+ }
862
976
  if (cameraTween) { var cp = tweenStep(cameraTween, ts); camera3.position.set(cp.x, cp.y, cp.z); }
863
977
  if (lookAtTween) { var lp = tweenStep(lookAtTween, ts); camera3.lookAt(lp.x, lp.y, lp.z); }
864
978
  if (orbitControls && orbitControls.enabled) orbitControls.update();
@@ -869,6 +983,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
869
983
  boot: boot,
870
984
  applyTick: applyTick,
871
985
  setCamera: setCamera,
986
+ flashCell: flashCell,
987
+ showRoute: showRoute,
988
+ clearRoute: clearRoute,
989
+ // The route currently drawn, cell by cell — an e2e assertion's read, so
990
+ // it can check the line follows the board's own exits rather than
991
+ // counting pixels on a software renderer.
992
+ routeCellsDrawn: function () { return routeCells.slice(); },
872
993
  cellOf: function (id) {
873
994
  if (agentGroups[id]) return agentGroups[id].cell;
874
995
  if (itemMeshes[id]) return itemMeshes[id].cell;
@@ -886,10 +1007,13 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
886
1007
  // manifest's own targetHeight to compare it against — an e2e assertion's
887
1008
  // read, so it goes through the group actually in the scene rather than a
888
1009
  // second, locally invented measurement.
1010
+ // Measured through the loaded MODEL, never the whole group: the group
1011
+ // also carries the id label, whose sprite geometry would widen the box
1012
+ // and report a height nobody rendered.
889
1013
  meshHeightOf: function (id) {
890
1014
  var entry = agentGroups[id];
891
- if (!entry || !entry.group || entry.group.children.length === 0) return null;
892
- var box = new THREE.Box3().setFromObject(entry.group);
1015
+ if (!entry || !entry.model) return null;
1016
+ var box = new THREE.Box3().setFromObject(entry.model);
893
1017
  var size = new THREE.Vector3();
894
1018
  box.getSize(size);
895
1019
  var asset = manifestByKind[entry.kind];