@polycode-projects/the-mechanical-code-talker 5.0.6 → 5.0.8
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/README.md +21 -0
- package/bin/tmct.mjs +63 -2
- package/package.json +3 -2
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +8 -1
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +118 -23
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +228 -24
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +108 -25
- package/src/services/ledger-viz.mjs +4 -2
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +17 -0
- package/src/services/mudiii-scene.mjs +271 -24
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +302 -125
- package/src/services/plan-viz.mjs +23 -2
- package/src/services/predator-prey.mjs +82 -33
- package/src/services/research-viz.mjs +12 -19
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +10 -3
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
|
@@ -11,11 +11,8 @@
|
|
|
11
11
|
// this file renders is a read of one of those two — never a second, locally
|
|
12
12
|
// invented notion of where an agent stands.
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// the vendor bundle (handles KHR_mesh_quantization and EXT_texture_webp
|
|
17
|
-
// natively; only EXT_meshopt_compression needs a decoder line), and there is
|
|
18
|
-
// no globalThis.tmctMudiii — pages publish one globalThis.tmct.
|
|
14
|
+
// GLTFLoader ships in the vendor bundle, and handles KHR_mesh_quantization and
|
|
15
|
+
// EXT_texture_webp natively; only EXT_meshopt_compression needs a decoder line.
|
|
19
16
|
//
|
|
20
17
|
// The page-shell <-> scene handshake, since the two scripts share no scope:
|
|
21
18
|
// - the page shell already defines `window.mudiiiHandleSceneClick(cellId)`
|
|
@@ -68,6 +65,12 @@ import { prefersReducedMotion } from "./viz-ticker.mjs";
|
|
|
68
65
|
* own `transition: left .25s ease` becomes in a requestAnimationFrame loop. */
|
|
69
66
|
export const TWEEN_DURATION_MS = 250;
|
|
70
67
|
|
|
68
|
+
/** The vertical field of view the page's one perspective camera is built with.
|
|
69
|
+
* Every rig this scene asks `cameraRigFor` for is measured against the live
|
|
70
|
+
* `camera3.fov`, so this number is only ever the camera's own starting value
|
|
71
|
+
* rather than a second copy a rig could drift from. */
|
|
72
|
+
export const CAMERA_FIELD_OF_VIEW_DEGREES = 55;
|
|
73
|
+
|
|
71
74
|
// ---------------------------------------------------------------------------
|
|
72
75
|
// Pure geometry/tween helpers — unit-tested directly in Node, then spliced
|
|
73
76
|
// via `.toString()` into the standalone browser IIFE below, the same
|
|
@@ -100,8 +103,11 @@ export function tweenStep(tween, now) {
|
|
|
100
103
|
return { ...point, t, done: false };
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
/** A fresh tween from `from` to `to`, starting at `now`. Pure
|
|
104
|
-
|
|
106
|
+
/** A fresh tween from `from` to `to`, starting at `now`. Pure — the default
|
|
107
|
+
* duration is written out rather than read from `TWEEN_DURATION_MS`, because
|
|
108
|
+
* the browser runs a `.toString()` copy of this function in a script that
|
|
109
|
+
* declares no such binding. mudiii-viz.test.mjs holds the two together. */
|
|
110
|
+
export function startTween(from, to, now, durationMs = 250) {
|
|
105
111
|
return { from, to, startedAt: now, durationMs };
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -111,7 +117,7 @@ export function startTween(from, to, now, durationMs = TWEEN_DURATION_MS) {
|
|
|
111
117
|
* tweening); `to` is the new destination point. Pure — `tweenStep` is
|
|
112
118
|
* called on the OLD tween at `now` to read the current position before
|
|
113
119
|
* building the new one. */
|
|
114
|
-
export function reseedTween(existingTween, to, now, durationMs =
|
|
120
|
+
export function reseedTween(existingTween, to, now, durationMs = 250) {
|
|
115
121
|
const current = existingTween ? tweenStep(existingTween, now) : null;
|
|
116
122
|
const from = current ? Object.fromEntries(Object.keys(to).map((k) => [k, current[k]])) : to;
|
|
117
123
|
return startTween(from, to, now, durationMs);
|
|
@@ -128,6 +134,69 @@ export function chebyshevDistanceBetweenCells(a, b) {
|
|
|
128
134
|
return Math.max(Math.abs(Number(ma[1]) - Number(mb[1])), Math.abs(Number(ma[2]) - Number(mb[2])));
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
/** Where the camera should aim once the visitor's own drag is applied on top
|
|
138
|
+
* of the rig's aim. `position` is where the rig put the camera, `lookAt` is
|
|
139
|
+
* where the rig wants it pointed, and `offset` is `{ yaw, pitch }` in radians
|
|
140
|
+
* — the drag's accumulated turn, held apart from the rig so a tick that
|
|
141
|
+
* recomputes the rig leaves the visitor's view alone.
|
|
142
|
+
*
|
|
143
|
+
* The offset turns the rig's own aim rather than replacing it, so the view
|
|
144
|
+
* swings with the agent when it turns: in POV that reads as the animal
|
|
145
|
+
* turning its head, which is the whole point of riding one.
|
|
146
|
+
*
|
|
147
|
+
* Pitch is clamped to 1.2 radians (about 69 degrees) either side of level, so
|
|
148
|
+
* a long upward drag can never carry the view past vertical and roll the
|
|
149
|
+
* board upside down. The returned `pitchOffset` is what survived that clamp,
|
|
150
|
+
* which the caller writes back over its own accumulator — otherwise a drag
|
|
151
|
+
* held past the clamp banks up an offset the view never shows, and the next
|
|
152
|
+
* drag back the other way does nothing until it has spent it.
|
|
153
|
+
*
|
|
154
|
+
* Returns `{ x, y, z, pitchOffset }`. Pure, self-contained. */
|
|
155
|
+
export function lookTargetWithOffset(position, lookAt, offset) {
|
|
156
|
+
const MAX_PITCH_RADIANS = 1.2;
|
|
157
|
+
const yaw = Number(offset && offset.yaw) || 0;
|
|
158
|
+
const pitch = Number(offset && offset.pitch) || 0;
|
|
159
|
+
if (!yaw && !pitch) return { x: lookAt.x, y: lookAt.y, z: lookAt.z, pitchOffset: 0 };
|
|
160
|
+
const dx = lookAt.x - position.x;
|
|
161
|
+
const dy = lookAt.y - position.y;
|
|
162
|
+
const dz = lookAt.z - position.z;
|
|
163
|
+
const radius = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
164
|
+
if (!radius) return { x: lookAt.x, y: lookAt.y, z: lookAt.z, pitchOffset: 0 };
|
|
165
|
+
const basePitch = Math.asin(Math.max(-1, Math.min(1, dy / radius)));
|
|
166
|
+
const aimedPitch = Math.max(-MAX_PITCH_RADIANS, Math.min(MAX_PITCH_RADIANS, basePitch + pitch));
|
|
167
|
+
const aimedYaw = Math.atan2(dx, dz) + yaw;
|
|
168
|
+
const flat = Math.cos(aimedPitch) * radius;
|
|
169
|
+
return {
|
|
170
|
+
x: position.x + flat * Math.sin(aimedYaw),
|
|
171
|
+
y: position.y + radius * Math.sin(aimedPitch),
|
|
172
|
+
z: position.z + flat * Math.cos(aimedYaw),
|
|
173
|
+
pitchOffset: aimedPitch - basePitch,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The town square's own sky, as gradient stops running from straight up
|
|
178
|
+
* (`at` 0) to straight down (`at` 1), each `{ at, color }`.
|
|
179
|
+
*
|
|
180
|
+
* The colours are the page's own `--square-sky`, `--square-horizon`,
|
|
181
|
+
* `--square-stone` and `--square-stone-dark`, in the order the page body's
|
|
182
|
+
* own background already reads them, so the canvas continues the page it
|
|
183
|
+
* sits in rather than cutting a dark hole in it. The board is twelve units
|
|
184
|
+
* across with nothing beyond it, so a camera aimed anywhere off it — the
|
|
185
|
+
* opening follow shot, an overhead orbit, a long look-drag in POV — is
|
|
186
|
+
* looking at this and nothing else.
|
|
187
|
+
*
|
|
188
|
+
* The band sits at 0.5 because that is the horizon in an equirectangular
|
|
189
|
+
* projection, where the caller maps these onto a sphere. Pure,
|
|
190
|
+
* self-contained. */
|
|
191
|
+
export function skyGradientStops() {
|
|
192
|
+
return [
|
|
193
|
+
{ at: 0, color: "#BFE3F0" },
|
|
194
|
+
{ at: 0.46, color: "#E9D9B6" },
|
|
195
|
+
{ at: 0.52, color: "#8C8172" },
|
|
196
|
+
{ at: 1, color: "#59503F" },
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
131
200
|
/** A facing word to a Y rotation in radians. Assumes a model's own local
|
|
132
201
|
* forward is +Z (true for fox and goblin, the two rigs in the current
|
|
133
202
|
* manifest, checked by walking each GLB's own bind-pose bone chain) so a
|
|
@@ -142,7 +211,7 @@ export function yawForFacing(facing) {
|
|
|
142
211
|
/** Whether `agentId` (predator or prey) currently believes an agent of the
|
|
143
212
|
* opposing role — the one belief-derived bit that separates a predator's
|
|
144
213
|
* "chase" rung from "wander", and a prey's "evade" rung from "forage"/
|
|
145
|
-
* "wander"
|
|
214
|
+
* "wander". Read entirely off
|
|
146
215
|
* the tick payload's own `role`/`belief` fields — no rung name travels on
|
|
147
216
|
* the wire, so this reconstructs which rung applied rather than trusting a
|
|
148
217
|
* label. Pure. */
|
|
@@ -317,6 +386,8 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
317
386
|
var startTween = ${startTween.toString()};
|
|
318
387
|
var reseedTween = ${reseedTween.toString()};
|
|
319
388
|
var chebyshevDistanceBetweenCells = ${chebyshevDistanceBetweenCells.toString()};
|
|
389
|
+
var lookTargetWithOffset = ${lookTargetWithOffset.toString()};
|
|
390
|
+
var skyGradientStops = ${skyGradientStops.toString()};
|
|
320
391
|
var yawForFacing = ${yawForFacing.toString()};
|
|
321
392
|
var threatEngagedFor = ${threatEngagedFor.toString()};
|
|
322
393
|
var movementRungFor = ${movementRungFor.toString()};
|
|
@@ -373,6 +444,29 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
373
444
|
var tickRungs = {};
|
|
374
445
|
var cameraState = { mode: "overhead", selectedId: null };
|
|
375
446
|
var cameraTween = null, lookAtTween = null;
|
|
447
|
+
// Where the rig last wanted the camera pointed. Held past the end of its own
|
|
448
|
+
// tween because the visitor's look is re-applied every frame, not only while
|
|
449
|
+
// the tween runs.
|
|
450
|
+
var lookTarget = null;
|
|
451
|
+
// Whether setCamera has rigged the camera at least once. Without it the very
|
|
452
|
+
// first call — boot's own overhead call, which changes nothing — would read
|
|
453
|
+
// as "no change" and leave the camera at its construction position.
|
|
454
|
+
var cameraRigged = false;
|
|
455
|
+
// Whether the visitor has turned or zoomed the overhead view themselves.
|
|
456
|
+
// OrbitControls writes straight to the camera, so a resize cannot tell an
|
|
457
|
+
// orbited view from a rigged one by reading the camera back.
|
|
458
|
+
var overheadOrbited = false;
|
|
459
|
+
// The visitor's own look, turned by dragging the canvas and applied on top
|
|
460
|
+
// of whatever rig the camera mode computes. It survives every tick: a tick
|
|
461
|
+
// re-issues setCamera with the same mode and the same agent, so clearing it
|
|
462
|
+
// there would snap the view back a fifth of a second after every drag.
|
|
463
|
+
var lookOffset = { yaw: 0, pitch: 0 };
|
|
464
|
+
var LOOK_RADIANS_PER_PIXEL = 0.005;
|
|
465
|
+
// How far a pointer may travel and still count as a click on a cell rather
|
|
466
|
+
// than a drag. Without it every look-around also walked the followed agent
|
|
467
|
+
// to whatever cell the drag started over.
|
|
468
|
+
var CLICK_SLOP_PX = 6;
|
|
469
|
+
var drag = null;
|
|
376
470
|
var lastFrameTs = null;
|
|
377
471
|
var booted = false;
|
|
378
472
|
|
|
@@ -409,10 +503,31 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
409
503
|
return true;
|
|
410
504
|
}
|
|
411
505
|
|
|
506
|
+
// Equirectangular, not a flat screen-space backdrop: three maps this onto a
|
|
507
|
+
// sphere around the camera, so the horizon stays put as the visitor turns
|
|
508
|
+
// instead of sliding with the view. Eight pixels wide is enough for a
|
|
509
|
+
// gradient with no horizontal variation, and wraps with no seam.
|
|
510
|
+
function buildSkyTexture() {
|
|
511
|
+
var swatch = document.createElement("canvas");
|
|
512
|
+
swatch.width = 8;
|
|
513
|
+
swatch.height = 256;
|
|
514
|
+
var ctx = swatch.getContext("2d");
|
|
515
|
+
var gradient = ctx.createLinearGradient(0, 0, 0, swatch.height);
|
|
516
|
+
var stops = skyGradientStops();
|
|
517
|
+
for (var i = 0; i < stops.length; i += 1) gradient.addColorStop(stops[i].at, stops[i].color);
|
|
518
|
+
ctx.fillStyle = gradient;
|
|
519
|
+
ctx.fillRect(0, 0, swatch.width, swatch.height);
|
|
520
|
+
var texture = new THREE.CanvasTexture(swatch);
|
|
521
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
522
|
+
if (THREE.SRGBColorSpace) texture.colorSpace = THREE.SRGBColorSpace;
|
|
523
|
+
texture.needsUpdate = true;
|
|
524
|
+
return texture;
|
|
525
|
+
}
|
|
526
|
+
|
|
412
527
|
function setUpScene() {
|
|
413
528
|
scene = new THREE.Scene();
|
|
414
|
-
scene.background =
|
|
415
|
-
camera3 = new THREE.PerspectiveCamera(
|
|
529
|
+
scene.background = buildSkyTexture();
|
|
530
|
+
camera3 = new THREE.PerspectiveCamera(${CAMERA_FIELD_OF_VIEW_DEGREES}, canvasAspect(), 0.1, 500);
|
|
416
531
|
camera3.position.set(0, 8, 8);
|
|
417
532
|
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
|
|
418
533
|
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.2));
|
|
@@ -428,6 +543,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
428
543
|
orbitControls.target.set(0, 0, 0);
|
|
429
544
|
|
|
430
545
|
canvas.addEventListener("pointerdown", onPointerDown);
|
|
546
|
+
canvas.addEventListener("wheel", function () {
|
|
547
|
+
if (cameraState.mode === "overhead") overheadOrbited = true;
|
|
548
|
+
}, { passive: true });
|
|
549
|
+
// On the window, not the canvas: a look-around that runs off the edge of
|
|
550
|
+
// the 3D stage must keep turning, and must still end when the button comes
|
|
551
|
+
// up over the deck.
|
|
552
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
553
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
554
|
+
window.addEventListener("pointercancel", onPointerCancel);
|
|
431
555
|
window.addEventListener("resize", onResize);
|
|
432
556
|
watchFoodPill();
|
|
433
557
|
onResize();
|
|
@@ -450,12 +574,35 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
450
574
|
scene.add(grid);
|
|
451
575
|
}
|
|
452
576
|
|
|
577
|
+
function canvasAspect() {
|
|
578
|
+
return (canvas.clientWidth || 640) / Math.max(1, canvas.clientHeight || 360);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// What the overhead rig has to fit the board into. Read off the LIVE camera
|
|
582
|
+
// so one field of view drives both the projection and the distance chosen to
|
|
583
|
+
// suit it.
|
|
584
|
+
function cameraView() {
|
|
585
|
+
return { aspect: canvasAspect(), fovDegrees: camera3 ? camera3.fov : ${CAMERA_FIELD_OF_VIEW_DEGREES} };
|
|
586
|
+
}
|
|
587
|
+
|
|
453
588
|
function onResize() {
|
|
454
589
|
if (!renderer || !camera3) return;
|
|
455
590
|
var w = canvas.clientWidth || 640, h = canvas.clientHeight || 360;
|
|
456
591
|
renderer.setSize(w, h, false);
|
|
457
|
-
camera3.aspect =
|
|
592
|
+
camera3.aspect = canvasAspect();
|
|
458
593
|
camera3.updateProjectionMatrix();
|
|
594
|
+
refitOverheadCamera();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// A new canvas shape fits a different amount of board, so the overhead rig is
|
|
598
|
+
// recomputed for it — a phone turned on its side otherwise keeps the height
|
|
599
|
+
// the old shape earned and crops the board's edges. A visitor who has already
|
|
600
|
+
// orbited keeps the view they made: pulling it back to the board's centre
|
|
601
|
+
// would undo their own gesture.
|
|
602
|
+
function refitOverheadCamera() {
|
|
603
|
+
if (!cameraRigged || cameraState.mode !== "overhead" || overheadOrbited) return;
|
|
604
|
+
var rig = cameraRigFor("overhead", null, GRID_SIZE, cameraView());
|
|
605
|
+
if (rig) tweenCameraToRig(rig, performance.now());
|
|
459
606
|
}
|
|
460
607
|
|
|
461
608
|
function pointFromEvent(evt) {
|
|
@@ -466,11 +613,46 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
466
613
|
};
|
|
467
614
|
}
|
|
468
615
|
|
|
616
|
+
// A press starts a drag rather than resolving a cell. Which of the two it
|
|
617
|
+
// turns out to be is only known when the button comes up, and until then
|
|
618
|
+
// pointermove turns the camera.
|
|
619
|
+
function onPointerDown(evt) {
|
|
620
|
+
if (!scene || !camera3 || !groundMesh) return;
|
|
621
|
+
drag = { pointerId: evt.pointerId, startX: evt.clientX, startY: evt.clientY, lastX: evt.clientX, lastY: evt.clientY, travelled: 0 };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function onPointerMove(evt) {
|
|
625
|
+
if (!drag || (drag.pointerId != null && evt.pointerId !== drag.pointerId)) return;
|
|
626
|
+
var dx = evt.clientX - drag.lastX;
|
|
627
|
+
var dy = evt.clientY - drag.lastY;
|
|
628
|
+
drag.lastX = evt.clientX;
|
|
629
|
+
drag.lastY = evt.clientY;
|
|
630
|
+
drag.travelled += Math.abs(dx) + Math.abs(dy);
|
|
631
|
+
// Overhead is OrbitControls' own drag, and two things turning one camera
|
|
632
|
+
// fight each other.
|
|
633
|
+
if (cameraState.mode === "overhead") { overheadOrbited = true; return; }
|
|
634
|
+
lookOffset.yaw -= dx * LOOK_RADIANS_PER_PIXEL;
|
|
635
|
+
lookOffset.pitch -= dy * LOOK_RADIANS_PER_PIXEL;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function onPointerUp(evt) {
|
|
639
|
+
if (!drag || (drag.pointerId != null && evt.pointerId !== drag.pointerId)) return;
|
|
640
|
+
var straightLine = Math.abs(evt.clientX - drag.startX) + Math.abs(evt.clientY - drag.startY);
|
|
641
|
+
var wasDrag = drag.travelled > CLICK_SLOP_PX || straightLine > CLICK_SLOP_PX;
|
|
642
|
+
drag = null;
|
|
643
|
+
if (wasDrag) return;
|
|
644
|
+
resolveCellClick(evt);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function onPointerCancel() { drag = null; }
|
|
648
|
+
|
|
469
649
|
// The raycast target is the ground mesh ALONE — never scene.children — so
|
|
470
650
|
// a click a hair off a prop or an agent still resolves to the cell under
|
|
471
651
|
// the cursor rather than hitting whatever mesh happens to sit in front.
|
|
472
|
-
function
|
|
652
|
+
function resolveCellClick(evt) {
|
|
473
653
|
if (!scene || !camera3 || !groundMesh) return;
|
|
654
|
+
var rect = canvas.getBoundingClientRect();
|
|
655
|
+
if (evt.clientX < rect.left || evt.clientX > rect.right || evt.clientY < rect.top || evt.clientY > rect.bottom) return;
|
|
474
656
|
var ndc = pointFromEvent(evt);
|
|
475
657
|
raycaster.setFromCamera(ndc, camera3);
|
|
476
658
|
var hits = raycaster.intersectObject(groundMesh, false);
|
|
@@ -877,15 +1059,40 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
877
1059
|
return entry ? { cell: entry.cell, facing: entry.facing } : null;
|
|
878
1060
|
}
|
|
879
1061
|
|
|
1062
|
+
// Both tweens start from where the camera ACTUALLY is, never from the old
|
|
1063
|
+
// tween's own endpoint: the visitor may have dragged or orbited it somewhere
|
|
1064
|
+
// else since, and a tween seeded off the rig alone teleports past that.
|
|
1065
|
+
function tweenCameraToRig(rig, now) {
|
|
1066
|
+
var from = camera3
|
|
1067
|
+
? { x: camera3.position.x, y: camera3.position.y, z: camera3.position.z }
|
|
1068
|
+
: rig.position;
|
|
1069
|
+
cameraTween = startTween(from, rig.position, now, tweenDurationMs);
|
|
1070
|
+
lookAtTween = startTween(lookTarget || rig.lookAt, rig.lookAt, now, tweenDurationMs);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Re-rigs the camera ONLY when the mode or the followed agent actually
|
|
1074
|
+
// changed. Every tick calls this with the state it already had, and re-rigging
|
|
1075
|
+
// on those calls is what snapped a dragged view back a fifth of a second
|
|
1076
|
+
// after every drag, and what undid an overhead orbit a frame after it was
|
|
1077
|
+
// made. Tracking the agent as it moves is refreshCameraTween's job below,
|
|
1078
|
+
// which leaves the visitor's own look alone.
|
|
880
1079
|
function setCamera(state) {
|
|
881
|
-
|
|
1080
|
+
var next = { mode: (state && state.mode) || "overhead", selectedId: (state && state.selectedId) || null };
|
|
1081
|
+
var changed = !cameraRigged || next.mode !== cameraState.mode || next.selectedId !== cameraState.selectedId;
|
|
1082
|
+
cameraState = next;
|
|
1083
|
+
if (orbitControls) orbitControls.enabled = cameraState.mode === "overhead";
|
|
1084
|
+
if (!changed) return;
|
|
1085
|
+
// A new mode or a new agent is a request for a fresh view, so the drag's
|
|
1086
|
+
// own turn goes with the old one.
|
|
1087
|
+
lookOffset.yaw = 0;
|
|
1088
|
+
lookOffset.pitch = 0;
|
|
1089
|
+
overheadOrbited = false;
|
|
882
1090
|
var agent = cameraState.selectedId ? agentSnapshotFor(cameraState.selectedId) : null;
|
|
883
|
-
var
|
|
1091
|
+
var view = cameraView();
|
|
1092
|
+
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE, view) || cameraRigFor("overhead", null, GRID_SIZE, view);
|
|
884
1093
|
if (!rig) return;
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
lookAtTween = reseedTween(lookAtTween, rig.lookAt, now, tweenDurationMs);
|
|
888
|
-
if (orbitControls) orbitControls.enabled = cameraState.mode === "overhead";
|
|
1094
|
+
cameraRigged = true;
|
|
1095
|
+
tweenCameraToRig(rig, performance.now());
|
|
889
1096
|
}
|
|
890
1097
|
|
|
891
1098
|
// Re-tween the camera toward its own rig after a tick lands, in case the
|
|
@@ -895,10 +1102,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
895
1102
|
if (cameraState.mode === "overhead") return;
|
|
896
1103
|
if (!cameraState.selectedId) return;
|
|
897
1104
|
var agent = agentSnapshotFor(cameraState.selectedId);
|
|
898
|
-
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE);
|
|
1105
|
+
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE, cameraView());
|
|
899
1106
|
if (!rig) return;
|
|
900
|
-
|
|
901
|
-
lookAtTween = reseedTween(lookAtTween, rig.lookAt, now, tweenDurationMs);
|
|
1107
|
+
tweenCameraToRig(rig, now);
|
|
902
1108
|
}
|
|
903
1109
|
|
|
904
1110
|
// ---- boot / tick / render loop --------------------------------------------
|
|
@@ -928,6 +1134,9 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
928
1134
|
removalLog = [];
|
|
929
1135
|
manifestByKind = buildManifestByKind(input && input.assetManifest);
|
|
930
1136
|
await placeProps((input && input.propPlacements) || []);
|
|
1137
|
+
cameraRigged = false;
|
|
1138
|
+
lookTarget = null;
|
|
1139
|
+
drag = null;
|
|
931
1140
|
setCamera({ mode: "overhead", selectedId: null });
|
|
932
1141
|
booted = true;
|
|
933
1142
|
}
|
|
@@ -973,8 +1182,29 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
973
1182
|
if (flashLeft <= 0) clearFlash();
|
|
974
1183
|
else flashMesh.material.opacity = 0.75 * (flashLeft / FLASH_MS);
|
|
975
1184
|
}
|
|
976
|
-
|
|
977
|
-
|
|
1185
|
+
// A finished tween is dropped rather than re-applied every frame. Left in
|
|
1186
|
+
// place it pinned the camera to its rig point on every single frame, which
|
|
1187
|
+
// is what stopped an overhead orbit from surviving the frame it was made in.
|
|
1188
|
+
if (cameraTween) {
|
|
1189
|
+
var cp = tweenStep(cameraTween, ts);
|
|
1190
|
+
camera3.position.set(cp.x, cp.y, cp.z);
|
|
1191
|
+
if (cp.done) cameraTween = null;
|
|
1192
|
+
}
|
|
1193
|
+
if (lookAtTween) {
|
|
1194
|
+
var lp = tweenStep(lookAtTween, ts);
|
|
1195
|
+
lookTarget = { x: lp.x, y: lp.y, z: lp.z };
|
|
1196
|
+
if (lp.done) lookAtTween = null;
|
|
1197
|
+
}
|
|
1198
|
+
// Follow and pov aim every frame, tween or no tween, because the visitor's
|
|
1199
|
+
// own drag has to keep showing between ticks. Overhead hands the camera to
|
|
1200
|
+
// OrbitControls once its own tween is spent.
|
|
1201
|
+
if (lookTarget && cameraState.mode !== "overhead") {
|
|
1202
|
+
var aimed = lookTargetWithOffset(camera3.position, lookTarget, lookOffset);
|
|
1203
|
+
lookOffset.pitch = aimed.pitchOffset;
|
|
1204
|
+
camera3.lookAt(aimed.x, aimed.y, aimed.z);
|
|
1205
|
+
} else if (lookTarget && lookAtTween) {
|
|
1206
|
+
camera3.lookAt(lookTarget.x, lookTarget.y, lookTarget.z);
|
|
1207
|
+
}
|
|
978
1208
|
if (orbitControls && orbitControls.enabled) orbitControls.update();
|
|
979
1209
|
renderer.render(scene, camera3);
|
|
980
1210
|
}
|
|
@@ -1028,6 +1258,23 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
1028
1258
|
for (var i = 0; i < removalLog.length; i += 1) if (removalLog[i].id === id) out.push(removalLog[i]);
|
|
1029
1259
|
return out;
|
|
1030
1260
|
},
|
|
1261
|
+
// How much of the canvas the ground plane actually covers, width and
|
|
1262
|
+
// height, each 0..1 — an e2e assertion's read, so a framing check measures
|
|
1263
|
+
// the projection the visitor sees rather than re-deriving the rig from the
|
|
1264
|
+
// numbers that built it. The four corners are projected through the live
|
|
1265
|
+
// camera, so a cropped board reads over 1 on the axis it runs off.
|
|
1266
|
+
boardFrameFraction: function () {
|
|
1267
|
+
if (!camera3 || !groundMesh) return null;
|
|
1268
|
+
var half = (GRID_SIZE * CELL_SIZE) / 2;
|
|
1269
|
+
var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1270
|
+
var corners = [[-half, -half], [-half, half], [half, -half], [half, half]];
|
|
1271
|
+
for (var i = 0; i < corners.length; i += 1) {
|
|
1272
|
+
var ndc = new THREE.Vector3(corners[i][0], 0, corners[i][1]).project(camera3);
|
|
1273
|
+
minX = Math.min(minX, ndc.x); maxX = Math.max(maxX, ndc.x);
|
|
1274
|
+
minY = Math.min(minY, ndc.y); maxY = Math.max(maxY, ndc.y);
|
|
1275
|
+
}
|
|
1276
|
+
return { width: (maxX - minX) / 2, height: (maxY - minY) / 2 };
|
|
1277
|
+
},
|
|
1031
1278
|
ready: function () { return booted; },
|
|
1032
1279
|
};
|
|
1033
1280
|
})();`;
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// command, the addressed teach-frame (extended to the food channel spider-fly
|
|
4
4
|
// has no equivalent of), the bare "tick" command, the player's own food-
|
|
5
5
|
// placement verb, and the belief and orientation asides. The fifth lane on
|
|
6
|
-
// the shared plan slot, shaped exactly like spider-fly-turn.mjs
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// the shared plan slot, shaped exactly like spider-fly-turn.mjs, with
|
|
7
|
+
// vocabulary from predator-prey.mjs's own MUDIII_ROLES: fox hunts goblin,
|
|
8
|
+
// goblins forage crumbs and morsels.
|
|
9
9
|
//
|
|
10
10
|
// This module never plans a move or runs the ecology pass itself — every bit
|
|
11
11
|
// of game logic (fold, pathfinding, belief, ecology, food placement) lives in
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
MUDIII_ROLES, foldTownSquareState, startTownSquareGame, runTownSquareTick,
|
|
22
22
|
placeFood, roleOfId, beliefSnapshotFor,
|
|
23
23
|
} from "./predator-prey.mjs";
|
|
24
|
-
import { snapshotSubject } from "./adventure.mjs";
|
|
24
|
+
import { snapshotSubject, parseSnapshotSubject } from "./adventure.mjs";
|
|
25
25
|
import { correctMisspellings, QUESTION_LEAD_RE } from "../domain/interpret/normalize.mjs";
|
|
26
26
|
import { worldProvenanceTag } from "../domain/worlds-pack.mjs";
|
|
27
27
|
import { getWorldsPackProvider } from "../adapters/corpus/worlds-pack.mjs";
|
|
@@ -424,6 +424,63 @@ export function believedFactSentence(id, believedCell) {
|
|
|
424
424
|
return believedCell ? `${id} is at ${believedCell}.` : `${id} has not been observed.`;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
// The mark both visitor-driven write paths stamp on a placement row: the teach
|
|
428
|
+
// lane's `world:<name>:taught:turnK`, and placeFood's own, which builds the
|
|
429
|
+
// same tag. The board's own ticks write the bare world tag with no such mark.
|
|
430
|
+
const TAUGHT_PROVENANCE_MARK = ":taught:";
|
|
431
|
+
|
|
432
|
+
/** Every subject standing where a VISITOR put it — taught into place by a
|
|
433
|
+
* sentence, or dropped there by the food verb. Both write the same
|
|
434
|
+
* `:taught:turnK` provenance, and the board's own ticks write none of it.
|
|
435
|
+
*
|
|
436
|
+
* Reads the row that WON, ranked the way foldTownSquareState ranks a
|
|
437
|
+
* placement, so a crumb a visitor moved and the board has since moved on from
|
|
438
|
+
* no longer counts. `epoch` is the fold's own, which unstamped rows (a
|
|
439
|
+
* hand-built fixture, a seed row) are ranked at. Pure. */
|
|
440
|
+
export function taughtPlacementSubjects(factRows, epoch = 0) {
|
|
441
|
+
const winner = new Map(); // subject -> { epoch, turn, taught }
|
|
442
|
+
for (const row of factRows || []) {
|
|
443
|
+
if (row.predicate !== PLACEMENT_PREDICATE) continue;
|
|
444
|
+
const snap = parseSnapshotSubject(row.subject);
|
|
445
|
+
const base = snap ? snap.base : row.subject;
|
|
446
|
+
const rowEpoch = snap ? snap.epoch : epoch;
|
|
447
|
+
const turn = snap ? snap.turn : 0;
|
|
448
|
+
const prior = winner.get(base);
|
|
449
|
+
if (prior && !(rowEpoch > prior.epoch || (rowEpoch === prior.epoch && turn >= prior.turn))) continue;
|
|
450
|
+
winner.set(base, { epoch: rowEpoch, turn, taught: String(row.provenance || "").includes(TAUGHT_PROVENANCE_MARK) });
|
|
451
|
+
}
|
|
452
|
+
const taught = new Set();
|
|
453
|
+
for (const [subject, entry] of winner) if (entry.taught) taught.add(subject);
|
|
454
|
+
return taught;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** One observer's whole belief snapshot as a sentence.
|
|
458
|
+
*
|
|
459
|
+
* Anything the observer can place is named: seen, or told. So is anything the
|
|
460
|
+
* visitor put where it stands, seen or not, because the teach lane exists so
|
|
461
|
+
* a claim about ONE individual lands where a visitor can watch it, and an
|
|
462
|
+
* individual that dissolved into a count would take that with it.
|
|
463
|
+
*
|
|
464
|
+
* Everything else unobserved is counted rather than listed. A square at its
|
|
465
|
+
* food cap otherwise answers a question about what a goblin can see with a
|
|
466
|
+
* row of crumbs it cannot, and the list grows with the cap.
|
|
467
|
+
*
|
|
468
|
+
* `beliefEntries` is `Object.entries(beliefSnapshotFor(...))` and
|
|
469
|
+
* `taughtSubjects` a Set from taughtPlacementSubjects. Pure. */
|
|
470
|
+
export function beliefLineFor(observerId, beliefEntries, taughtSubjects) {
|
|
471
|
+
const taught = taughtSubjects || new Set();
|
|
472
|
+
const named = [];
|
|
473
|
+
let unobserved = 0;
|
|
474
|
+
for (const [id, cell] of beliefEntries || []) {
|
|
475
|
+
if (cell || taught.has(id)) named.push(believedFactSentence(id, cell));
|
|
476
|
+
else unobserved += 1;
|
|
477
|
+
}
|
|
478
|
+
if (!named.length && !unobserved) return `${observerId} is alone on the board — nothing else to see.`;
|
|
479
|
+
const rest = unobserved === 1 ? "1 other has not been observed." : `${unobserved} others have not been observed.`;
|
|
480
|
+
if (!named.length) return `${observerId} sees nothing yet. ${rest}`;
|
|
481
|
+
return `${observerId} sees: ${named.join(" ")}${unobserved ? ` ${rest}` : ""}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
427
484
|
/** "what does the goblin see?" / "what does the fox see?" rendered as plain
|
|
428
485
|
* text: the same beliefSnapshotFor read predator-prey.mjs's own tick loop
|
|
429
486
|
* uses, over the CURRENT board state — read-only, no tick runs. Candidates
|
|
@@ -431,7 +488,9 @@ export function believedFactSentence(id, believedCell) {
|
|
|
431
488
|
* goblin's own forage target), exactly the beliefCandidates set
|
|
432
489
|
* runTownSquareTick itself builds. toldFacts is empty — a told position only
|
|
433
490
|
* ever arrives fresh alongside a tick, so there is none standing between
|
|
434
|
-
* ticks to read back here.
|
|
491
|
+
* ticks to read back here. beliefLineFor turns the snapshot into the
|
|
492
|
+
* sentence, naming what the observer can place and what a visitor put where
|
|
493
|
+
* it stands, and counting the rest. */
|
|
435
494
|
async function mudiiiBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GAME_CONFIG }) {
|
|
436
495
|
const kind = match[1].toLowerCase();
|
|
437
496
|
const num = match[2];
|
|
@@ -451,10 +510,7 @@ async function mudiiiBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GAME_
|
|
|
451
510
|
? gameConfig?.mudiii?.predatorVisionRadius
|
|
452
511
|
: gameConfig?.mudiii?.preyVisionRadius;
|
|
453
512
|
const belief = beliefSnapshotFor(observerId, observerCell, candidateIds, state, { visionRadius });
|
|
454
|
-
const
|
|
455
|
-
const text = entries.length
|
|
456
|
-
? `${observerId} sees: ${entries.map(([id, cell]) => believedFactSentence(id, cell)).join(" ")}`
|
|
457
|
-
: `${observerId} is alone on the board — nothing else to see.`;
|
|
513
|
+
const text = beliefLineFor(observerId, Object.entries(belief), taughtPlacementSubjects(rows, state.epoch));
|
|
458
514
|
return {
|
|
459
515
|
text,
|
|
460
516
|
lane: "game-inform",
|