@polycode-projects/the-mechanical-code-talker 5.0.2 → 5.0.4
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/corpus/worlds/manifest.json +9 -9
- package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
- package/corpus/worlds/src/town-square-chapel.jsonl +1 -1
- package/corpus/worlds/src/town-square-market.jsonl +1 -1
- package/corpus/worlds/src/town-square.jsonl +1 -1
- package/data/mudiii-assets.json +35 -5
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +236 -18
- package/src/domain/ask-vocab.mjs +1 -1
- package/src/domain/ask.mjs +20 -10
- package/src/domain/game-config.mjs +10 -1
- package/src/domain/interpret/normalize.mjs +4 -0
- package/src/domain/memory/retraction.mjs +232 -0
- package/src/domain/p2p/sync-filter.mjs +9 -1
- package/src/domain/spider-fly-world.mjs +80 -35
- package/src/domain/syllogise.mjs +128 -75
- package/src/services/adventure-autoplay.mjs +2 -2
- package/src/services/adventure-viz.mjs +12 -7
- package/src/services/chat.mjs +42 -14
- package/src/services/mud-turn.mjs +1 -1
- package/src/services/mud-viz.mjs +11 -2
- package/src/services/mudiii-scene.mjs +199 -55
- package/src/services/mudiii-turn.mjs +82 -1
- package/src/services/mudiii-viz.mjs +17 -7
- package/src/services/p2p-room.mjs +130 -9
- package/src/services/predator-prey.mjs +524 -69
- package/src/services/spider-fly-turn.mjs +128 -56
- package/src/services/spider-fly-viz.mjs +65 -71
- package/src/services/world-teach.mjs +3 -3
- package/src/surfaces/web/adventure-browser-entry.mjs +12 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +118 -118
- package/src/surfaces/web/mud-browser-entry.mjs +10 -3
- package/src/surfaces/web/mudiii-browser-entry.mjs +3 -3
- package/src/surfaces/web/spider-fly-browser-entry.mjs +26 -22
- package/src/services/spider-fly.mjs +0 -943
|
@@ -130,16 +130,14 @@ export function chebyshevDistanceBetweenCells(a, b) {
|
|
|
130
130
|
return Math.max(Math.abs(Number(ma[1]) - Number(mb[1])), Math.abs(Number(ma[2]) - Number(mb[2])));
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** A facing word to a Y rotation in radians
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* world-facing convention, not a guarantee about any one GLB's neutral
|
|
140
|
-
* pose. Pure. */
|
|
133
|
+
/** A facing word to a Y rotation in radians. Assumes a model's own local
|
|
134
|
+
* forward is +Z (true for fox and goblin, the two rigs in the current
|
|
135
|
+
* manifest, checked by walking each GLB's own bind-pose bone chain) so a
|
|
136
|
+
* rotated mesh matches `cameraRigFor`'s own FACING_VECTOR table exactly for
|
|
137
|
+
* all four cardinals — a model with a -Z neutral pose would need its own
|
|
138
|
+
* offset. Pure. */
|
|
141
139
|
export function yawForFacing(facing) {
|
|
142
|
-
const YAW = { south: 0, north: Math.PI, east:
|
|
140
|
+
const YAW = { south: 0, north: Math.PI, east: Math.PI / 2, west: -Math.PI / 2 };
|
|
143
141
|
return YAW[facing] ?? YAW.south;
|
|
144
142
|
}
|
|
145
143
|
|
|
@@ -188,12 +186,14 @@ export function currentActionFor(agentId, agentsById, moving) {
|
|
|
188
186
|
* so acting on the ecology row too would double it). `kind` is one of
|
|
189
187
|
* "death" (the rig's own Death clip, then scale to zero) or "consume" (an
|
|
190
188
|
* eaten item scales to zero, no clip — items are primitive geometry).
|
|
191
|
-
*
|
|
189
|
+
* `eat-agent` alone also carries `actorId`, the predator that gets a
|
|
190
|
+
* one-shot attack clip while its prey plays death — the one event where a
|
|
191
|
+
* second, surviving agent has its own flourish to play. Pure. */
|
|
192
192
|
export function flourishForEcologyEvent(event) {
|
|
193
193
|
if (!event || !event.type) return null;
|
|
194
194
|
switch (event.type) {
|
|
195
195
|
case "eat-agent":
|
|
196
|
-
return { kind: "death", targetId: event.prey, cell: event.cell };
|
|
196
|
+
return { kind: "death", targetId: event.prey, cell: event.cell, actorId: event.predator };
|
|
197
197
|
case "starve":
|
|
198
198
|
return { kind: "death", targetId: event.agent, cell: event.cell };
|
|
199
199
|
case "eat-item":
|
|
@@ -352,7 +352,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
352
352
|
|
|
353
353
|
var loadThreeVendor = createThreeVendorLoader();
|
|
354
354
|
var THREE = null, GLTFLoaderCtor = null, OrbitControlsCtor = null, MeshoptDecoderRef = null;
|
|
355
|
-
var gltfLoader = null, loadQueue = createConcurrencyQueue(4)
|
|
355
|
+
var gltfLoader = null, loadQueue = createConcurrencyQueue(4);
|
|
356
356
|
var scene = null, camera3 = null, renderer = null, orbitControls = null, groundMesh = null, raycaster = null;
|
|
357
357
|
var agentGroups = {};
|
|
358
358
|
var itemMeshes = {};
|
|
@@ -361,21 +361,37 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
361
361
|
var manifestByKind = {};
|
|
362
362
|
var lastAgentsById = {};
|
|
363
363
|
var lastItemsById = {};
|
|
364
|
+
// Every removeAgent/removeItem call, kept small and reset on boot — an
|
|
365
|
+
// e2e assertion's read, so it can tell an eaten agent/item's flourish
|
|
366
|
+
// removal (source "ecology") apart from applyTick's own diff no-op
|
|
367
|
+
// (source "diff") without guessing from a vanished mesh alone.
|
|
368
|
+
var removalLog = [];
|
|
364
369
|
var cameraState = { mode: "overhead", selectedId: null };
|
|
365
370
|
var cameraTween = null, lookAtTween = null;
|
|
366
371
|
var lastFrameTs = null;
|
|
367
372
|
var booted = false;
|
|
368
373
|
|
|
374
|
+
// Cache the fetched BYTES, not the parsed scene: one network request per
|
|
375
|
+
// model URL, however many agents share that kind, then a GLTFLoader.parse
|
|
376
|
+
// per caller off the shared ArrayBuffer. Each caller still ends up owning
|
|
377
|
+
// its own object graph, so normalizeToHeight's own guards still hold.
|
|
378
|
+
function fetchGlbBytes(url) {
|
|
379
|
+
return fetch(url).then(function (res) {
|
|
380
|
+
if (!res.ok) throw new Error("model fetch failed with status " + res.status + ": " + url);
|
|
381
|
+
return res.arrayBuffer();
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
var glbBytesCache = createCachedLoader(fetchGlbBytes);
|
|
385
|
+
|
|
369
386
|
function loadGlbRaw(url) {
|
|
370
|
-
return
|
|
371
|
-
return
|
|
372
|
-
|
|
387
|
+
return glbBytesCache.load(url).then(function (buffer) {
|
|
388
|
+
return loadQueue.run(function () {
|
|
389
|
+
return new Promise(function (resolve, reject) {
|
|
390
|
+
gltfLoader.parse(buffer, THREE.LoaderUtils.extractUrlBase(url), resolve, reject);
|
|
391
|
+
});
|
|
373
392
|
});
|
|
374
393
|
});
|
|
375
394
|
}
|
|
376
|
-
var glbCache = null;
|
|
377
|
-
function loadGlb(url) { return glbCache.load(url); }
|
|
378
|
-
|
|
379
395
|
async function ensureThree() {
|
|
380
396
|
if (THREE) return true;
|
|
381
397
|
var result = await loadThreeVendor();
|
|
@@ -384,7 +400,6 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
384
400
|
OrbitControlsCtor = result.OrbitControls; MeshoptDecoderRef = result.MeshoptDecoder;
|
|
385
401
|
gltfLoader = new GLTFLoaderCtor();
|
|
386
402
|
gltfLoader.setMeshoptDecoder(MeshoptDecoderRef);
|
|
387
|
-
glbCache = createCachedLoader(loadGlbRaw);
|
|
388
403
|
setUpScene();
|
|
389
404
|
return true;
|
|
390
405
|
}
|
|
@@ -473,24 +488,51 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
473
488
|
}
|
|
474
489
|
|
|
475
490
|
// ---- props ---------------------------------------------------------------
|
|
491
|
+
// Box3.setFromObject reads through a parent's world matrix, so measuring a
|
|
492
|
+
// parented object reads whatever that parent's spawn flourish is doing in
|
|
493
|
+
// that frame and applies a scale that depends on the frame. The guards make
|
|
494
|
+
// both misuses fail loudly rather than render a wrong size silently. Keep
|
|
495
|
+
// no backtick in this block: the function's source sits inside the module's
|
|
496
|
+
// own template literal.
|
|
476
497
|
function normalizeToHeight(object3D, targetHeight) {
|
|
498
|
+
if (object3D.parent) {
|
|
499
|
+
throw new Error("normalizeToHeight refused an object that already has a parent: " + (object3D.name || object3D.uuid));
|
|
500
|
+
}
|
|
501
|
+
if (object3D.userData.tmctNormalized) {
|
|
502
|
+
throw new Error("normalizeToHeight refused a second call on the same object: " + (object3D.name || object3D.uuid));
|
|
503
|
+
}
|
|
477
504
|
var box = new THREE.Box3().setFromObject(object3D);
|
|
478
505
|
var size = new THREE.Vector3();
|
|
479
506
|
box.getSize(size);
|
|
480
|
-
|
|
481
|
-
|
|
507
|
+
if (!size.y) {
|
|
508
|
+
// Substituting 1 here invented a scale from an unmeasurable height, and
|
|
509
|
+
// that is what produced the house-height mesh.
|
|
510
|
+
throw new Error("normalizeToHeight found no measurable height on: " + (object3D.name || object3D.uuid));
|
|
511
|
+
}
|
|
512
|
+
var scale = targetHeight ? targetHeight / size.y : 1;
|
|
482
513
|
object3D.scale.setScalar(scale);
|
|
483
514
|
var seated = new THREE.Box3().setFromObject(object3D);
|
|
484
515
|
object3D.position.y -= seated.min.y;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
516
|
+
object3D.userData.tmctNormalized = true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Keyed on the manifest row's own key field, not its destPath: food-crumb
|
|
520
|
+
// and food-morsel share one GLB at two different targetHeights, and
|
|
521
|
+
// normalizeToHeight refuses a second call on the same object, so a
|
|
522
|
+
// destPath-keyed cache would hand the second row the first row's
|
|
523
|
+
// already-normalized graph and throw.
|
|
524
|
+
function loadPropTemplate(asset) {
|
|
525
|
+
var key = asset.key;
|
|
526
|
+
if (!propTemplates[key]) {
|
|
527
|
+
var url = modelUrlFor(asset.destPath);
|
|
528
|
+
var promise = loadGlbRaw(url).then(function (gltf) {
|
|
529
|
+
normalizeToHeight(gltf.scene, asset.targetHeight);
|
|
530
|
+
return gltf.scene;
|
|
531
|
+
});
|
|
532
|
+
promise.catch(function () { delete propTemplates[key]; });
|
|
533
|
+
propTemplates[key] = promise;
|
|
534
|
+
}
|
|
535
|
+
return propTemplates[key];
|
|
494
536
|
}
|
|
495
537
|
|
|
496
538
|
async function placeProps(propPlacements) {
|
|
@@ -515,11 +557,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
515
557
|
}
|
|
516
558
|
}
|
|
517
559
|
|
|
518
|
-
// ---- items: crumbs and morsels,
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
function
|
|
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. ---------------------------------------------------------
|
|
564
|
+
function itemAssetKeyFor(kind) { return kind === "morsel" ? "food-morsel" : "food-crumb"; }
|
|
523
565
|
|
|
524
566
|
function animateScaleTo(object3D, target, now) {
|
|
525
567
|
var duration = tweenDurationMs;
|
|
@@ -535,29 +577,55 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
535
577
|
requestAnimationFrame(step);
|
|
536
578
|
}
|
|
537
579
|
|
|
580
|
+
// A Group per live item, the same shape ensureAgent uses for agents: the
|
|
581
|
+
// group is in the scene (and in itemMeshes) synchronously so a second tick
|
|
582
|
+
// arriving before the GLB resolves finds an existing entry rather than
|
|
583
|
+
// starting a second load, but stays invisible until the model is in it.
|
|
538
584
|
function applyItemTick(id, item, now) {
|
|
539
585
|
var world = cellToWorld(item.cell, GRID_SIZE, CELL_SIZE);
|
|
540
586
|
if (!world) return;
|
|
541
587
|
var entry = itemMeshes[id];
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
mesh.
|
|
545
|
-
mesh.position.set(world.x, itemHeightFor(item.kind), world.z);
|
|
546
|
-
mesh.scale.setScalar(0);
|
|
547
|
-
scene.add(mesh);
|
|
548
|
-
entry = { mesh: mesh, cell: item.cell, kind: item.kind };
|
|
549
|
-
itemMeshes[id] = entry;
|
|
550
|
-
animateScaleTo(mesh, 1, now);
|
|
588
|
+
if (entry) {
|
|
589
|
+
entry.cell = item.cell;
|
|
590
|
+
entry.mesh.position.set(world.x, 0, world.z);
|
|
551
591
|
return;
|
|
552
592
|
}
|
|
553
|
-
|
|
554
|
-
|
|
593
|
+
var group = new THREE.Group();
|
|
594
|
+
group.name = "item-" + id;
|
|
595
|
+
group.position.set(world.x, 0, world.z);
|
|
596
|
+
group.visible = false;
|
|
597
|
+
scene.add(group);
|
|
598
|
+
entry = { mesh: group, cell: item.cell, kind: item.kind };
|
|
599
|
+
itemMeshes[id] = entry;
|
|
600
|
+
var asset = manifestByKind[itemAssetKeyFor(item.kind)];
|
|
601
|
+
if (!asset) return;
|
|
602
|
+
loadPropTemplate(asset).then(function (template) {
|
|
603
|
+
if (itemMeshes[id] !== entry) return; // removed while the model was still loading
|
|
604
|
+
var instance = template.clone();
|
|
605
|
+
instance.position.y = template.position.y;
|
|
606
|
+
group.add(instance);
|
|
607
|
+
group.visible = true;
|
|
608
|
+
group.scale.setScalar(0);
|
|
609
|
+
animateScaleTo(group, 1, performance.now());
|
|
610
|
+
}).catch(function (err) {
|
|
611
|
+
// eslint-disable-next-line no-console
|
|
612
|
+
console.warn("tmct: an item model failed to load", id, err);
|
|
613
|
+
});
|
|
555
614
|
}
|
|
556
615
|
|
|
557
|
-
function
|
|
616
|
+
function recordRemoval(kind, id, source) {
|
|
617
|
+
removalLog.push({ kind: kind, id: id, source: source });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// source ("ecology" | "diff") is recorded, never branched on — it exists
|
|
621
|
+
// so an e2e assertion can tell an eaten item's flourish removal apart from
|
|
622
|
+
// a plain diff no-op, since a vanished mesh alone does not say which path
|
|
623
|
+
// did the removing.
|
|
624
|
+
function removeItem(id, withFlourish, source) {
|
|
558
625
|
var entry = itemMeshes[id];
|
|
559
626
|
if (!entry) return;
|
|
560
627
|
delete itemMeshes[id];
|
|
628
|
+
recordRemoval("item", id, source);
|
|
561
629
|
if (withFlourish) animateScaleTo(entry.mesh, 0, performance.now());
|
|
562
630
|
else if (entry.mesh.parent) entry.mesh.parent.remove(entry.mesh);
|
|
563
631
|
}
|
|
@@ -572,14 +640,14 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
572
640
|
var kind = roleOfAgentId(id);
|
|
573
641
|
var entry = {
|
|
574
642
|
group: new THREE.Group(), tween: null, cell: null, facing: agent.facing, role: agent.role,
|
|
575
|
-
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null,
|
|
643
|
+
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null, oneShotAction: null,
|
|
576
644
|
};
|
|
577
645
|
entry.group.visible = false;
|
|
578
646
|
scene.add(entry.group);
|
|
579
647
|
agentGroups[id] = entry;
|
|
580
648
|
var asset = manifestByKind[kind];
|
|
581
649
|
if (asset) {
|
|
582
|
-
|
|
650
|
+
loadGlbRaw(modelUrlFor(asset.destPath)).then(function (gltf) {
|
|
583
651
|
normalizeToHeight(gltf.scene, asset.targetHeight);
|
|
584
652
|
entry.group.add(gltf.scene);
|
|
585
653
|
entry.group.visible = true;
|
|
@@ -597,6 +665,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
597
665
|
}
|
|
598
666
|
|
|
599
667
|
function playClip(entry, clipName) {
|
|
668
|
+
// A one-shot flourish left its own action running at full weight (see
|
|
669
|
+
// playClipOnce below — fadeIn ramps the NEW action in but never ramps
|
|
670
|
+
// the old one out on its own, so without this the flourish pose would
|
|
671
|
+
// keep blending into every clip after it, forever). Fade it out before
|
|
672
|
+
// any of the guards below can skip the rest of this call.
|
|
673
|
+
if (entry.oneShotAction) {
|
|
674
|
+
entry.oneShotAction.fadeOut(0.15);
|
|
675
|
+
entry.oneShotAction = null;
|
|
676
|
+
}
|
|
600
677
|
if (!entry.mixer || !clipName || entry.currentClip === clipName) return;
|
|
601
678
|
var next = entry.actions[clipName];
|
|
602
679
|
if (!next) return;
|
|
@@ -606,6 +683,28 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
606
683
|
entry.currentClip = clipName;
|
|
607
684
|
}
|
|
608
685
|
|
|
686
|
+
// A one-shot version of playClip for a flourish (the predator's own bite
|
|
687
|
+
// on eat-agent): LoopOnce + clampWhenFinished so the clip plays through and
|
|
688
|
+
// holds its last pose rather than looping. Never skips a repeat of
|
|
689
|
+
// entry.currentClip the way playClip does — a flourish must always play,
|
|
690
|
+
// even if the actor happened to already be on this same clip. Clears
|
|
691
|
+
// entry.currentClip to null so the next applyAgentTick's own playClip call
|
|
692
|
+
// is never refused as "already on this clip", and records the action on
|
|
693
|
+
// entry.oneShotAction so that same next call fades it back out.
|
|
694
|
+
function playClipOnce(entry, clipName) {
|
|
695
|
+
if (!entry.mixer || !clipName) return;
|
|
696
|
+
var action = entry.actions[clipName];
|
|
697
|
+
if (!action) return;
|
|
698
|
+
var prev = entry.currentClip && entry.currentClip !== clipName ? entry.actions[entry.currentClip] : null;
|
|
699
|
+
action.reset();
|
|
700
|
+
action.setLoop(THREE.LoopOnce, 1);
|
|
701
|
+
action.clampWhenFinished = true;
|
|
702
|
+
action.fadeIn(0.15).play();
|
|
703
|
+
if (prev) prev.fadeOut(0.15);
|
|
704
|
+
entry.oneShotAction = action;
|
|
705
|
+
entry.currentClip = null;
|
|
706
|
+
}
|
|
707
|
+
|
|
609
708
|
function playSpawnFlourish(group, now) {
|
|
610
709
|
group.scale.setScalar(0);
|
|
611
710
|
animateScaleTo(group, 1, now);
|
|
@@ -636,10 +735,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
636
735
|
}
|
|
637
736
|
}
|
|
638
737
|
|
|
639
|
-
function removeAgent(id) {
|
|
738
|
+
function removeAgent(id, source) {
|
|
640
739
|
var entry = agentGroups[id];
|
|
641
740
|
if (!entry) return;
|
|
642
741
|
delete agentGroups[id];
|
|
742
|
+
recordRemoval("agent", id, source);
|
|
643
743
|
var deathClip = entry.clipMap && entry.clipMap.death;
|
|
644
744
|
var deathClipName = Array.isArray(deathClip) ? deathClip[0] : deathClip;
|
|
645
745
|
if (deathClipName) {
|
|
@@ -652,10 +752,17 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
652
752
|
|
|
653
753
|
function applyEcology(ecology) {
|
|
654
754
|
for (var i = 0; i < (ecology || []).length; i += 1) {
|
|
655
|
-
var
|
|
755
|
+
var event = ecology[i];
|
|
756
|
+
var flourish = flourishForEcologyEvent(event);
|
|
656
757
|
if (!flourish) continue;
|
|
657
|
-
if (flourish.kind === "death") removeAgent(flourish.targetId);
|
|
658
|
-
else if (flourish.kind === "consume") removeItem(flourish.targetId, true);
|
|
758
|
+
if (flourish.kind === "death") removeAgent(flourish.targetId, "ecology");
|
|
759
|
+
else if (flourish.kind === "consume") removeItem(flourish.targetId, true, "ecology");
|
|
760
|
+
if (flourish.actorId) {
|
|
761
|
+
var actorEntry = agentGroups[flourish.actorId];
|
|
762
|
+
if (actorEntry && actorEntry.clipMap) {
|
|
763
|
+
playClipOnce(actorEntry, clipForAction(actorEntry.role, event.type, actorEntry.clipMap));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
659
766
|
}
|
|
660
767
|
}
|
|
661
768
|
|
|
@@ -710,6 +817,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
710
817
|
itemMeshes = {};
|
|
711
818
|
lastAgentsById = {};
|
|
712
819
|
lastItemsById = {};
|
|
820
|
+
removalLog = [];
|
|
713
821
|
manifestByKind = buildManifestByKind(input && input.assetManifest);
|
|
714
822
|
await placeProps((input && input.propPlacements) || []);
|
|
715
823
|
setCamera({ mode: "overhead", selectedId: null });
|
|
@@ -723,9 +831,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
723
831
|
var items = (tick && tick.items) || {};
|
|
724
832
|
for (var id in agents) if (Object.prototype.hasOwnProperty.call(agents, id)) applyAgentTick(id, agents[id], now);
|
|
725
833
|
for (var itemId in items) if (Object.prototype.hasOwnProperty.call(items, itemId)) applyItemTick(itemId, items[itemId], now);
|
|
726
|
-
|
|
727
|
-
|
|
834
|
+
// Ecology first: an eaten agent/item is already gone from this tick's own
|
|
835
|
+
// agents/items map, so applyEcology's own removeAgent/removeItem call is
|
|
836
|
+
// what actually removes it (with its flourish). The two diff loops below
|
|
837
|
+
// then find that id already deleted and no-op — removeAgent/removeItem
|
|
838
|
+
// both delete their map entry before anything else, so a second call for
|
|
839
|
+
// the same id returns immediately.
|
|
728
840
|
applyEcology(tick && tick.ecology);
|
|
841
|
+
for (var goneAgent in lastAgentsById) if (!(goneAgent in agents)) removeAgent(goneAgent, "diff");
|
|
842
|
+
for (var goneItem in lastItemsById) if (!(goneItem in items)) removeItem(goneItem, false, "diff");
|
|
729
843
|
lastAgentsById = agents;
|
|
730
844
|
lastItemsById = items;
|
|
731
845
|
refreshCameraTween(now);
|
|
@@ -760,6 +874,36 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
760
874
|
if (itemMeshes[id]) return itemMeshes[id].cell;
|
|
761
875
|
return null;
|
|
762
876
|
},
|
|
877
|
+
// The live agent group's own applied Y rotation, in radians -- an e2e
|
|
878
|
+
// assertion's read, so it can compare the yaw actually applied against
|
|
879
|
+
// the direction an agent travelled between two cells rather than trusting
|
|
880
|
+
// yawForFacing's own output in isolation.
|
|
881
|
+
yawOf: function (id) {
|
|
882
|
+
var entry = agentGroups[id];
|
|
883
|
+
return entry ? entry.group.rotation.y : null;
|
|
884
|
+
},
|
|
885
|
+
// The live agent mesh's world-space height and lowest point, plus the
|
|
886
|
+
// manifest's own targetHeight to compare it against — an e2e assertion's
|
|
887
|
+
// read, so it goes through the group actually in the scene rather than a
|
|
888
|
+
// second, locally invented measurement.
|
|
889
|
+
meshHeightOf: function (id) {
|
|
890
|
+
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);
|
|
893
|
+
var size = new THREE.Vector3();
|
|
894
|
+
box.getSize(size);
|
|
895
|
+
var asset = manifestByKind[entry.kind];
|
|
896
|
+
return { height: size.y, minY: box.min.y, targetHeight: asset ? asset.targetHeight : null };
|
|
897
|
+
},
|
|
898
|
+
// Every removal recorded for id, oldest first — each entry's own
|
|
899
|
+
// source is "ecology" (an eat/starve flourish removed it) or "diff"
|
|
900
|
+
// (applyTick found it missing from a tick's own agents/items map with no
|
|
901
|
+
// ecology event driving that). An e2e assertion's read.
|
|
902
|
+
removalsFor: function (id) {
|
|
903
|
+
var out = [];
|
|
904
|
+
for (var i = 0; i < removalLog.length; i += 1) if (removalLog[i].id === id) out.push(removalLog[i]);
|
|
905
|
+
return out;
|
|
906
|
+
},
|
|
763
907
|
ready: function () { return booted; },
|
|
764
908
|
};
|
|
765
909
|
})();`;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import {
|
|
16
16
|
TOWN_SQUARE_LAYOUTS, DEFAULT_GRID_SIZE, DIRECTION_DELTA,
|
|
17
17
|
cellId, parseCellId, inBounds, chebyshevDistance, oneStepDirectionBetween,
|
|
18
|
-
agentKindOf, liveIdsOfKind, layoutNamed,
|
|
18
|
+
agentKindOf, liveIdsOfKind, layoutNamed, isFoodId,
|
|
19
19
|
} from "../domain/town-square-world.mjs";
|
|
20
20
|
import {
|
|
21
21
|
MUDIII_ROLES, foldTownSquareState, startTownSquareGame, runTownSquareTick,
|
|
@@ -88,6 +88,25 @@ const MUDIII_TOLD_RE = new RegExp(
|
|
|
88
88
|
// never ground truth.
|
|
89
89
|
const MUDIII_SEE_RE = /^what (?:does|can) the (fox|goblin)(?:-(\d+))?\s+see[.!?\s]*$/i;
|
|
90
90
|
|
|
91
|
+
// "who put/placed that there?" — provenance, ground truth (not belief), read
|
|
92
|
+
// from placeFood's own mgx:placed-by row. The subject must be named, either
|
|
93
|
+
// an item id ("who put morsel-1 there?") or a cell ("who put food at
|
|
94
|
+
// cell-3-4?") — a bare "who put that there?" with no referent at all names
|
|
95
|
+
// nothing to look up and is left to the ordinary lanes. Requires the leading
|
|
96
|
+
// "who put/placed/dropped" verb, so it never collides with an unrelated "who"
|
|
97
|
+
// question (the fox's own catches, the goblins' names, anything not about a
|
|
98
|
+
// placement) — those never start with this verb at all.
|
|
99
|
+
const MUDIII_WHO_PLACED_RE = new RegExp(
|
|
100
|
+
"^who\\s+(?:put|placed|dropped)\\s+"
|
|
101
|
+
+ "(?:"
|
|
102
|
+
+ "(?:the\\s+)?(morsel|crumb)(?:-(\\d+))?"
|
|
103
|
+
+ "|"
|
|
104
|
+
+ "(?:(?:the\\s+)?(?:food|morsel|crumb|that|it)\\s+)?(?:at|on)\\s+(cell-\\d+-\\d+)"
|
|
105
|
+
+ ")"
|
|
106
|
+
+ "(?:\\s+there)?[?.!\\s]*$",
|
|
107
|
+
"i",
|
|
108
|
+
);
|
|
109
|
+
|
|
91
110
|
// The player's own verb: "put food at cell-3-4" (primary, never shadowed —
|
|
92
111
|
// the adventure imperative grammar's own "put" arm hard-requires a literal
|
|
93
112
|
// "in", so "at" never parses there) and "drop a morsel at cell-3-4"
|
|
@@ -441,6 +460,63 @@ async function mudiiiBeliefAnswer(match, { memoryDir, gameConfig = DEFAULT_GAME_
|
|
|
441
460
|
};
|
|
442
461
|
}
|
|
443
462
|
|
|
463
|
+
function noLivePlacedSubjectAnswer(kind) {
|
|
464
|
+
return {
|
|
465
|
+
text: `there's no live ${kind} on the board for that to be about.`,
|
|
466
|
+
lane: "game-inform",
|
|
467
|
+
note: `MUDIII — who-placed declined: no live ${kind} resolves`,
|
|
468
|
+
miss: true,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** "who put/placed <item|cell> there?": ground truth read off the folded
|
|
473
|
+
* state's own placedBy Map (predator-prey.mjs's fold of every mgx:placed-by
|
|
474
|
+
* row), never belief. Resolves the subject the same way the told-fact and
|
|
475
|
+
* see recognizers already do — resolveAgentId for a named item, a direct
|
|
476
|
+
* placements scan for a cell — then declines, naming the reason, when the
|
|
477
|
+
* item isn't on the board at all or is on the board but carries no placedBy
|
|
478
|
+
* row (spawned food never does; placeFood is the only writer of that row). */
|
|
479
|
+
async function mudiiiWhoPlacedAnswer(match, { memoryDir }) {
|
|
480
|
+
const [, kindRaw, num, cellLiteral] = match;
|
|
481
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
482
|
+
const state = foldTownSquareState(rows);
|
|
483
|
+
|
|
484
|
+
let itemId = null;
|
|
485
|
+
if (kindRaw) {
|
|
486
|
+
itemId = resolveAgentId(kindRaw.toLowerCase(), num, state);
|
|
487
|
+
} else {
|
|
488
|
+
const entry = [...state.placements.entries()].find(([id, place]) =>
|
|
489
|
+
isFoodId(id) && !state.removed.has(id) && place.cell === cellLiteral);
|
|
490
|
+
itemId = entry ? entry[0] : null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (!itemId) {
|
|
494
|
+
if (kindRaw) return noLivePlacedSubjectAnswer(kindRaw.toLowerCase());
|
|
495
|
+
return {
|
|
496
|
+
text: `there's no food at ${cellLiteral} to ask about.`,
|
|
497
|
+
lane: "game-inform",
|
|
498
|
+
note: `MUDIII — who-placed declined: no live food item sits at ${cellLiteral}`,
|
|
499
|
+
miss: true,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const placed = state.placedBy.get(itemId);
|
|
504
|
+
if (!placed) {
|
|
505
|
+
return {
|
|
506
|
+
text: `${itemId} was never placed — it spawned on its own.`,
|
|
507
|
+
lane: "game-inform",
|
|
508
|
+
note: `MUDIII — who-placed declined: ${itemId} carries no mgx:placed-by row (spawned food)`,
|
|
509
|
+
miss: true,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
text: `${placed.by} put ${itemId} at ${state.placements.get(itemId).cell}.`,
|
|
515
|
+
lane: "game-inform",
|
|
516
|
+
note: `MUDIII — who-placed answered from state.placedBy for ${itemId}`,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
444
520
|
/** The addressed teach-frame turn: resolve the addressee and the belief
|
|
445
521
|
* subject (an agent OR a food item), resolve the told cell, and run ONE tick
|
|
446
522
|
* with that told-fact fed in. Told-facts are not persisted across turns —
|
|
@@ -659,6 +735,11 @@ export async function mudiiiTurn(line, { planHolder, memoryDir, env, cache = nul
|
|
|
659
735
|
return mudiiiBeliefAnswer(seeMatch, { memoryDir, gameConfig });
|
|
660
736
|
}
|
|
661
737
|
|
|
738
|
+
const whoPlacedMatch = trimmed.match(MUDIII_WHO_PLACED_RE);
|
|
739
|
+
if (whoPlacedMatch) {
|
|
740
|
+
return mudiiiWhoPlacedAnswer(whoPlacedMatch, { memoryDir });
|
|
741
|
+
}
|
|
742
|
+
|
|
662
743
|
if (MUDIII_TICK_RE.test(line)) {
|
|
663
744
|
return runTickAndRender({ planHolder, memoryDir, cache, world: mudiii.world, toldFacts: [], gameConfig });
|
|
664
745
|
}
|
|
@@ -82,7 +82,7 @@ const NPC_COUNT_MIN = 1;
|
|
|
82
82
|
const NPC_COUNT_MAX = 10;
|
|
83
83
|
const NPC_COUNT_LABELLED = [1, 5, 10];
|
|
84
84
|
const DEFAULT_NPC_COUNT = 2;
|
|
85
|
-
const DEFAULT_DELAY_MS =
|
|
85
|
+
const DEFAULT_DELAY_MS = 220;
|
|
86
86
|
const DEFAULT_MAX_TURNS = 400;
|
|
87
87
|
// test/fixtures/mudiii-ticks.json's own board size — the fallback for a
|
|
88
88
|
// scenario that names no gridSize of its own.
|
|
@@ -813,6 +813,7 @@ function pageScript() {
|
|
|
813
813
|
const el = (id) => document.getElementById(id);
|
|
814
814
|
let scenarioIndex = 0;
|
|
815
815
|
const scenario = function () { return DATA.scenarios[scenarioIndex]; };
|
|
816
|
+
const gridSizeOf = function () { return scenario().gridSize || DATA.gridSize; };
|
|
816
817
|
const rosterOf = function (s, role) {
|
|
817
818
|
return (s.agents || []).filter(function (a) { return !role || a.role === role; }).map(function (a) { return a.id; });
|
|
818
819
|
};
|
|
@@ -881,6 +882,9 @@ function pageScript() {
|
|
|
881
882
|
|
|
882
883
|
function applyTickResult(result) {
|
|
883
884
|
if (!result) return;
|
|
885
|
+
// The engine owns the count. Anything that advances a turn — the deck, a
|
|
886
|
+
// chat frame — lands here, so the page never keeps a rival tally.
|
|
887
|
+
if (typeof result.turn === "number") globalTurn = result.turn;
|
|
884
888
|
if (result.agents) agentsById = result.agents;
|
|
885
889
|
if (result.items) itemsById = result.items;
|
|
886
890
|
callScene("applyTick", { agents: result.agents, items: result.items, ecology: result.ecology });
|
|
@@ -892,8 +896,7 @@ function pageScript() {
|
|
|
892
896
|
async function runOneTick() {
|
|
893
897
|
return serializeTick(async function () {
|
|
894
898
|
if (!session) return null;
|
|
895
|
-
|
|
896
|
-
const result = await session.tick(globalTurn);
|
|
899
|
+
const result = await session.tick();
|
|
897
900
|
applyTickResult(result);
|
|
898
901
|
renderAll();
|
|
899
902
|
return result;
|
|
@@ -921,8 +924,16 @@ function pageScript() {
|
|
|
921
924
|
function sendCommand(line) {
|
|
922
925
|
appendChat("u", line);
|
|
923
926
|
if (!session) { appendChat("a", "no session is open yet \\u2014 reset to start one."); return Promise.resolve(); }
|
|
924
|
-
|
|
927
|
+
// A chat line can run a real turn. An addressed told-fact does, and the
|
|
928
|
+
// visitor should watch the lie land, so the board is read back in the same
|
|
929
|
+
// queue slot. board() spends no turn, so a line that ran none costs
|
|
930
|
+
// nothing. It reports no goal or plan either, because a resting board has
|
|
931
|
+
// decided nothing, which is the blank boot() already draws at turn 0.
|
|
932
|
+
return serializeTick(async function () {
|
|
933
|
+
const res = await tmct.turn(line);
|
|
934
|
+
const board = await session.board();
|
|
925
935
|
appendChat("a", res.answer);
|
|
936
|
+
applyTickResult(board);
|
|
926
937
|
renderAll();
|
|
927
938
|
return res;
|
|
928
939
|
});
|
|
@@ -1027,7 +1038,7 @@ function pageScript() {
|
|
|
1027
1038
|
|
|
1028
1039
|
// ---- the top-down map panel ---------------------------------------------
|
|
1029
1040
|
function renderMapPanel() {
|
|
1030
|
-
const dots = mapDotsFor(agentsList(), itemsList(),
|
|
1041
|
+
const dots = mapDotsFor(agentsList(), itemsList(), gridSizeOf());
|
|
1031
1042
|
el("mapPanelBoard").innerHTML = dots.map(function (d) {
|
|
1032
1043
|
return '<span class="map-dot map-dot-' + esc(d.kind) + '" style="left:' + d.xPct + '%;top:' + d.yPct + '%" title="' + esc(d.id) + '"></span>';
|
|
1033
1044
|
}).join("");
|
|
@@ -1216,7 +1227,7 @@ function pageScript() {
|
|
|
1216
1227
|
agentsById = {};
|
|
1217
1228
|
itemsById = {};
|
|
1218
1229
|
el("chatInput").disabled = false;
|
|
1219
|
-
callScene("boot", { propPlacements: props, assetManifest: DATA.assetManifest, gridSize:
|
|
1230
|
+
await callScene("boot", { propPlacements: props, assetManifest: DATA.assetManifest, gridSize: gridSizeOf(), cellSize: 1 });
|
|
1220
1231
|
|
|
1221
1232
|
// The opening board, drawn through the very path a tick takes. Without
|
|
1222
1233
|
// this the page's first sight of where anything stands is the first tick,
|
|
@@ -1225,7 +1236,6 @@ function pageScript() {
|
|
|
1225
1236
|
// the cells both come back from it rather than being guessed here.
|
|
1226
1237
|
const opening = await session.board();
|
|
1227
1238
|
if (seq !== bootSeq) return;
|
|
1228
|
-
globalTurn = opening.turn || 0;
|
|
1229
1239
|
camera.selectedId = Object.keys(opening.agents || {}).sort()[0] || null;
|
|
1230
1240
|
applyTickResult(opening);
|
|
1231
1241
|
renderAll();
|