@polycode-projects/the-mechanical-code-talker 5.0.3 → 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 +11 -6
- package/package.json +1 -1
- package/src/domain/game-config.mjs +3 -1
- package/src/domain/spider-fly-world.mjs +11 -13
- package/src/services/mudiii-scene.mjs +131 -39
- package/src/services/mudiii-turn.mjs +82 -1
- package/src/services/mudiii-viz.mjs +16 -6
- package/src/services/spider-fly-viz.mjs +57 -60
- package/src/surfaces/web/memory-ask-browser.bundle.js +64 -64
- package/src/surfaces/web/mudiii-browser-entry.mjs +3 -3
|
@@ -50,17 +50,17 @@
|
|
|
50
50
|
{
|
|
51
51
|
"file": "src/town-square-chapel.jsonl",
|
|
52
52
|
"bytes": 108408,
|
|
53
|
-
"sha256": "
|
|
53
|
+
"sha256": "02036d284ec27244494e1f575b750efa71d9a0fc232e57dbd8b5dab7e6a6b7b3"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"file": "src/town-square-market.jsonl",
|
|
57
57
|
"bytes": 52311,
|
|
58
|
-
"sha256": "
|
|
58
|
+
"sha256": "dd374f9cb1c6a13ccfab62badeb4150b8cfefdc349f834430c2c9e105c1d7a4f"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"file": "src/town-square.jsonl",
|
|
62
62
|
"bytes": 75251,
|
|
63
|
-
"sha256": "
|
|
63
|
+
"sha256": "2fb87416759d8cee1464b55d4e811b91e772d52f65e0f06a88eac1d3a8fc5e7f"
|
|
64
64
|
}
|
|
65
65
|
],
|
|
66
66
|
"files": [
|
|
@@ -106,18 +106,18 @@
|
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"file": "shards/town-square-chapel.jsonl.gz",
|
|
109
|
-
"bytes":
|
|
110
|
-
"sha256": "
|
|
109
|
+
"bytes": 5538,
|
|
110
|
+
"sha256": "826e8732b419b506098f4fbce1adcc07d11bd7d91ea672aa46f13ed3ccebb0bb"
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"file": "shards/town-square-market.jsonl.gz",
|
|
114
|
-
"bytes":
|
|
115
|
-
"sha256": "
|
|
114
|
+
"bytes": 3113,
|
|
115
|
+
"sha256": "c6834f0754498ff800e7be620e3e2268c375686bd753fa9da9b7dd89a63aa009"
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
"file": "shards/town-square.jsonl.gz",
|
|
119
|
-
"bytes":
|
|
120
|
-
"sha256": "
|
|
119
|
+
"bytes": 4275,
|
|
120
|
+
"sha256": "460864ccee3a6eed9dae93f158abaf9780f2b7fb4051f45c3ebc63c7400b01d2"
|
|
121
121
|
}
|
|
122
122
|
]
|
|
123
123
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -920,7 +920,7 @@
|
|
|
920
920
|
{"world":"town-square-chapel","kind":"fact","subject":"goblin","predicate":"mgx:lose","object":"0.06 mass per turn by default"}
|
|
921
921
|
{"world":"town-square-chapel","kind":"fact","subject":"goblin","predicate":"mgx:arrive","object":"every 5 turns at the edge of the board by default"}
|
|
922
922
|
{"world":"town-square-chapel","kind":"fact","subject":"crumb","predicate":"mgx:arrive","object":"every 3 turns on any open cell by default"}
|
|
923
|
-
{"world":"town-square-chapel","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass
|
|
923
|
+
{"world":"town-square-chapel","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
924
924
|
{"world":"town-square-chapel","kind":"fact","subject":"morsel","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
925
925
|
{"world":"town-square-chapel","kind":"fact","subject":"morsel","predicate":"mgx:arrive","object":"where the player puts it"}
|
|
926
926
|
{"world":"town-square-chapel","kind":"fact","subject":"vision radius","predicate":"mgx:hasProperty","object":"4 cells for the fox by default"}
|
|
@@ -446,7 +446,7 @@
|
|
|
446
446
|
{"world":"town-square-market","kind":"fact","subject":"goblin","predicate":"mgx:lose","object":"0.06 mass per turn by default"}
|
|
447
447
|
{"world":"town-square-market","kind":"fact","subject":"goblin","predicate":"mgx:arrive","object":"every 5 turns at the edge of the board by default"}
|
|
448
448
|
{"world":"town-square-market","kind":"fact","subject":"crumb","predicate":"mgx:arrive","object":"every 3 turns on any open cell by default"}
|
|
449
|
-
{"world":"town-square-market","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass
|
|
449
|
+
{"world":"town-square-market","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
450
450
|
{"world":"town-square-market","kind":"fact","subject":"morsel","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
451
451
|
{"world":"town-square-market","kind":"fact","subject":"morsel","predicate":"mgx:arrive","object":"where the player puts it"}
|
|
452
452
|
{"world":"town-square-market","kind":"fact","subject":"vision radius","predicate":"mgx:hasProperty","object":"4 cells for the fox by default"}
|
|
@@ -678,7 +678,7 @@
|
|
|
678
678
|
{"world":"town-square","kind":"fact","subject":"goblin","predicate":"mgx:lose","object":"0.06 mass per turn by default"}
|
|
679
679
|
{"world":"town-square","kind":"fact","subject":"goblin","predicate":"mgx:arrive","object":"every 5 turns at the edge of the board by default"}
|
|
680
680
|
{"world":"town-square","kind":"fact","subject":"crumb","predicate":"mgx:arrive","object":"every 3 turns on any open cell by default"}
|
|
681
|
-
{"world":"town-square","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass
|
|
681
|
+
{"world":"town-square","kind":"fact","subject":"crumb","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
682
682
|
{"world":"town-square","kind":"fact","subject":"morsel","predicate":"mgx:start-with","object":"mass 2 by default"}
|
|
683
683
|
{"world":"town-square","kind":"fact","subject":"morsel","predicate":"mgx:arrive","object":"where the player puts it"}
|
|
684
684
|
{"world":"town-square","kind":"fact","subject":"vision radius","predicate":"mgx:hasProperty","object":"4 cells for the fox by default"}
|
package/data/mudiii-assets.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"destPath": "public/models/creatures/fox.glb",
|
|
22
22
|
"licence": "CC0-1.0",
|
|
23
23
|
"author": "Quaternius",
|
|
24
|
-
"source": "https://poly.pizza/u/Quaternius
|
|
24
|
+
"source": "https://poly.pizza/u/Quaternius \u00b7 https://quaternius.com",
|
|
25
25
|
"bytes": 332748,
|
|
26
26
|
"sha256": "2694028b47e84bad3b9d985aa7743dae02a3bcfddd6becd73b46c747a1e067b3",
|
|
27
27
|
"targetHeight": 1.0,
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"walk": "Walk",
|
|
31
31
|
"run": "Gallop",
|
|
32
32
|
"attack": "Attack",
|
|
33
|
-
"hit": [
|
|
33
|
+
"hit": [
|
|
34
|
+
"Idle_HitReact_Left",
|
|
35
|
+
"Idle_HitReact_Right"
|
|
36
|
+
],
|
|
34
37
|
"death": "Death",
|
|
35
38
|
"eat": "Eating"
|
|
36
39
|
}
|
|
@@ -41,7 +44,7 @@
|
|
|
41
44
|
"destPath": "public/models/creatures/goblin.glb",
|
|
42
45
|
"licence": "CC0-1.0",
|
|
43
46
|
"author": "Quaternius",
|
|
44
|
-
"source": "https://poly.pizza/u/Quaternius
|
|
47
|
+
"source": "https://poly.pizza/u/Quaternius \u00b7 https://quaternius.com",
|
|
45
48
|
"bytes": 47924,
|
|
46
49
|
"sha256": "2f02ce927a8871554f3931655d75f1abb8237d36be8a19a35b09920d0812bc3c",
|
|
47
50
|
"targetHeight": 2.1,
|
|
@@ -50,7 +53,9 @@
|
|
|
50
53
|
"walk": "Walk",
|
|
51
54
|
"run": "Run",
|
|
52
55
|
"attack": "Attack",
|
|
53
|
-
"hit": [
|
|
56
|
+
"hit": [
|
|
57
|
+
"HitRecieve"
|
|
58
|
+
],
|
|
54
59
|
"death": "Death"
|
|
55
60
|
}
|
|
56
61
|
},
|
|
@@ -207,7 +212,7 @@
|
|
|
207
212
|
"source": "https://github.com/KayKit-Game-Assets/KayKit-Dungeon-Remastered-1.0",
|
|
208
213
|
"bytes": 14004,
|
|
209
214
|
"sha256": "8b340af18ea1325d5a9bd4a967d3ef37bb90838ca2d90927b123e23877f7ddad",
|
|
210
|
-
"targetHeight": 0.
|
|
215
|
+
"targetHeight": 0.6,
|
|
211
216
|
"clips": null
|
|
212
217
|
},
|
|
213
218
|
{
|
|
@@ -219,7 +224,7 @@
|
|
|
219
224
|
"source": "https://github.com/KayKit-Game-Assets/KayKit-Dungeon-Remastered-1.0",
|
|
220
225
|
"bytes": 14004,
|
|
221
226
|
"sha256": "8b340af18ea1325d5a9bd4a967d3ef37bb90838ca2d90927b123e23877f7ddad",
|
|
222
|
-
"targetHeight": 0.
|
|
227
|
+
"targetHeight": 0.6,
|
|
223
228
|
"clips": null
|
|
224
229
|
}
|
|
225
230
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
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; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -78,7 +78,9 @@ export const DEFAULT_GAME_CONFIG = Object.freeze({
|
|
|
78
78
|
preyVisionRadius: 3,
|
|
79
79
|
preySpawnIntervalTurns: 5,
|
|
80
80
|
foodSpawnIntervalTurns: 3,
|
|
81
|
-
|
|
81
|
+
// Spawned and placed food are one thing: the same hay bale, worth the
|
|
82
|
+
// same. The two knobs stay separate so a world can still tell them apart.
|
|
83
|
+
spawnedFoodMass: 2,
|
|
82
84
|
placedFoodMass: 2,
|
|
83
85
|
maxPreyPopulation: 6,
|
|
84
86
|
maxFoodItems: 8,
|
|
@@ -57,9 +57,9 @@ export const chebyshevDistance = (ax, ay, bx, by) => Math.max(Math.abs(ax - bx),
|
|
|
57
57
|
export const inBounds = (x, y) => x >= 1 && x <= GRID_SIZE && y >= 1 && y <= GRID_SIZE;
|
|
58
58
|
|
|
59
59
|
/** Every cell within Chebyshev `radius` of (cx, cy), clipped to the board, in
|
|
60
|
-
* raster order — the one visibility primitive
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* raster order — the one visibility primitive ("visibleCells(cx, cy, radius=4)")
|
|
61
|
+
* shared by both agents' belief and this module's own web-block derivation
|
|
62
|
+
* below. */
|
|
63
63
|
export function visibleCells(cx, cy, radius) {
|
|
64
64
|
const out = [];
|
|
65
65
|
for (let y = Math.max(1, cy - radius); y <= Math.min(GRID_SIZE, cy + radius); y += 1) {
|
|
@@ -72,8 +72,7 @@ export function visibleCells(cx, cy, radius) {
|
|
|
72
72
|
|
|
73
73
|
export const isInWebBlock = (x, y) => chebyshevDistance(x, y, WEB_HOME.x, WEB_HOME.y) <= WEB_RADIUS;
|
|
74
74
|
|
|
75
|
-
/** Every board-edge (perimeter) cell, raster order
|
|
76
|
-
* arrives (PLAN_SPIDER_FLY.md §10). */
|
|
75
|
+
/** Every board-edge (perimeter) cell, raster order. */
|
|
77
76
|
export function perimeterCells() {
|
|
78
77
|
const out = [];
|
|
79
78
|
for (let y = 1; y <= GRID_SIZE; y += 1) {
|
|
@@ -110,11 +109,10 @@ export function oneStepDirectionBetween(fromCell, toCell) {
|
|
|
110
109
|
return null;
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
/** The world's seed taxonomy
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* as rdfs:subClassOf facts. */
|
|
112
|
+
/** The world's seed taxonomy — enough for the ontology-to-sprite worked example
|
|
113
|
+
* (a poodle sprite, a sheepdog falling back to the generic dog sprite) to run
|
|
114
|
+
* on the default persona without the --persona-size large flag. [subject,
|
|
115
|
+
* superclass] pairs, written as rdfs:subClassOf facts. */
|
|
118
116
|
export const SEED_TAXONOMY = Object.freeze([
|
|
119
117
|
Object.freeze(["poodle", "dog"]),
|
|
120
118
|
Object.freeze(["sheepdog", "dog"]),
|
|
@@ -293,9 +291,9 @@ export function isLiveRenderableAgent(id, state) {
|
|
|
293
291
|
/** A minimal, inert rule-row family, so scripts/build-worlds-pack.mjs's
|
|
294
292
|
* shared validator ("every world needs at least one rule row") passes.
|
|
295
293
|
* src/services/spider-fly-turn.mjs never reads these back: grid movement is
|
|
296
|
-
* hand-written pathfinding over findActionPath/findReachableSet
|
|
297
|
-
*
|
|
298
|
-
*
|
|
294
|
+
* hand-written pathfinding over findActionPath/findReachableSet, not the
|
|
295
|
+
* taught action-Rule DSL, so this rides in the shard unused, same as an
|
|
296
|
+
* unrelated fact would. */
|
|
299
297
|
export function* worldRuleRows() {
|
|
300
298
|
yield {
|
|
301
299
|
world: WORLD_NAME, kind: "rule", name: "go", ruleKind: "action-signature",
|
|
@@ -186,12 +186,14 @@ export function currentActionFor(agentId, agentsById, moving) {
|
|
|
186
186
|
* so acting on the ecology row too would double it). `kind` is one of
|
|
187
187
|
* "death" (the rig's own Death clip, then scale to zero) or "consume" (an
|
|
188
188
|
* eaten item scales to zero, no clip — items are primitive geometry).
|
|
189
|
-
*
|
|
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. */
|
|
190
192
|
export function flourishForEcologyEvent(event) {
|
|
191
193
|
if (!event || !event.type) return null;
|
|
192
194
|
switch (event.type) {
|
|
193
195
|
case "eat-agent":
|
|
194
|
-
return { kind: "death", targetId: event.prey, cell: event.cell };
|
|
196
|
+
return { kind: "death", targetId: event.prey, cell: event.cell, actorId: event.predator };
|
|
195
197
|
case "starve":
|
|
196
198
|
return { kind: "death", targetId: event.agent, cell: event.cell };
|
|
197
199
|
case "eat-item":
|
|
@@ -359,6 +361,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
359
361
|
var manifestByKind = {};
|
|
360
362
|
var lastAgentsById = {};
|
|
361
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 = [];
|
|
362
369
|
var cameraState = { mode: "overhead", selectedId: null };
|
|
363
370
|
var cameraTween = null, lookAtTween = null;
|
|
364
371
|
var lastFrameTs = null;
|
|
@@ -385,9 +392,6 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
385
392
|
});
|
|
386
393
|
});
|
|
387
394
|
}
|
|
388
|
-
var glbCache = null;
|
|
389
|
-
function loadGlb(url) { return glbCache.load(url); }
|
|
390
|
-
|
|
391
395
|
async function ensureThree() {
|
|
392
396
|
if (THREE) return true;
|
|
393
397
|
var result = await loadThreeVendor();
|
|
@@ -396,7 +400,6 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
396
400
|
OrbitControlsCtor = result.OrbitControls; MeshoptDecoderRef = result.MeshoptDecoder;
|
|
397
401
|
gltfLoader = new GLTFLoaderCtor();
|
|
398
402
|
gltfLoader.setMeshoptDecoder(MeshoptDecoderRef);
|
|
399
|
-
glbCache = createCachedLoader(loadGlbRaw);
|
|
400
403
|
setUpScene();
|
|
401
404
|
return true;
|
|
402
405
|
}
|
|
@@ -513,13 +516,23 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
513
516
|
object3D.userData.tmctNormalized = true;
|
|
514
517
|
}
|
|
515
518
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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];
|
|
523
536
|
}
|
|
524
537
|
|
|
525
538
|
async function placeProps(propPlacements) {
|
|
@@ -544,13 +557,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
544
557
|
}
|
|
545
558
|
}
|
|
546
559
|
|
|
547
|
-
// ---- items: crumbs and morsels,
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
function
|
|
552
|
-
function itemMaterialFor(kind) { return new THREE.MeshStandardMaterial({ color: kind === "morsel" ? 0xd98a2b : 0x8c6a3f }); }
|
|
553
|
-
function itemHeightFor(kind) { return kind === "morsel" ? MORSEL_RADIUS : CRUMB_RADIUS; }
|
|
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"; }
|
|
554
565
|
|
|
555
566
|
function animateScaleTo(object3D, target, now) {
|
|
556
567
|
var duration = tweenDurationMs;
|
|
@@ -566,29 +577,55 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
566
577
|
requestAnimationFrame(step);
|
|
567
578
|
}
|
|
568
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.
|
|
569
584
|
function applyItemTick(id, item, now) {
|
|
570
585
|
var world = cellToWorld(item.cell, GRID_SIZE, CELL_SIZE);
|
|
571
586
|
if (!world) return;
|
|
572
587
|
var entry = itemMeshes[id];
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
mesh.
|
|
576
|
-
mesh.position.set(world.x, itemHeightFor(item.kind), world.z);
|
|
577
|
-
mesh.scale.setScalar(0);
|
|
578
|
-
scene.add(mesh);
|
|
579
|
-
entry = { mesh: mesh, cell: item.cell, kind: item.kind };
|
|
580
|
-
itemMeshes[id] = entry;
|
|
581
|
-
animateScaleTo(mesh, 1, now);
|
|
588
|
+
if (entry) {
|
|
589
|
+
entry.cell = item.cell;
|
|
590
|
+
entry.mesh.position.set(world.x, 0, world.z);
|
|
582
591
|
return;
|
|
583
592
|
}
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function recordRemoval(kind, id, source) {
|
|
617
|
+
removalLog.push({ kind: kind, id: id, source: source });
|
|
586
618
|
}
|
|
587
619
|
|
|
588
|
-
|
|
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) {
|
|
589
625
|
var entry = itemMeshes[id];
|
|
590
626
|
if (!entry) return;
|
|
591
627
|
delete itemMeshes[id];
|
|
628
|
+
recordRemoval("item", id, source);
|
|
592
629
|
if (withFlourish) animateScaleTo(entry.mesh, 0, performance.now());
|
|
593
630
|
else if (entry.mesh.parent) entry.mesh.parent.remove(entry.mesh);
|
|
594
631
|
}
|
|
@@ -603,7 +640,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
603
640
|
var kind = roleOfAgentId(id);
|
|
604
641
|
var entry = {
|
|
605
642
|
group: new THREE.Group(), tween: null, cell: null, facing: agent.facing, role: agent.role,
|
|
606
|
-
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null,
|
|
643
|
+
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null, oneShotAction: null,
|
|
607
644
|
};
|
|
608
645
|
entry.group.visible = false;
|
|
609
646
|
scene.add(entry.group);
|
|
@@ -628,6 +665,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
628
665
|
}
|
|
629
666
|
|
|
630
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
|
+
}
|
|
631
677
|
if (!entry.mixer || !clipName || entry.currentClip === clipName) return;
|
|
632
678
|
var next = entry.actions[clipName];
|
|
633
679
|
if (!next) return;
|
|
@@ -637,6 +683,28 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
637
683
|
entry.currentClip = clipName;
|
|
638
684
|
}
|
|
639
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
|
+
|
|
640
708
|
function playSpawnFlourish(group, now) {
|
|
641
709
|
group.scale.setScalar(0);
|
|
642
710
|
animateScaleTo(group, 1, now);
|
|
@@ -667,10 +735,11 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
667
735
|
}
|
|
668
736
|
}
|
|
669
737
|
|
|
670
|
-
function removeAgent(id) {
|
|
738
|
+
function removeAgent(id, source) {
|
|
671
739
|
var entry = agentGroups[id];
|
|
672
740
|
if (!entry) return;
|
|
673
741
|
delete agentGroups[id];
|
|
742
|
+
recordRemoval("agent", id, source);
|
|
674
743
|
var deathClip = entry.clipMap && entry.clipMap.death;
|
|
675
744
|
var deathClipName = Array.isArray(deathClip) ? deathClip[0] : deathClip;
|
|
676
745
|
if (deathClipName) {
|
|
@@ -683,10 +752,17 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
683
752
|
|
|
684
753
|
function applyEcology(ecology) {
|
|
685
754
|
for (var i = 0; i < (ecology || []).length; i += 1) {
|
|
686
|
-
var
|
|
755
|
+
var event = ecology[i];
|
|
756
|
+
var flourish = flourishForEcologyEvent(event);
|
|
687
757
|
if (!flourish) continue;
|
|
688
|
-
if (flourish.kind === "death") removeAgent(flourish.targetId);
|
|
689
|
-
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
|
+
}
|
|
690
766
|
}
|
|
691
767
|
}
|
|
692
768
|
|
|
@@ -741,6 +817,7 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
741
817
|
itemMeshes = {};
|
|
742
818
|
lastAgentsById = {};
|
|
743
819
|
lastItemsById = {};
|
|
820
|
+
removalLog = [];
|
|
744
821
|
manifestByKind = buildManifestByKind(input && input.assetManifest);
|
|
745
822
|
await placeProps((input && input.propPlacements) || []);
|
|
746
823
|
setCamera({ mode: "overhead", selectedId: null });
|
|
@@ -754,9 +831,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
754
831
|
var items = (tick && tick.items) || {};
|
|
755
832
|
for (var id in agents) if (Object.prototype.hasOwnProperty.call(agents, id)) applyAgentTick(id, agents[id], now);
|
|
756
833
|
for (var itemId in items) if (Object.prototype.hasOwnProperty.call(items, itemId)) applyItemTick(itemId, items[itemId], now);
|
|
757
|
-
|
|
758
|
-
|
|
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.
|
|
759
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");
|
|
760
843
|
lastAgentsById = agents;
|
|
761
844
|
lastItemsById = items;
|
|
762
845
|
refreshCameraTween(now);
|
|
@@ -812,6 +895,15 @@ export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {
|
|
|
812
895
|
var asset = manifestByKind[entry.kind];
|
|
813
896
|
return { height: size.y, minY: box.min.y, targetHeight: asset ? asset.targetHeight : null };
|
|
814
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
|
+
},
|
|
815
907
|
ready: function () { return booted; },
|
|
816
908
|
};
|
|
817
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
|
}
|