@polycode-projects/the-mechanical-code-talker 3.2.0 → 4.0.0
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/sprites/src/sprite-facts.jsonl +28 -0
- package/corpus/tier2/generate.mjs +10 -1
- package/corpus/tier2/human.jsonl +23 -0
- package/corpus/tier2/manifest.json +3 -3
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +15 -5
- package/corpus/worlds/shards/mud-garden.jsonl.gz +0 -0
- package/corpus/worlds/src/mud-garden.jsonl +101 -0
- package/package.json +2 -1
- package/src/adapters/p2p/webrtc-transport.mjs +146 -0
- package/src/domain/game-config.mjs +67 -0
- package/src/domain/grammar/ace.mjs +11 -3
- package/src/domain/grammar/lexicon-core.json +3 -0
- package/src/domain/grammar/lexicon.mjs +13 -0
- package/src/domain/memory/trust.mjs +15 -0
- package/src/domain/p2p/facts.mjs +81 -0
- package/src/domain/p2p/peer-id.mjs +32 -0
- package/src/domain/p2p/provenance-relabel.mjs +26 -0
- package/src/domain/p2p/sync-filter.mjs +31 -0
- package/src/domain/p2p/wire.mjs +123 -0
- package/src/domain/sprite-map.mjs +10 -2
- package/src/services/adventure-editor.mjs +10 -2
- package/src/services/adventure-viz.mjs +192 -39
- package/src/services/adventure.mjs +989 -69
- package/src/services/chat-page-viz.mjs +1060 -7
- package/src/services/chat-session.mjs +28 -3
- package/src/services/chat.mjs +2 -2
- package/src/services/mud-editor.mjs +313 -0
- package/src/services/mud-turn.mjs +572 -0
- package/src/services/mud-viz.mjs +2055 -0
- package/src/services/p2p-room.mjs +559 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +77 -77
- package/src/surfaces/web/mud-browser-entry.mjs +330 -0
- package/src/surfaces/web/p2p-browser-entry.mjs +39 -0
|
@@ -281,6 +281,14 @@ export function parseWorldEditorText(text, contextRows = []) {
|
|
|
281
281
|
|
|
282
282
|
const tripleKey = (t) => `${t.subject} ${t.predicate} ${t.object}`;
|
|
283
283
|
|
|
284
|
+
// The three flag predicates the renderer only ever writes a line for when they
|
|
285
|
+
// read "true" ("Basket is a container."). A row saying "false" has no sentence
|
|
286
|
+
// in this vocabulary, so it must not be diffable either — counted as editable
|
|
287
|
+
// while being unrenderable, it sits in the diff's current set, matches nothing
|
|
288
|
+
// the text can say, and gets retracted on the next clean parse: a fact the
|
|
289
|
+
// player never saw, silently deleted by an edit somewhere else in the document.
|
|
290
|
+
const TRUE_ONLY_FLAG_PREDICATES = ["mgx:is-container", "mgx:is-npc", "mgx:is-objective"];
|
|
291
|
+
|
|
284
292
|
/** Every raw fact row this editor's "other" family (type/exits/container/
|
|
285
293
|
* puzzle — never placement/openness) is allowed to touch — the closed set
|
|
286
294
|
* planWorldEditorSync diffs against and the only rows a removal can ever
|
|
@@ -290,8 +298,8 @@ export function editableOtherRows(rows) {
|
|
|
290
298
|
if (SNAPSHOT_RE.test(r.subject)) return false;
|
|
291
299
|
if (r.predicate === "rdf:type") return true;
|
|
292
300
|
if (EXIT_PREDICATE_RE.test(r.predicate)) return true;
|
|
293
|
-
|
|
294
|
-
|
|
301
|
+
if (TRUE_ONLY_FLAG_PREDICATES.includes(r.predicate)) return r.object === "true";
|
|
302
|
+
return ["mgx:unlocks-with", "mgx:acts-on-turn", "mgx:acts-toward"].includes(r.predicate);
|
|
295
303
|
});
|
|
296
304
|
}
|
|
297
305
|
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
// ancestor walk, then floor), `roomSceneLayout` (the room split into a wall
|
|
34
34
|
// band and floor stacks, over `roomSceneObjects` and `scenePlacement`),
|
|
35
35
|
// `roomKindForRoom` (a room's border treatment, from its own rdf:type),
|
|
36
|
-
// `carriedItems` (every object placed with the player),
|
|
36
|
+
// `carriedItems` (every object placed with the player), `exitDoorways` (the
|
|
37
|
+
// written ways out of one room, in compass order, each marked with whether
|
|
38
|
+
// this session has walked it — what the room view's own door plates are
|
|
39
|
+
// drawn from), and `visitedRoomGraph`
|
|
37
40
|
// (a directions-only layout of the rooms a session has actually visited —
|
|
38
41
|
// see its own header for the exposure discipline). None of these import
|
|
39
42
|
// anything beyond this module's own exports, which is what keeps a raw
|
|
@@ -109,22 +112,41 @@ export function spriteClassForObject(rows, subject) {
|
|
|
109
112
|
return typeRow ? typeRow.object : "portable";
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
/** `rows` plus
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
115
|
+
/** `rows` plus synthetic rdfs:subClassOf edges covering `subject`'s whole
|
|
116
|
+
* class chain — never written back to the corpus, just handed to
|
|
117
|
+
* sprite-templates.mjs's resolver so a NAMED object (`cabinet`, `butler`)
|
|
118
|
+
* can carry its own sprite template while an unauthored object of the same
|
|
119
|
+
* class still falls back cleanly, one ancestor at a time, to the nearest
|
|
120
|
+
* ancestor that HAS a sprite. Two kinds of edge are synthesized: one from
|
|
121
|
+
* the subject's own name to its declared sprite class
|
|
122
|
+
* (spriteClassForObject's own result), and one mirroring every `rdf:type`
|
|
123
|
+
* hop further up the chain. The mirroring is what makes the fallback reach
|
|
124
|
+
* past the first hop: the resolver walks rdfs:subClassOf only, so a world
|
|
125
|
+
* that writes `turnip rdf:type vegetable` would otherwise stop at `turnip`
|
|
126
|
+
* and skip a perfectly good `vegetable` sprite. Ashcombe Hall's own objects
|
|
127
|
+
* have no rdfs:subClassOf chain at all (each is directly typed, e.g.
|
|
128
|
+
* `cabinet rdf:type furniture`); this is the same ancestor-walk mechanism
|
|
129
|
+
* spider-fly's real poodle-IsA-dog taxonomy already exercises, synthesized
|
|
130
|
+
* at render time for a world whose taxonomy doesn't reach that deep. Returns
|
|
131
|
+
* `rows` itself when there is nothing to synthesize. Pure. */
|
|
124
132
|
export function spriteAncestryRows(rows, subject) {
|
|
125
133
|
const declaredClass = spriteClassForObject(rows, subject);
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
const synthesized = [];
|
|
135
|
+
if (subject !== declaredClass) synthesized.push({ subject, predicate: "rdfs:subClassOf", object: declaredClass });
|
|
136
|
+
const seen = new Set([subject]);
|
|
137
|
+
const queue = [declaredClass];
|
|
138
|
+
while (queue.length) {
|
|
139
|
+
const node = queue.shift();
|
|
140
|
+
if (seen.has(node)) continue;
|
|
141
|
+
seen.add(node);
|
|
142
|
+
for (const row of rows || []) {
|
|
143
|
+
if (row.subject !== node || row.object === node) continue;
|
|
144
|
+
if (row.predicate === "rdf:type") synthesized.push({ subject: node, predicate: "rdfs:subClassOf", object: row.object });
|
|
145
|
+
else if (row.predicate !== "rdfs:subClassOf") continue;
|
|
146
|
+
queue.push(row.object);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return synthesized.length ? [...(rows || []), ...synthesized] : rows;
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
/** Every fact row belonging to `subject` — the small `{predicate, object}`
|
|
@@ -415,6 +437,28 @@ export function goalStatusLines(rows, state, visitedRoomIds) {
|
|
|
415
437
|
});
|
|
416
438
|
}
|
|
417
439
|
|
|
440
|
+
/** Every written way out of `here`, in a fixed compass order, each with the
|
|
441
|
+
* room it leads to and whether this session has already walked there:
|
|
442
|
+
* `[{ direction, target, walked }]`. Only exits the world actually writes
|
|
443
|
+
* appear, so a doorway drawn from this list can never offer a `go` the
|
|
444
|
+
* engine would then refuse. `walked` reads the SAME visited set the manor
|
|
445
|
+
* map and the goal lines already do — the rooms the player has stood in, so
|
|
446
|
+
* marking a door as new tells them nothing they haven't earned. Ashcombe
|
|
447
|
+
* Hall teaches no `dig` action family, so unlike the burrow's own compass
|
|
448
|
+
* ring there is no second kind of way out to draw. Pure and
|
|
449
|
+
* `.toString()`-splice-safe. */
|
|
450
|
+
export function exitDoorways(state, here, visitedRoomIds) {
|
|
451
|
+
const compass = ["north", "east", "south", "west", "up", "down"];
|
|
452
|
+
const walked = new Set(visitedRoomIds || []);
|
|
453
|
+
const exits = state.exits.get(here);
|
|
454
|
+
if (!exits) return [];
|
|
455
|
+
return compass.filter((direction) => exits.has(direction)).map((direction) => ({
|
|
456
|
+
direction,
|
|
457
|
+
target: exits.get(direction),
|
|
458
|
+
walked: walked.has(exits.get(direction)),
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
|
|
418
462
|
/** The clickable command suggestions for the room the player is CURRENTLY
|
|
419
463
|
* in — a thin, testable wrapper over adventure.mjs's own `roomAffordances`
|
|
420
464
|
* (the exact same data take/open/talk/examine already check), so a pill can
|
|
@@ -667,10 +711,47 @@ ${THEME_TOKENS_CSS}
|
|
|
667
711
|
falls back through room's own ancestor chain to the generic room icon).
|
|
668
712
|
Rendered at 120px — twice a room-object sprite's own 60px frame
|
|
669
713
|
(.sprite-frame below) — so the room itself reads as the biggest thing
|
|
670
|
-
drawn in its own scene.
|
|
671
|
-
|
|
714
|
+
drawn in its own scene. It takes the wall's own brass rather than the
|
|
715
|
+
page ink: at that size an ink-black icon out-shouts every person and
|
|
716
|
+
prop standing in front of it, and this is a mark ON the wall, not
|
|
717
|
+
another thing in the room. */
|
|
718
|
+
.room-kind-icon { position: absolute; top: .5rem; right: .6rem; width: 120px; height: 120px; color: var(--gilt); opacity: .5; }
|
|
672
719
|
.room-kind-icon svg { width: 100%; height: 100%; display: block; }
|
|
673
720
|
.room-kind-icon:empty { display: none; }
|
|
721
|
+
/* the doorways — every written way out drawn where it actually points,
|
|
722
|
+
as a brass door plate set into the wall it leads through. The plate
|
|
723
|
+
borrows the room plaque's own treatment above (parchment ground, gilt
|
|
724
|
+
rule, inset mat): a manor has one brass vocabulary, and a door plate and
|
|
725
|
+
a room plate are the same object. North and south sit in a band OUTSIDE
|
|
726
|
+
the frame so neither fights the plaque or the room-kind icon; west and
|
|
727
|
+
east sit just inside their own walls. Stairs get a round chip instead —
|
|
728
|
+
neither up nor down is a compass point, and neither hangs on a wall. */
|
|
729
|
+
.room-stage { position: relative; padding: 1.35rem 0; }
|
|
730
|
+
.dir-ring { position: absolute; inset: 0; pointer-events: none; }
|
|
731
|
+
.dir-slot { position: absolute; pointer-events: auto; }
|
|
732
|
+
.dir-north { top: 0; left: 50%; transform: translateX(-50%); }
|
|
733
|
+
.dir-south { bottom: 0; left: 50%; transform: translateX(-50%); }
|
|
734
|
+
.dir-west { left: .3rem; top: 50%; transform: translateY(-50%); }
|
|
735
|
+
.dir-east { right: .3rem; top: 50%; transform: translateY(-50%); }
|
|
736
|
+
/* the stairs stand at the left of each band: the frame's own corner
|
|
737
|
+
flourishes sit top-left and bottom-right, and this is the one inset that
|
|
738
|
+
clears both. */
|
|
739
|
+
.dir-up { top: 0; left: 1.4rem; }
|
|
740
|
+
.dir-down { bottom: 0; left: 1.4rem; }
|
|
741
|
+
.dir-door {
|
|
742
|
+
position: relative; display: inline-flex; align-items: center; justify-content: center;
|
|
743
|
+
font-family: ${MONO_STACK}; font-size: .62rem; letter-spacing: .14em; text-transform: uppercase;
|
|
744
|
+
color: var(--gilt); background: var(--parchment); border: 1px solid var(--gilt);
|
|
745
|
+
box-shadow: inset 0 0 0 2px var(--parchment-strong);
|
|
746
|
+
padding: .2rem .6rem; white-space: nowrap;
|
|
747
|
+
}
|
|
748
|
+
.dir-door:hover, .dir-door:focus-visible { background: var(--parchment-strong); color: var(--ink); }
|
|
749
|
+
.dir-door.stair { border-radius: 50%; width: 1.5rem; height: 1.5rem; padding: 0; font-size: .72rem; letter-spacing: 0; }
|
|
750
|
+
/* a door onto a room this session has not stood in yet, marked with the
|
|
751
|
+
same gilt dot the manor board prints for a direction it cannot draw a
|
|
752
|
+
room for — one hint shape, two surfaces. */
|
|
753
|
+
.dir-door .unwalked { position: absolute; top: -.16rem; right: -.16rem; width: .34rem; height: .34rem; border-radius: 50%; background: var(--gilt); }
|
|
754
|
+
|
|
674
755
|
.sprite-row { display: flex; align-items: flex-end; gap: .9rem .7rem; min-height: 2.5rem; }
|
|
675
756
|
/* the floor band: every non-wall-mounted stack, wrapping and pinned to the
|
|
676
757
|
left edge of the room, as the flat sprite row always has. */
|
|
@@ -874,6 +955,10 @@ ${THEME_TOKENS_CSS}
|
|
|
874
955
|
#legendList { display: flex; flex-wrap: wrap; gap: .5rem .3rem; }
|
|
875
956
|
|
|
876
957
|
body.preview .side, body.preview .stage-left > .panel, body.preview .controls-row, body.preview .status { display: none; }
|
|
958
|
+
/* the hero iframe is a picture of a room, not a control surface — every
|
|
959
|
+
other control is hidden there, and the doorways go with them. */
|
|
960
|
+
body.preview .room-stage { padding: 0; }
|
|
961
|
+
body.preview .dir-ring { display: none; }
|
|
877
962
|
body.preview main { padding: 0; max-width: none; }
|
|
878
963
|
body.preview .stage { display: block; }
|
|
879
964
|
body.preview .eyebrow, body.preview .mode-toggle, body.preview #editStage, body.preview .page-note { display: none; }
|
|
@@ -904,13 +989,23 @@ ${THEME_TOKENS_CSS}
|
|
|
904
989
|
<h2>satchel</h2>
|
|
905
990
|
<div class="chips" id="carryList"></div>
|
|
906
991
|
</div>
|
|
907
|
-
<div class="room-
|
|
908
|
-
<div class="room-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
<div class="
|
|
913
|
-
|
|
992
|
+
<div class="room-stage">
|
|
993
|
+
<div class="room-frame" id="roomFrame">
|
|
994
|
+
<div class="room-plaque mono" id="roomName"></div>
|
|
995
|
+
<div class="room-kind-icon" id="roomKindIcon"></div>
|
|
996
|
+
<div class="wall-row" id="wallRow"></div>
|
|
997
|
+
<div class="sprite-row" id="spriteRow">
|
|
998
|
+
<div class="floor-row" id="floorRow"></div>
|
|
999
|
+
<div class="you-slot" id="youSlot"></div>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
<div class="dir-ring" id="dirRing" aria-label="the ways out of this room">
|
|
1003
|
+
<div class="dir-slot dir-north" id="dir-north"></div>
|
|
1004
|
+
<div class="dir-slot dir-east" id="dir-east"></div>
|
|
1005
|
+
<div class="dir-slot dir-south" id="dir-south"></div>
|
|
1006
|
+
<div class="dir-slot dir-west" id="dir-west"></div>
|
|
1007
|
+
<div class="dir-slot dir-up" id="dir-up"></div>
|
|
1008
|
+
<div class="dir-slot dir-down" id="dir-down"></div>
|
|
914
1009
|
</div>
|
|
915
1010
|
</div>
|
|
916
1011
|
<div class="panel command">
|
|
@@ -995,6 +1090,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
995
1090
|
const visitedRoomGraph = ${visitedRoomGraph.toString()};
|
|
996
1091
|
const allRoomIds = ${allRoomIds.toString()};
|
|
997
1092
|
const groundedPlaceholder = ${groundedPlaceholder.toString()};
|
|
1093
|
+
const exitDoorways = ${exitDoorways.toString()};
|
|
998
1094
|
const spriteAncestryRows = ${spriteAncestryRows.toString()};
|
|
999
1095
|
const factsForSubject = ${factsForSubject.toString()};
|
|
1000
1096
|
const renderWorldEditorText = ${renderWorldEditorText.toString()};
|
|
@@ -1004,6 +1100,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1004
1100
|
const roomFrameEl = el("roomFrame");
|
|
1005
1101
|
const roomNameEl = el("roomName");
|
|
1006
1102
|
const roomKindIconEl = el("roomKindIcon");
|
|
1103
|
+
const dirRingEl = el("dirRing");
|
|
1007
1104
|
const wallRowEl = el("wallRow");
|
|
1008
1105
|
const floorRowEl = el("floorRow");
|
|
1009
1106
|
const youSlotEl = el("youSlot");
|
|
@@ -1386,11 +1483,16 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1386
1483
|
// Clicking one inserts its exact command text into the input and focuses
|
|
1387
1484
|
// it; it never auto-submits, so free typing still works and a clicked
|
|
1388
1485
|
// suggestion can still be edited first.
|
|
1486
|
+
//
|
|
1487
|
+
// The row shows what to do with the things IN the room; the doorways above
|
|
1488
|
+
// own every way OUT of it. A "go" command is the one affordance no player
|
|
1489
|
+
// ever wants to edit before running, and drawing it in both places would
|
|
1490
|
+
// make one control a suggestion and its twin an action.
|
|
1389
1491
|
function pillsFor(rows, state, here) {
|
|
1390
1492
|
return tmctAdventure.roomAffordances(rows, state, here);
|
|
1391
1493
|
}
|
|
1392
1494
|
function renderPills(rows, state, here) {
|
|
1393
|
-
const actions = pillsFor(rows, state, here);
|
|
1495
|
+
const actions = pillsFor(rows, state, here).filter((a) => a.indexOf("go ") !== 0);
|
|
1394
1496
|
pillsEl.innerHTML = actions.map((a) => '<button type="button" class="pill">' + esc(a) + "</button>").join("");
|
|
1395
1497
|
// The empty input teaches the grounded noun form off the same affordance
|
|
1396
1498
|
// list the pills read — real props from THIS room, so a first-time player
|
|
@@ -1422,11 +1524,19 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1422
1524
|
// stay keyed on that declared class unchanged. The player's own "you" row
|
|
1423
1525
|
// has no backing fact row, so it resolves directly by its fixed
|
|
1424
1526
|
// "adventurer" class with no ancestry/property rows to check.
|
|
1527
|
+
//
|
|
1528
|
+
// When the whole chain misses, the root the resolver lands on is chosen by
|
|
1529
|
+
// what KIND of thing the class is, never the resolver's own "animal"
|
|
1530
|
+
// default: an unknown prop in a manor should read as a plain parcel and an
|
|
1531
|
+
// unknown character as a figure, and neither should ever draw a creature.
|
|
1532
|
+
const ROOT_FALLBACK_BY_CLASS = { adventurer: "person", person: "person", room: "room" };
|
|
1533
|
+
const rootFallbackFor = (cls) => ROOT_FALLBACK_BY_CLASS[cls] || "portable";
|
|
1425
1534
|
function resolveObjectSprite(rows, s) {
|
|
1426
|
-
if (s.subject === "you") return tmctAdventure.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY, { instanceKey: "you" });
|
|
1535
|
+
if (s.subject === "you") return tmctAdventure.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY, { rootFallback: "person", instanceKey: "you" });
|
|
1427
1536
|
return tmctAdventure.resolveSpriteAsset(
|
|
1428
1537
|
s.subject, spriteAncestryRows(rows, s.subject), factsForSubject(rows, s.subject),
|
|
1429
|
-
activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
|
|
1538
|
+
activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
|
|
1539
|
+
{ rootFallback: rootFallbackFor(s.spriteClass), instanceKey: s.subject },
|
|
1430
1540
|
);
|
|
1431
1541
|
}
|
|
1432
1542
|
|
|
@@ -1438,10 +1548,38 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1438
1548
|
function roomKindIconSvg(rows, here) {
|
|
1439
1549
|
return tmctAdventure.resolveSpriteAsset(
|
|
1440
1550
|
here, spriteAncestryRows(rows, here), factsForSubject(rows, here),
|
|
1441
|
-
activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
|
|
1551
|
+
activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
|
|
1552
|
+
{ rootFallback: "room", instanceKey: "roomkind-" + here },
|
|
1442
1553
|
);
|
|
1443
1554
|
}
|
|
1444
1555
|
|
|
1556
|
+
// ---- the doorways — one brass plate per written exit, in the slot that
|
|
1557
|
+
// exit actually points at, drawn from exitDoorways over the SAME snapshot
|
|
1558
|
+
// the room scene reads. A direction the world writes no exit for draws
|
|
1559
|
+
// nothing at all, so the ring never offers a "go" the engine would refuse.
|
|
1560
|
+
const STAIR_GLYPH = { up: "\\u2191", down: "\\u2193" };
|
|
1561
|
+
function renderDoorways(state, here, visitedRoomIds) {
|
|
1562
|
+
const bySlot = {};
|
|
1563
|
+
exitDoorways(state, here, visitedRoomIds).forEach((d) => { bySlot[d.direction] = d; });
|
|
1564
|
+
for (const direction of ["north", "east", "south", "west", "up", "down"]) {
|
|
1565
|
+
const slot = el("dir-" + direction);
|
|
1566
|
+
const doorway = bySlot[direction];
|
|
1567
|
+
if (!doorway) { slot.innerHTML = ""; continue; }
|
|
1568
|
+
const stair = direction === "up" || direction === "down";
|
|
1569
|
+
const label = "go " + direction + (doorway.walked ? "" : " (not walked yet)");
|
|
1570
|
+
slot.innerHTML = '<button type="button" class="dir-door' + (stair ? " stair" : "") + '"'
|
|
1571
|
+
+ ' data-direction="' + esc(direction) + '" title="' + esc(label) + '" aria-label="' + esc(label) + '">'
|
|
1572
|
+
+ (stair ? STAIR_GLYPH[direction] : esc(direction))
|
|
1573
|
+
+ (doorway.walked ? "" : '<span class="unwalked" aria-hidden="true"></span>') + "</button>";
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
// A door is not a suggestion to edit — clicking one walks through it.
|
|
1577
|
+
dirRingEl.addEventListener("click", (e) => {
|
|
1578
|
+
const btn = e.target.closest(".dir-door");
|
|
1579
|
+
if (!btn || !session) return;
|
|
1580
|
+
runTurn("go " + btn.getAttribute("data-direction"));
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1445
1583
|
function redraw(snap) {
|
|
1446
1584
|
lastSnapshot = snap;
|
|
1447
1585
|
const layout = roomSceneLayout(snap.rows, snap.state, snap.here);
|
|
@@ -1460,27 +1598,34 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1460
1598
|
captionEl.textContent = captionFor(snap.rows, snap.state, snap.here);
|
|
1461
1599
|
turnLabelEl.textContent = "turn: " + snap.turn;
|
|
1462
1600
|
renderPills(snap.rows, snap.state, snap.here);
|
|
1601
|
+
renderDoorways(snap.state, snap.here, snap.visitedRoomIds);
|
|
1463
1602
|
renderCarrying(snap.rows, snap.state);
|
|
1464
1603
|
renderRoomMap(snap.rows, snap.state, snap.visitedRoomIds);
|
|
1465
1604
|
renderGoals(snap.rows, snap.state, snap.visitedRoomIds);
|
|
1466
1605
|
schedulePersistSave();
|
|
1467
1606
|
}
|
|
1468
1607
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
// A manual command lands mid-tick otherwise — pause first, and stay
|
|
1475
|
-
// paused until the visitor presses play again.
|
|
1608
|
+
// Every command a visitor issues by hand — typed, or walked through a
|
|
1609
|
+
// doorway — runs here, so the transcript, the pause and the redraw are the
|
|
1610
|
+
// same for both. A manual command lands mid-tick otherwise, so this pauses
|
|
1611
|
+
// first, and stays paused until the visitor presses play again.
|
|
1612
|
+
function runTurn(line) {
|
|
1476
1613
|
ticker.pause();
|
|
1477
|
-
addChatLine("u", esc(
|
|
1478
|
-
withLock(async () => {
|
|
1479
|
-
const result = await session.turn(
|
|
1614
|
+
addChatLine("u", esc(line));
|
|
1615
|
+
return withLock(async () => {
|
|
1616
|
+
const result = await session.turn(line);
|
|
1480
1617
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1481
1618
|
const snap = await session.snapshot();
|
|
1482
1619
|
redraw(snap);
|
|
1483
1620
|
});
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
chatformEl.addEventListener("submit", (e) => {
|
|
1624
|
+
e.preventDefault();
|
|
1625
|
+
const q = chatqEl.value.trim();
|
|
1626
|
+
if (!q || !session) return;
|
|
1627
|
+
chatqEl.value = "";
|
|
1628
|
+
runTurn(q);
|
|
1484
1629
|
});
|
|
1485
1630
|
|
|
1486
1631
|
// ---- edit mode ---------------------------------------------------------
|
|
@@ -1522,12 +1667,20 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1522
1667
|
// ---- legend — every known object/character class this world actually
|
|
1523
1668
|
// uses, by its real icon. Scoped to worldOnlyRows so the background
|
|
1524
1669
|
// corpus's own (unrelated) rdf:type vocabulary never floods the list.
|
|
1670
|
+
// Each class resolves through the SAME ancestry walk and kind-aware root
|
|
1671
|
+
// the room's own cards use, so a class an edited world invents climbs to
|
|
1672
|
+
// the nearest ancestor that has a picture instead of dropping straight to
|
|
1673
|
+
// a generic one.
|
|
1525
1674
|
function renderLegend(rows) {
|
|
1526
1675
|
const subjects = new Set(rows.filter((r) => r.predicate === "rdf:type").map((r) => r.subject));
|
|
1527
1676
|
const classes = new Set(["adventurer"]);
|
|
1528
1677
|
subjects.forEach((s) => { if (s !== "player") classes.add(spriteClassForObject(rows, s)); });
|
|
1529
1678
|
legendListEl.innerHTML = Array.from(classes).sort().map((cls) => {
|
|
1530
|
-
const svg = tmctAdventure.resolveSpriteAsset(
|
|
1679
|
+
const svg = tmctAdventure.resolveSpriteAsset(
|
|
1680
|
+
cls, spriteAncestryRows(rows, cls), factsForSubject(rows, cls),
|
|
1681
|
+
activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY,
|
|
1682
|
+
{ rootFallback: rootFallbackFor(cls), instanceKey: "legend-" + cls },
|
|
1683
|
+
);
|
|
1531
1684
|
return spriteCardHtml(cls, cls, svg);
|
|
1532
1685
|
}).join("");
|
|
1533
1686
|
}
|