@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +37 -6
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// already takes with its own precomputed memory payload, just applied to a
|
|
20
20
|
// second kind of build-time data.
|
|
21
21
|
//
|
|
22
|
-
//
|
|
22
|
+
// Seven pure, `.toString()`-splice-safe pieces are exported as real functions
|
|
23
23
|
// (not raw inline-script text) so they can be pinned directly by tests, the
|
|
24
24
|
// same discipline spider-fly-viz.mjs holds classOfAgentId/
|
|
25
25
|
// threadCellsForSpiderPlan to: `spriteClassForObject` (an object's sprite
|
|
@@ -33,27 +33,35 @@
|
|
|
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
|
|
37
|
-
// written ways out of one room, in compass
|
|
38
|
-
// this session has walked it — what the room
|
|
39
|
-
// drawn from)
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// `
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// `
|
|
54
|
-
// `
|
|
55
|
-
//
|
|
56
|
-
//
|
|
36
|
+
// `carriedItems` (every object currently placed with a `holder`, "player" by
|
|
37
|
+
// default), and `exitDoorways` (the written ways out of one room, in compass
|
|
38
|
+
// order, each marked with whether this session has walked it — what the room
|
|
39
|
+
// view's own door plates are drawn from). None of these import anything
|
|
40
|
+
// beyond this module's own exports, which is what keeps a raw `.toString()`
|
|
41
|
+
// splice safe.
|
|
42
|
+
//
|
|
43
|
+
// Four further pure helpers are exported for testing but NOT spliced,
|
|
44
|
+
// because each calls another module's export — the in-page script instead
|
|
45
|
+
// reaches through the browser bundle's own `tmct` surface (mirroring
|
|
46
|
+
// how the spider-fly page's inline script calls it rather than re-importing
|
|
47
|
+
// spider-fly-world.mjs): `roomCaptionText` (calls `worldDigestRows`; the
|
|
48
|
+
// in-page `captionFor` mirrors it against `tmct.page.worldDigestRows`),
|
|
49
|
+
// `pillsForRoom` (a thin wrapper over adventure.mjs's own exported
|
|
50
|
+
// `roomAffordances`, whose header explains why its list can never promise an
|
|
51
|
+
// action one of take/open/talk/examine would then refuse; the in-page
|
|
52
|
+
// `pillsFor` mirrors it against `tmct.page.roomAffordances`),
|
|
53
|
+
// `goalStatusLines` (calls `foldWorldState` and adventure-autoplay.mjs's own
|
|
54
|
+
// `exposedFacts`; the in-page `goalStatusLinesFor` mirrors both against the
|
|
55
|
+
// `tmct` surface too), and `visitedRoomGraph` (a directions-only
|
|
56
|
+
// layout of the rooms a session has actually visited, now a thin wrapper over
|
|
57
|
+
// viz-room-graph.mjs's shared `directedGridLayout` — mud-viz.mjs's own
|
|
58
|
+
// burrowGraph wrote the same BFS-grid layout a second time under its own
|
|
59
|
+
// name, and that shared module is where the layout lives now; see its own
|
|
60
|
+
// header for the algorithm and what `hints`/disconnected components mean).
|
|
61
|
+
// The in-page script never splices `directedGridLayout`/`roomGraphSvg`
|
|
62
|
+
// either, for the same not-self-contained reason: it calls
|
|
63
|
+
// `tmct.page.directedGridLayout`/`tmct.page.roomGraphSvg` straight
|
|
64
|
+
// through the bundle instead of re-implementing the room map a second time.
|
|
57
65
|
//
|
|
58
66
|
// The chat dock (chatlog/chatform/chatq/pills, below) mirrors
|
|
59
67
|
// spider-fly-viz.mjs's own side panel: every manual exchange (via
|
|
@@ -84,13 +92,14 @@
|
|
|
84
92
|
// an edit implies run through the browser bundle's own `session.applyEdit`
|
|
85
93
|
// (adventure-browser-entry.mjs), never here — this module only renders and
|
|
86
94
|
// reads.
|
|
87
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel } from "./viz-theme.mjs";
|
|
88
|
-
import { createTicker } from "./viz-ticker.mjs";
|
|
95
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel, rowsForWorld, wordBeforeCursor } from "./viz-theme.mjs";
|
|
96
|
+
import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
|
|
97
|
+
import { directedGridLayout } from "./viz-room-graph.mjs";
|
|
89
98
|
import { worldDigestRows, roomAffordances, foldWorldState } from "./adventure.mjs";
|
|
90
99
|
import { exposedFacts } from "./adventure-autoplay.mjs";
|
|
91
100
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
92
101
|
import { classAncestorChain } from "../domain/sprite-map.mjs";
|
|
93
|
-
import { renderWorldEditorText
|
|
102
|
+
import { renderWorldEditorText } from "./adventure-editor.mjs";
|
|
94
103
|
|
|
95
104
|
const DEFAULT_TITLE = "tmct — the adventure";
|
|
96
105
|
const PREVIEW_MAX_TICKS = 30;
|
|
@@ -336,13 +345,16 @@ export function roomKindForRoom(rows, roomId) {
|
|
|
336
345
|
return "indoor";
|
|
337
346
|
}
|
|
338
347
|
|
|
339
|
-
/** Every object currently `mgx:located-in` `
|
|
340
|
-
*
|
|
348
|
+
/** Every object currently `mgx:located-in` `holder` (default "player"),
|
|
349
|
+
* sorted, each with its sprite class — the exact placement
|
|
341
350
|
* `worldDigestRows`'/`inventoryAnswer`'s own "carries the" branch already
|
|
342
|
-
* reads, just returned as a plain list instead of prose.
|
|
343
|
-
|
|
351
|
+
* reads, just returned as a plain list instead of prose. `holder` is a real
|
|
352
|
+
* parameter, not a name baked into the function — mud-viz.mjs's own
|
|
353
|
+
* `carriedItemsFor` used to hardcode "player" instead, so a differently-
|
|
354
|
+
* named character's own satchel never read correctly there. Pure. */
|
|
355
|
+
export function carriedItems(rows, state, holder = "player") {
|
|
344
356
|
return [...state.placements]
|
|
345
|
-
.filter(([, p]) => p.predicate === "mgx:located-in" && p.object ===
|
|
357
|
+
.filter(([, p]) => p.predicate === "mgx:located-in" && p.object === holder)
|
|
346
358
|
.map(([subject]) => ({ subject, spriteClass: spriteClassForObject(rows, subject) }))
|
|
347
359
|
.sort((a, b) => a.subject.localeCompare(b.subject));
|
|
348
360
|
}
|
|
@@ -361,47 +373,13 @@ export function carriedItems(rows, state, actingSubject = "player") {
|
|
|
361
373
|
* caller can draw at most "there's an exit that way" and nothing more.
|
|
362
374
|
* Disconnected visited rooms (not reachable from each other by traveled
|
|
363
375
|
* edges) lay out as separate side-by-side blocks rather than overlapping.
|
|
364
|
-
*
|
|
376
|
+
*
|
|
377
|
+
* A thin wrapper over viz-room-graph.mjs's shared `directedGridLayout`: no
|
|
378
|
+
* `root` (Ashcombe Hall is never dug two ways into one cell, so there is no
|
|
379
|
+
* level/turf to track) and no collision nudging (a manor fixed at authoring
|
|
380
|
+
* time never collides). Pure. */
|
|
365
381
|
export function visitedRoomGraph(state, visitedRoomIds, actingSubject = "player") {
|
|
366
|
-
|
|
367
|
-
const visited = new Set(visitedRoomIds || []);
|
|
368
|
-
const here = state.placements.get(actingSubject)?.object ?? null;
|
|
369
|
-
const positions = new Map();
|
|
370
|
-
const edges = [];
|
|
371
|
-
const edgeKeys = new Set();
|
|
372
|
-
const hints = [];
|
|
373
|
-
let offsetX = 0;
|
|
374
|
-
for (const start of [...visited].sort()) {
|
|
375
|
-
if (positions.has(start)) continue;
|
|
376
|
-
positions.set(start, { x: offsetX, y: 0 });
|
|
377
|
-
const queue = [start];
|
|
378
|
-
const component = [start];
|
|
379
|
-
while (queue.length) {
|
|
380
|
-
const room = queue.shift();
|
|
381
|
-
const pos = positions.get(room);
|
|
382
|
-
const dirs = state.exits.get(room);
|
|
383
|
-
for (const direction of [...(dirs?.keys() ?? [])].sort()) {
|
|
384
|
-
const target = dirs.get(direction);
|
|
385
|
-
if (!visited.has(target)) { hints.push({ from: room, direction }); continue; }
|
|
386
|
-
const key = [room, target].sort().join("\0");
|
|
387
|
-
if (!edgeKeys.has(key)) { edgeKeys.add(key); edges.push({ from: room, to: target, direction }); }
|
|
388
|
-
if (!positions.has(target)) {
|
|
389
|
-
const [dx, dy] = DELTA[direction] ?? [0, 0];
|
|
390
|
-
positions.set(target, { x: pos.x + dx, y: pos.y + dy });
|
|
391
|
-
component.push(target);
|
|
392
|
-
queue.push(target);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
offsetX = Math.max(...component.map((r) => positions.get(r).x)) + 2;
|
|
397
|
-
}
|
|
398
|
-
const minX = Math.min(0, ...[...positions.values()].map((p) => p.x));
|
|
399
|
-
const minY = Math.min(0, ...[...positions.values()].map((p) => p.y));
|
|
400
|
-
const nodes = [...visited].sort().map((room) => {
|
|
401
|
-
const p = positions.get(room) || { x: 0, y: 0 };
|
|
402
|
-
return { id: room, x: p.x - minX, y: p.y - minY, current: room === here };
|
|
403
|
-
});
|
|
404
|
-
return { nodes, edges, hints };
|
|
382
|
+
return directedGridLayout(state, visitedRoomIds, { actingSubject });
|
|
405
383
|
}
|
|
406
384
|
|
|
407
385
|
/** Every room the world DEFINES at all (every subject the fact rows type as
|
|
@@ -501,8 +479,8 @@ export function groundedPlaceholder(actions, fallback) {
|
|
|
501
479
|
* row. `[]` on an honest miss (neither lookup returns anything) — never a
|
|
502
480
|
* fabricated suggestion. NOT .toString()-splice-safe (it calls two other
|
|
503
481
|
* modules' exports) — the in-page script mirrors this same combination
|
|
504
|
-
* against the browser bundle's own `
|
|
505
|
-
* `
|
|
482
|
+
* against the browser bundle's own `tmct.page.relatedForTerm`/
|
|
483
|
+
* `tmct.page.classAncestorChain`, the same reach-through-the-global
|
|
506
484
|
* pattern `pillsFor`/`captionFor` already use for their own adventure.mjs
|
|
507
485
|
* calls (see this module's header). */
|
|
508
486
|
export function suggestionsForTerm(rows, term) {
|
|
@@ -528,11 +506,19 @@ export function suggestionsForTerm(rows, term) {
|
|
|
528
506
|
* itself (its own exits) or about something placed IN it — the same
|
|
529
507
|
* "visible here" boundary `roomSceneObjects` draws from. The player's own
|
|
530
508
|
* "is in the" row is excluded: the room frame already IS the current room,
|
|
531
|
-
* so restating "you are here" is redundant, never informative.
|
|
532
|
-
|
|
509
|
+
* so restating "you are here" is redundant, never informative.
|
|
510
|
+
*
|
|
511
|
+
* `{ caseInsensitive }` folds the room-id match to lowercase before
|
|
512
|
+
* comparing. Ashcombe Hall's own room ids are already lowercase, so this is
|
|
513
|
+
* a no-op here by default — it exists so mud-viz.mjs's own case-insensitive
|
|
514
|
+
* `roomCaptionFor` variant can share this one function instead of keeping a
|
|
515
|
+
* near-duplicate. */
|
|
516
|
+
export function roomCaptionText(rows, state, here, { caseInsensitive = false } = {}) {
|
|
533
517
|
const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
|
|
518
|
+
const objectMatches = (value) => (caseInsensitive ? String(value).toLowerCase() === here.toLowerCase() : value === here);
|
|
519
|
+
const subjectMatches = (value) => (caseInsensitive ? String(value).toLowerCase() === here.toLowerCase() : value === hereCased);
|
|
534
520
|
const lines = worldDigestRows(rows, state)
|
|
535
|
-
.filter((row) => row.subject !== "Player" && (row.object
|
|
521
|
+
.filter((row) => row.subject !== "Player" && (objectMatches(row.object) || subjectMatches(row.subject)))
|
|
536
522
|
.map((row) => `${row.subject} ${row.predicate} ${row.object}.`);
|
|
537
523
|
return lines.length ? lines.join(" ") : `Nothing more about the ${here} is written down yet.`;
|
|
538
524
|
}
|
|
@@ -886,7 +872,7 @@ ${THEME_TOKENS_CSS}
|
|
|
886
872
|
this just keeps the board's own footprint stable and click-to-enlarge
|
|
887
873
|
honest about what it's enlarging. */
|
|
888
874
|
.map-viewport-fixed { width: 190px; margin: 0 auto; cursor: zoom-in; }
|
|
889
|
-
/* the lights-down map lightbox — the same board, the same
|
|
875
|
+
/* the lights-down map lightbox — the same board, the same room-graph svg
|
|
890
876
|
output, just drawn bigger over a dimmed backdrop. Closes on a click
|
|
891
877
|
anywhere outside the enlarged board, or Escape. */
|
|
892
878
|
.map-lightbox { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 2.4rem; background: rgba(10, 8, 4, .74); }
|
|
@@ -1123,6 +1109,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1123
1109
|
(function () {
|
|
1124
1110
|
"use strict";
|
|
1125
1111
|
const createTicker = ${createTicker.toString()};
|
|
1112
|
+
const createSerialQueue = ${createSerialQueue.toString()};
|
|
1126
1113
|
const spriteClassForObject = ${spriteClassForObject.toString()};
|
|
1127
1114
|
const visibleRoomOf = ${visibleRoomOf.toString()};
|
|
1128
1115
|
const roomSceneObjects = ${roomSceneObjects.toString()};
|
|
@@ -1130,7 +1117,6 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1130
1117
|
const roomSceneLayout = ${roomSceneLayout.toString()};
|
|
1131
1118
|
const roomKindForRoom = ${roomKindForRoom.toString()};
|
|
1132
1119
|
const carriedItems = ${carriedItems.toString()};
|
|
1133
|
-
const visitedRoomGraph = ${visitedRoomGraph.toString()};
|
|
1134
1120
|
const allRoomIds = ${allRoomIds.toString()};
|
|
1135
1121
|
const groundedPlaceholder = ${groundedPlaceholder.toString()};
|
|
1136
1122
|
const exitDoorways = ${exitDoorways.toString()};
|
|
@@ -1138,6 +1124,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1138
1124
|
const factsForSubject = ${factsForSubject.toString()};
|
|
1139
1125
|
const renderWorldEditorText = ${renderWorldEditorText.toString()};
|
|
1140
1126
|
const wordBeforeCursor = ${wordBeforeCursor.toString()};
|
|
1127
|
+
const rowsForWorld = ${rowsForWorld.toString()};
|
|
1141
1128
|
const esc = ${escapeHtml.toString()};
|
|
1142
1129
|
// The identity Ashcombe Hall's own world facts place as the carrier/viewer
|
|
1143
1130
|
// — one named constant standing in for what was six separate "player"
|
|
@@ -1212,7 +1199,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1212
1199
|
let allStoreRows = [];
|
|
1213
1200
|
|
|
1214
1201
|
// ---- persistence — the manor remembers your visit, on this device -------
|
|
1215
|
-
// Best-effort IndexedDB (
|
|
1202
|
+
// Best-effort IndexedDB (tmct.page.openPersistedStore): after any
|
|
1216
1203
|
// state-changing redraw, the whole Backend-B payload plus the visited-room
|
|
1217
1204
|
// exposure set snapshots under the "adventure" key, debounced. Restored on
|
|
1218
1205
|
// boot; reset clears it and starts the world over. The home page's preview
|
|
@@ -1258,13 +1245,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1258
1245
|
|
|
1259
1246
|
// ---- serialize every engine-touching call: the ticker, the chat dock and
|
|
1260
1247
|
// the editor sync all share one in-memory store, and any overlapping pair
|
|
1261
|
-
// could race against the same write.
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
lock = run.catch(() => {});
|
|
1266
|
-
return run;
|
|
1267
|
-
}
|
|
1248
|
+
// could race against the same write. createSerialQueue is the shared
|
|
1249
|
+
// primitive mud-viz.mjs's own tickChain/serializeTick and spider-fly-viz.mjs's
|
|
1250
|
+
// own inlined withLock each duplicated under a different name.
|
|
1251
|
+
const { run: withLock } = createSerialQueue();
|
|
1268
1252
|
|
|
1269
1253
|
// ---- large-sprite-tier wiring — the gradient-shaded 400px tier
|
|
1270
1254
|
// (data/sprites-large/*.toml) arrives embedded at build time as
|
|
@@ -1313,22 +1297,22 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1313
1297
|
|
|
1314
1298
|
function captionFor(rows, state, here) {
|
|
1315
1299
|
const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
|
|
1316
|
-
const lines =
|
|
1300
|
+
const lines = tmct.page.worldDigestRows(rows, state)
|
|
1317
1301
|
.filter((row) => row.subject !== "Player" && (row.object === here || row.subject === hereCased))
|
|
1318
1302
|
.map((row) => row.subject + " " + row.predicate + " " + row.object + ".");
|
|
1319
1303
|
return lines.length ? lines.join(" ") : "Nothing more about the " + here + " is written down yet.";
|
|
1320
1304
|
}
|
|
1321
1305
|
|
|
1322
1306
|
// ---- goal status — mirrors adventure-viz.mjs's own goalStatusLines
|
|
1323
|
-
// against
|
|
1307
|
+
// against tmct.page.foldWorldState/tmct.page.exposedFacts (the
|
|
1324
1308
|
// same mirroring captionFor/pillsFor already do against their own
|
|
1325
1309
|
// adventure.mjs exports), so a location is never claimed before the room
|
|
1326
1310
|
// holding it has actually been visited this session.
|
|
1327
1311
|
function goalStatusLinesFor(rows, state, visitedRoomIds) {
|
|
1328
1312
|
const ids = Array.from(new Set(rows.filter((r) => r.predicate === "mgx:is-objective" && r.object === "true").map((r) => r.subject)));
|
|
1329
1313
|
const carriedIds = new Set(carriedItems(rows, state, ACTING_SUBJECT).map((o) => o.subject));
|
|
1330
|
-
const exposedRows =
|
|
1331
|
-
const exposedState =
|
|
1314
|
+
const exposedRows = tmct.page.exposedFacts(rows, visitedRoomIds);
|
|
1315
|
+
const exposedState = tmct.page.foldWorldState(exposedRows);
|
|
1332
1316
|
return ids.map((id) => {
|
|
1333
1317
|
if (carriedIds.has(id)) return { subject: id, status: "carried", text: "carrying the " + id + " \\u2014 the adventure is won." };
|
|
1334
1318
|
const room = visibleRoomOf(exposedRows, exposedState, id, ACTING_SUBJECT);
|
|
@@ -1349,53 +1333,39 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1349
1333
|
}
|
|
1350
1334
|
|
|
1351
1335
|
// ---- room map — ONE svg-building routine shared by play mode's visited-
|
|
1352
|
-
// only map and edit mode's whole-map (
|
|
1336
|
+
// only map and edit mode's whole-map (visitedRoomGraphFor fed allRoomIds
|
|
1353
1337
|
// instead of the exposure set — a parameter, not a second layout), so the
|
|
1354
1338
|
// fixed-size-viewport CSS treatment and the node layout can never drift
|
|
1355
1339
|
// between the two. clickable adds a data-room attribute and a pointer
|
|
1356
1340
|
// cursor per node; play mode's own map stays purely informational.
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const HINT_DELTA = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], up: [0, -1], down: [0, 1] };
|
|
1375
|
-
const hintsSvg = graph.hints.map((hi) => {
|
|
1376
|
-
const from = byRoom.get(hi.from);
|
|
1377
|
-
const d = HINT_DELTA[hi.direction] || [0, 0];
|
|
1378
|
-
return '<circle class="room-hint" cx="' + (cx(from) + d[0] * cell * 0.42) + '" cy="' + (cy(from) + d[1] * cell * 0.42) + '" r="3.5"></circle>';
|
|
1379
|
-
}).join("");
|
|
1380
|
-
const nodesSvg = graph.nodes.map((n) => {
|
|
1381
|
-
const cls = "room-node" + (n.current ? " current" : "") + (clickable ? " clickable" : "") + (clickable && n.id === selectedRoomId ? " selected" : "");
|
|
1382
|
-
const attr = clickable ? ' data-room="' + esc(n.id) + '"' : "";
|
|
1383
|
-
return '<g class="' + cls + '"' + attr + '><rect x="' + (cx(n) - roomW / 2) + '" y="' + (cy(n) - roomH / 2) + '" width="' + roomW + '" height="' + roomH + '" rx="3"></rect>'
|
|
1384
|
-
+ '<text x="' + cx(n) + '" y="' + (cy(n) + 2.5) + '">' + esc(n.id) + "</text></g>";
|
|
1385
|
-
}).join("");
|
|
1386
|
-
return '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="xMidYMid meet" role="img" aria-label="' + (clickable ? "the whole manor \\u2014 click a room to inspect it" : "the rooms visited so far") + '">'
|
|
1387
|
-
+ edgesSvg + hintsSvg + nodesSvg + "</svg>";
|
|
1341
|
+
// roomGraphSvgFor mirrors the OLD inline roomMapSvg's own board-game sizing
|
|
1342
|
+
// (a 64px square cell, 56x26px room footprints) through the shared
|
|
1343
|
+
// viz-room-graph.mjs renderer, reached via the tmct surface the
|
|
1344
|
+
// same way captionFor/pillsFor already reach their own adventure.mjs
|
|
1345
|
+
// calls: roomGraphSvg needs escapeHtml and a module-level exit-delta table
|
|
1346
|
+
// this splice-safe script carries neither of, so it runs through the
|
|
1347
|
+
// bundle rather than being spliced as text (see this page's own module
|
|
1348
|
+
// header). visitedRoomGraphFor is the same posture for the layout half.
|
|
1349
|
+
function roomGraphSvgFor(graph, clickable) {
|
|
1350
|
+
return tmct.page.roomGraphSvg(graph, {
|
|
1351
|
+
cellX: 64, cellY: 64, roomW: 56, roomH: 26,
|
|
1352
|
+
clickable, selectedRoomId,
|
|
1353
|
+
label: clickable ? "the whole manor \\u2014 click a room to inspect it" : "the rooms visited so far",
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
function visitedRoomGraphFor(state, visitedIds) {
|
|
1357
|
+
return tmct.page.directedGridLayout(state, visitedIds, { actingSubject: ACTING_SUBJECT });
|
|
1388
1358
|
}
|
|
1389
1359
|
function renderRoomMap(rows, state, visitedRoomIds) {
|
|
1390
|
-
mapWrapEl.innerHTML =
|
|
1360
|
+
mapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, visitedRoomIds), false) || '<span class="empty-note">nowhere yet</span>';
|
|
1391
1361
|
}
|
|
1392
1362
|
|
|
1393
1363
|
// ---- the map lightbox — clicking the fixed-square play-mode map redraws
|
|
1394
|
-
// the SAME
|
|
1395
|
-
// backdrop (not the board itself) or pressing Escape closes it.
|
|
1364
|
+
// the SAME roomGraphSvgFor output larger, over a dimmed backdrop; clicking
|
|
1365
|
+
// the backdrop (not the board itself) or pressing Escape closes it.
|
|
1396
1366
|
function openMapLightbox() {
|
|
1397
1367
|
if (!lastSnapshot) return;
|
|
1398
|
-
const svg =
|
|
1368
|
+
const svg = roomGraphSvgFor(visitedRoomGraphFor(lastSnapshot.state, lastSnapshot.visitedRoomIds), false);
|
|
1399
1369
|
if (!svg) return;
|
|
1400
1370
|
mapLightboxInnerEl.innerHTML = svg;
|
|
1401
1371
|
mapLightboxEl.hidden = false;
|
|
@@ -1447,7 +1417,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1447
1417
|
addChatLine("u", esc(line));
|
|
1448
1418
|
addObjDockLine("u", esc(line));
|
|
1449
1419
|
return withLock(async () => {
|
|
1450
|
-
const result = await
|
|
1420
|
+
const result = await tmct.turn(line);
|
|
1451
1421
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1452
1422
|
addObjDockLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1453
1423
|
const snap = await session.snapshot();
|
|
@@ -1472,7 +1442,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1472
1442
|
// nothing, so it needs no main-transcript echo — it IS the board's own
|
|
1473
1443
|
// content); pill/typed turns below do echo, since they can change state.
|
|
1474
1444
|
await withLock(async () => {
|
|
1475
|
-
const result = await
|
|
1445
|
+
const result = await tmct.turn("look " + subject);
|
|
1476
1446
|
objLookEl.textContent = result.answer;
|
|
1477
1447
|
});
|
|
1478
1448
|
}
|
|
@@ -1503,7 +1473,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1503
1473
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !objLightboxEl.hidden) closeObjectLightbox(); });
|
|
1504
1474
|
|
|
1505
1475
|
function renderEditMap(rows, state) {
|
|
1506
|
-
editMapWrapEl.innerHTML =
|
|
1476
|
+
editMapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, allRoomIds(rows)), true) || '<span class="empty-note">this world defines no rooms</span>';
|
|
1507
1477
|
}
|
|
1508
1478
|
editMapWrapEl.addEventListener("click", (e) => {
|
|
1509
1479
|
const g = e.target.closest("[data-room]");
|
|
@@ -1535,8 +1505,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1535
1505
|
|
|
1536
1506
|
// ---- contextual pills — refreshed every redraw from the CURRENT room's
|
|
1537
1507
|
// own roomAffordances-derived list (mirroring pillsForRoom against
|
|
1538
|
-
//
|
|
1539
|
-
// roomCaptionText against
|
|
1508
|
+
// tmct.page.roomAffordances, the same way captionFor above mirrors
|
|
1509
|
+
// roomCaptionText against tmct.page.worldDigestRows), so a pill for a
|
|
1540
1510
|
// room the player has left, or an object already taken, never lingers.
|
|
1541
1511
|
// Clicking one inserts its exact command text into the input and focuses
|
|
1542
1512
|
// it; it never auto-submits, so free typing still works and a clicked
|
|
@@ -1547,7 +1517,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1547
1517
|
// ever wants to edit before running, and drawing it in both places would
|
|
1548
1518
|
// make one control a suggestion and its twin an action.
|
|
1549
1519
|
function pillsFor(rows, state, here) {
|
|
1550
|
-
return
|
|
1520
|
+
return tmct.page.roomAffordances(rows, state, here);
|
|
1551
1521
|
}
|
|
1552
1522
|
function renderPills(rows, state, here) {
|
|
1553
1523
|
const actions = pillsFor(rows, state, here).filter((a) => a.indexOf("go ") !== 0);
|
|
@@ -1590,10 +1560,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1590
1560
|
const ROOT_FALLBACK_BY_CLASS = { adventurer: "person", person: "person", room: "room" };
|
|
1591
1561
|
const rootFallbackFor = (cls) => ROOT_FALLBACK_BY_CLASS[cls] || "portable";
|
|
1592
1562
|
function resolveObjectSprite(rows, s) {
|
|
1593
|
-
if (s.subject === "you") return
|
|
1594
|
-
return
|
|
1563
|
+
if (s.subject === "you") return tmct.page.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmct.page.SPRITE_REGISTRY, { rootFallback: "person", instanceKey: "you" });
|
|
1564
|
+
return tmct.page.resolveSpriteAsset(
|
|
1595
1565
|
s.subject, spriteAncestryRows(rows, s.subject), factsForSubject(rows, s.subject),
|
|
1596
|
-
activeSpriteTemplates,
|
|
1566
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1597
1567
|
{ rootFallback: rootFallbackFor(s.spriteClass), instanceKey: s.subject },
|
|
1598
1568
|
);
|
|
1599
1569
|
}
|
|
@@ -1604,9 +1574,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1604
1574
|
// everything else falls back through room's own ancestor chain to the
|
|
1605
1575
|
// generic room icon.
|
|
1606
1576
|
function roomKindIconSvg(rows, here) {
|
|
1607
|
-
return
|
|
1577
|
+
return tmct.page.resolveSpriteAsset(
|
|
1608
1578
|
here, spriteAncestryRows(rows, here), factsForSubject(rows, here),
|
|
1609
|
-
activeSpriteTemplates,
|
|
1579
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1610
1580
|
{ rootFallback: "room", instanceKey: "roomkind-" + here },
|
|
1611
1581
|
);
|
|
1612
1582
|
}
|
|
@@ -1671,7 +1641,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1671
1641
|
ticker.pause();
|
|
1672
1642
|
addChatLine("u", esc(line));
|
|
1673
1643
|
return withLock(async () => {
|
|
1674
|
-
const result = await
|
|
1644
|
+
const result = await tmct.turn(line);
|
|
1675
1645
|
addChatLine("a", esc(result.answer).replace(/\\n/g, "<br>"));
|
|
1676
1646
|
const snap = await session.snapshot();
|
|
1677
1647
|
redraw(snap);
|
|
@@ -1701,8 +1671,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1701
1671
|
// diff + write; this page only ever reads its result back.
|
|
1702
1672
|
|
|
1703
1673
|
function worldOnlyRows(rows) {
|
|
1704
|
-
|
|
1705
|
-
return (rows || []).filter((r) => typeof r.provenance === "string" && r.provenance.indexOf(prefix) === 0);
|
|
1674
|
+
return rowsForWorld(rows, world().name);
|
|
1706
1675
|
}
|
|
1707
1676
|
|
|
1708
1677
|
function renderRoomDetail() {
|
|
@@ -1734,9 +1703,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1734
1703
|
const classes = new Set(["adventurer"]);
|
|
1735
1704
|
subjects.forEach((s) => { if (s !== ACTING_SUBJECT) classes.add(spriteClassForObject(rows, s)); });
|
|
1736
1705
|
legendListEl.innerHTML = Array.from(classes).sort().map((cls) => {
|
|
1737
|
-
const svg =
|
|
1706
|
+
const svg = tmct.page.resolveSpriteAsset(
|
|
1738
1707
|
cls, spriteAncestryRows(rows, cls), factsForSubject(rows, cls),
|
|
1739
|
-
activeSpriteTemplates,
|
|
1708
|
+
activeSpriteTemplates, tmct.page.SPRITE_REGISTRY,
|
|
1740
1709
|
{ rootFallback: rootFallbackFor(cls), instanceKey: "legend-" + cls },
|
|
1741
1710
|
);
|
|
1742
1711
|
return spriteCardHtml(cls, cls, svg);
|
|
@@ -1755,8 +1724,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1755
1724
|
}
|
|
1756
1725
|
|
|
1757
1726
|
// ---- cursor-driven suggestion pills — the lateral SKOS neighbourhood
|
|
1758
|
-
// (
|
|
1759
|
-
// (
|
|
1727
|
+
// (tmct.page.relatedForTerm) plus the vertical is-a ancestor chain
|
|
1728
|
+
// (tmct.page.classAncestorChain), mirroring adventure-viz.mjs's own
|
|
1760
1729
|
// suggestionsForTerm against the browser bundle's global (the same
|
|
1761
1730
|
// reach-through-the-global pattern captionFor/pillsFor already use).
|
|
1762
1731
|
// Reads the FULL store (allStoreRows), not worldOnlyRows — a term's
|
|
@@ -1765,8 +1734,8 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1765
1734
|
function renderSuggestionPills() {
|
|
1766
1735
|
const term = wordBeforeCursor(editorTextEl.value, editorTextEl.selectionStart);
|
|
1767
1736
|
if (!term) { editorPillsEl.innerHTML = ""; return; }
|
|
1768
|
-
const related =
|
|
1769
|
-
const chain =
|
|
1737
|
+
const related = tmct.page.relatedForTerm(allStoreRows, term);
|
|
1738
|
+
const chain = tmct.page.classAncestorChain(term, allStoreRows);
|
|
1770
1739
|
const seen = new Set([term]);
|
|
1771
1740
|
const out = [];
|
|
1772
1741
|
const push = (label) => { if (label && !seen.has(label)) { seen.add(label); out.push(label); } };
|
|
@@ -1797,7 +1766,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1797
1766
|
const snap = await session.snapshot();
|
|
1798
1767
|
allStoreRows = snap.rows;
|
|
1799
1768
|
editRows = worldOnlyRows(snap.rows);
|
|
1800
|
-
editState =
|
|
1769
|
+
editState = tmct.page.foldWorldState(editRows);
|
|
1801
1770
|
if (result.unrecognized.length) {
|
|
1802
1771
|
const lines = result.unrecognized.map((u) => u.line).join(", ");
|
|
1803
1772
|
editorStatusEl.className = "edit-status pending";
|
|
@@ -1828,7 +1797,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1828
1797
|
const snap = await session.snapshot();
|
|
1829
1798
|
allStoreRows = snap.rows;
|
|
1830
1799
|
editRows = worldOnlyRows(snap.rows);
|
|
1831
|
-
editState =
|
|
1800
|
+
editState = tmct.page.foldWorldState(editRows);
|
|
1832
1801
|
editorTextEl.value = renderWorldEditorText(editRows, editState);
|
|
1833
1802
|
editorStatusEl.className = "edit-status";
|
|
1834
1803
|
editorStatusEl.textContent = "";
|
|
@@ -1849,7 +1818,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1849
1818
|
|
|
1850
1819
|
async function boot({ fresh = false } = {}) {
|
|
1851
1820
|
const saved = !fresh && persist ? await persist.load() : null;
|
|
1852
|
-
session = await
|
|
1821
|
+
session = await tmct.open(
|
|
1853
1822
|
world(),
|
|
1854
1823
|
saved && saved.payload && saved.payload.memoryPayload
|
|
1855
1824
|
? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
|
|
@@ -1908,9 +1877,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1908
1877
|
// to. Without that, one world's snapshot would restore into another's graph.
|
|
1909
1878
|
let siteVersion = "";
|
|
1910
1879
|
async function openPersistFor(worldName) {
|
|
1911
|
-
if (preview || !
|
|
1880
|
+
if (preview || !tmct.page.openPersistedStore) return;
|
|
1912
1881
|
if (!siteVersion) siteVersion = await fetchSiteVersion();
|
|
1913
|
-
persist =
|
|
1882
|
+
persist = tmct.page.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + worldName });
|
|
1914
1883
|
}
|
|
1915
1884
|
|
|
1916
1885
|
if (scenarioSelectEl) {
|