@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.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/README.md +178 -9
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +8 -8
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/sprites/src/sprite-facts.jsonl +401 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +49 -9
- package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
- package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
- package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
- package/corpus/worlds/src/mud-hollow.jsonl +82 -0
- package/corpus/worlds/src/mud-warren.jsonl +124 -0
- package/corpus/worlds/src/spider-fly.jsonl +1 -1
- package/data/sprites/animal-icon.toml +11 -5
- package/data/sprites/book-icon.toml +9 -5
- package/data/sprites/cabinet-icon.toml +10 -5
- package/data/sprites/cellar-icon.toml +16 -6
- package/data/sprites/container-icon.toml +7 -3
- package/data/sprites/desk-icon.toml +8 -5
- package/data/sprites/dog-icon.toml +8 -5
- package/data/sprites/dog-with-colour-icon.toml +13 -12
- package/data/sprites/drawing-room-icon.toml +14 -5
- package/data/sprites/egg-icon.toml +6 -3
- package/data/sprites/fly-icon.toml +10 -5
- package/data/sprites/furniture-icon.toml +5 -3
- package/data/sprites/garden-icon.toml +10 -3
- package/data/sprites/key-icon.toml +3 -1
- package/data/sprites/kitchen-icon.toml +15 -6
- package/data/sprites/lamp-icon.toml +9 -4
- package/data/sprites/letter-icon.toml +7 -4
- package/data/sprites/library-icon.toml +14 -3
- package/data/sprites/pan-icon.toml +7 -3
- package/data/sprites/person-icon.toml +6 -2
- package/data/sprites/poodle-icon.toml +1 -1
- package/data/sprites/portable-icon.toml +6 -4
- package/data/sprites/portrait-icon.toml +7 -4
- package/data/sprites/room-icon.toml +11 -2
- package/data/sprites/spider-icon.toml +9 -3
- package/data/sprites/study-icon.toml +10 -2
- package/package.json +5 -4
- 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/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +18 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/reference-pack.mjs +31 -7
- 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 +37 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +169 -20
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +209 -157
- package/src/services/adventure.mjs +526 -391
- package/src/services/chat-page-viz.mjs +69 -25
- package/src/services/chat.mjs +200 -51
- package/src/services/code-explorer-viz.mjs +102 -62
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +68 -82
- package/src/services/ledger-viz.mjs +136 -67
- package/src/services/memory-panel-viz.mjs +62 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +1016 -208
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +123 -64
- package/src/services/research-viz.mjs +160 -108
- package/src/services/spider-fly-turn.mjs +15 -23
- package/src/services/spider-fly-viz.mjs +146 -161
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +414 -240
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +90 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +71 -29
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -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 +25 -1
|
@@ -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 `tmctAdventure` global (mirroring
|
|
46
|
+
// how the inline script calls `tmctSpiderFly.*` rather than re-importing
|
|
47
|
+
// spider-fly-world.mjs): `roomCaptionText` (calls `worldDigestRows`; the
|
|
48
|
+
// in-page `captionFor` mirrors it against `tmctAdventure.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 `tmctAdventure.roomAffordances`),
|
|
53
|
+
// `goalStatusLines` (calls `foldWorldState` and adventure-autoplay.mjs's own
|
|
54
|
+
// `exposedFacts`; the in-page `goalStatusLinesFor` mirrors both against the
|
|
55
|
+
// `tmctAdventure` global 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
|
+
// `tmctAdventure.directedGridLayout`/`tmctAdventure.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 } 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;
|
|
@@ -168,15 +177,21 @@ export function factsForSubject(rows, subject) {
|
|
|
168
177
|
* feeding it an exposure-filtered pair (as `goalStatusLines` does) is what
|
|
169
178
|
* keeps a not-yet-visited location unknown. Pure, self-contained — no
|
|
170
179
|
* reference to adventure.mjs's own private helpers, since those aren't
|
|
171
|
-
* exported.
|
|
172
|
-
|
|
180
|
+
* exported. `actingSubject` is the identity the world places as the
|
|
181
|
+
* carrier/viewer — a parameter, not a baked-in name, the same way
|
|
182
|
+
* adventure.mjs's own `runWorldCommand`/`roomAffordances` take it; it
|
|
183
|
+
* defaults to "player" because that's the one identity Ashcombe Hall's own
|
|
184
|
+
* world facts actually use, not because this function only ever means
|
|
185
|
+
* that individual (mud-viz.mjs threads its own per-character viewer through
|
|
186
|
+
* the same shape, see `mudRoomSceneObjects`/`carriedItemsFor`). */
|
|
187
|
+
export function visibleRoomOf(rows, state, subject, actingSubject = "player") {
|
|
173
188
|
const isRoom = (id) => (rows || []).some((r) => r.subject === id && r.predicate === "rdf:type" && r.object === "room");
|
|
174
189
|
if (isRoom(subject)) return subject;
|
|
175
190
|
const place = state.placements.get(subject);
|
|
176
191
|
if (!place || place.predicate === "mgx:hidden-in") return null;
|
|
177
192
|
if (place.predicate === "mgx:currently-in" || isRoom(place.object)) return place.object;
|
|
178
193
|
const holder = place.object;
|
|
179
|
-
if (holder ===
|
|
194
|
+
if (holder === actingSubject) return null;
|
|
180
195
|
if (!state.openness.get(holder)?.open) return null;
|
|
181
196
|
const holderPlace = state.placements.get(holder);
|
|
182
197
|
return holderPlace && holderPlace.predicate !== "mgx:hidden-in" ? holderPlace.object : null;
|
|
@@ -185,13 +200,14 @@ export function visibleRoomOf(rows, state, subject) {
|
|
|
185
200
|
/** Every subject actually visible in `here`, sorted, each with its sprite
|
|
186
201
|
* class — built over `visibleRoomOf` (one containment hop through an OPEN
|
|
187
202
|
* container) so this can never draw a hidden or carried object the text
|
|
188
|
-
* digest wouldn't also mention. `
|
|
189
|
-
* player's own adventurer sprite
|
|
190
|
-
|
|
203
|
+
* digest wouldn't also mention. `actingSubject` is excluded (default
|
|
204
|
+
* "player"); the caller draws that individual's own adventurer sprite
|
|
205
|
+
* separately. Pure. */
|
|
206
|
+
export function roomSceneObjects(rows, state, here, actingSubject = "player") {
|
|
191
207
|
const out = [];
|
|
192
208
|
for (const subject of [...state.placements.keys()].sort()) {
|
|
193
|
-
if (subject ===
|
|
194
|
-
if (visibleRoomOf(rows, state, subject) !== here) continue;
|
|
209
|
+
if (subject === actingSubject) continue;
|
|
210
|
+
if (visibleRoomOf(rows, state, subject, actingSubject) !== here) continue;
|
|
195
211
|
out.push({ subject, spriteClass: spriteClassForObject(rows, subject) });
|
|
196
212
|
}
|
|
197
213
|
return out;
|
|
@@ -329,13 +345,16 @@ export function roomKindForRoom(rows, roomId) {
|
|
|
329
345
|
return "indoor";
|
|
330
346
|
}
|
|
331
347
|
|
|
332
|
-
/** Every object currently `mgx:located-in` "player",
|
|
333
|
-
* sprite class — the exact placement
|
|
334
|
-
* own "carries the" branch already
|
|
335
|
-
* instead of prose.
|
|
336
|
-
|
|
348
|
+
/** Every object currently `mgx:located-in` `holder` (default "player"),
|
|
349
|
+
* sorted, each with its sprite class — the exact placement
|
|
350
|
+
* `worldDigestRows`'/`inventoryAnswer`'s own "carries the" branch already
|
|
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") {
|
|
337
356
|
return [...state.placements]
|
|
338
|
-
.filter(([, p]) => p.predicate === "mgx:located-in" && p.object ===
|
|
357
|
+
.filter(([, p]) => p.predicate === "mgx:located-in" && p.object === holder)
|
|
339
358
|
.map(([subject]) => ({ subject, spriteClass: spriteClassForObject(rows, subject) }))
|
|
340
359
|
.sort((a, b) => a.subject.localeCompare(b.subject));
|
|
341
360
|
}
|
|
@@ -354,47 +373,13 @@ export function carriedItems(rows, state) {
|
|
|
354
373
|
* caller can draw at most "there's an exit that way" and nothing more.
|
|
355
374
|
* Disconnected visited rooms (not reachable from each other by traveled
|
|
356
375
|
* edges) lay out as separate side-by-side blocks rather than overlapping.
|
|
357
|
-
*
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const edgeKeys = new Set();
|
|
365
|
-
const hints = [];
|
|
366
|
-
let offsetX = 0;
|
|
367
|
-
for (const start of [...visited].sort()) {
|
|
368
|
-
if (positions.has(start)) continue;
|
|
369
|
-
positions.set(start, { x: offsetX, y: 0 });
|
|
370
|
-
const queue = [start];
|
|
371
|
-
const component = [start];
|
|
372
|
-
while (queue.length) {
|
|
373
|
-
const room = queue.shift();
|
|
374
|
-
const pos = positions.get(room);
|
|
375
|
-
const dirs = state.exits.get(room);
|
|
376
|
-
for (const direction of [...(dirs?.keys() ?? [])].sort()) {
|
|
377
|
-
const target = dirs.get(direction);
|
|
378
|
-
if (!visited.has(target)) { hints.push({ from: room, direction }); continue; }
|
|
379
|
-
const key = [room, target].sort().join("\0");
|
|
380
|
-
if (!edgeKeys.has(key)) { edgeKeys.add(key); edges.push({ from: room, to: target, direction }); }
|
|
381
|
-
if (!positions.has(target)) {
|
|
382
|
-
const [dx, dy] = DELTA[direction] ?? [0, 0];
|
|
383
|
-
positions.set(target, { x: pos.x + dx, y: pos.y + dy });
|
|
384
|
-
component.push(target);
|
|
385
|
-
queue.push(target);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
offsetX = Math.max(...component.map((r) => positions.get(r).x)) + 2;
|
|
390
|
-
}
|
|
391
|
-
const minX = Math.min(0, ...[...positions.values()].map((p) => p.x));
|
|
392
|
-
const minY = Math.min(0, ...[...positions.values()].map((p) => p.y));
|
|
393
|
-
const nodes = [...visited].sort().map((room) => {
|
|
394
|
-
const p = positions.get(room) || { x: 0, y: 0 };
|
|
395
|
-
return { id: room, x: p.x - minX, y: p.y - minY, current: room === here };
|
|
396
|
-
});
|
|
397
|
-
return { nodes, edges, hints };
|
|
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. */
|
|
381
|
+
export function visitedRoomGraph(state, visitedRoomIds, actingSubject = "player") {
|
|
382
|
+
return directedGridLayout(state, visitedRoomIds, { actingSubject });
|
|
398
383
|
}
|
|
399
384
|
|
|
400
385
|
/** Every room the world DEFINES at all (every subject the fact rows type as
|
|
@@ -521,11 +506,19 @@ export function suggestionsForTerm(rows, term) {
|
|
|
521
506
|
* itself (its own exits) or about something placed IN it — the same
|
|
522
507
|
* "visible here" boundary `roomSceneObjects` draws from. The player's own
|
|
523
508
|
* "is in the" row is excluded: the room frame already IS the current room,
|
|
524
|
-
* so restating "you are here" is redundant, never informative.
|
|
525
|
-
|
|
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 } = {}) {
|
|
526
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);
|
|
527
520
|
const lines = worldDigestRows(rows, state)
|
|
528
|
-
.filter((row) => row.subject !== "Player" && (row.object
|
|
521
|
+
.filter((row) => row.subject !== "Player" && (objectMatches(row.object) || subjectMatches(row.subject)))
|
|
529
522
|
.map((row) => `${row.subject} ${row.predicate} ${row.object}.`);
|
|
530
523
|
return lines.length ? lines.join(" ") : `Nothing more about the ${here} is written down yet.`;
|
|
531
524
|
}
|
|
@@ -558,12 +551,16 @@ export function roomCaptionText(rows, state, here) {
|
|
|
558
551
|
export function renderAdventureHtml({
|
|
559
552
|
title = DEFAULT_TITLE,
|
|
560
553
|
worldPayload = { facts: [], rules: [], opening: "" },
|
|
554
|
+
scenarios = [],
|
|
561
555
|
spriteTemplates = [],
|
|
562
556
|
largeSpriteTemplates = [],
|
|
563
557
|
engineBundleJs = "",
|
|
564
558
|
} = {}) {
|
|
559
|
+
const scenarioList = scenarios.length
|
|
560
|
+
? scenarios
|
|
561
|
+
: [{ label: scenarioLabel(worldPayload?.name), worldPayload }];
|
|
565
562
|
const pageData = embedJson({
|
|
566
|
-
|
|
563
|
+
scenarios: scenarioList,
|
|
567
564
|
previewMaxTicks: PREVIEW_MAX_TICKS,
|
|
568
565
|
tickWaitMs: TICK_WAIT_MS,
|
|
569
566
|
spriteTemplates,
|
|
@@ -726,31 +723,56 @@ ${THEME_TOKENS_CSS}
|
|
|
726
723
|
the frame so neither fights the plaque or the room-kind icon; west and
|
|
727
724
|
east sit just inside their own walls. Stairs get a round chip instead —
|
|
728
725
|
neither up nor down is a compass point, and neither hangs on a wall. */
|
|
729
|
-
.room-stage { position: relative; padding: 1.
|
|
726
|
+
.room-stage { position: relative; padding: 1.85rem 0; }
|
|
727
|
+
/* the "ways out" eyebrow — this ring is the ONLY place movement controls
|
|
728
|
+
exist on the page (the pill row deliberately drops every "go", see
|
|
729
|
+
renderPills), so a first-time player needs a named affordance here, not
|
|
730
|
+
just brass plates they have to guess at. */
|
|
731
|
+
.ways-out-label {
|
|
732
|
+
font-family: ${MONO_STACK}; font-size: .68rem; letter-spacing: .14em; text-transform: uppercase;
|
|
733
|
+
color: var(--gilt); opacity: .85; margin: 0 0 -.3rem .1rem;
|
|
734
|
+
}
|
|
730
735
|
.dir-ring { position: absolute; inset: 0; pointer-events: none; }
|
|
731
736
|
.dir-slot { position: absolute; pointer-events: auto; }
|
|
732
737
|
.dir-north { top: 0; left: 50%; transform: translateX(-50%); }
|
|
733
738
|
.dir-south { bottom: 0; left: 50%; transform: translateX(-50%); }
|
|
734
|
-
.dir-west { left: .
|
|
735
|
-
.dir-east { right: .
|
|
739
|
+
.dir-west { left: .4rem; top: 50%; transform: translateY(-50%); }
|
|
740
|
+
.dir-east { right: .4rem; top: 50%; transform: translateY(-50%); }
|
|
736
741
|
/* the stairs stand at the left of each band: the frame's own corner
|
|
737
742
|
flourishes sit top-left and bottom-right, and this is the one inset that
|
|
738
743
|
clears both. */
|
|
739
|
-
.dir-up { top: 0; left: 1.
|
|
740
|
-
.dir-down { bottom: 0; left: 1.
|
|
744
|
+
.dir-up { top: 0; left: 1.5rem; }
|
|
745
|
+
.dir-down { bottom: 0; left: 1.5rem; }
|
|
746
|
+
/* a brass PUSH-plate, not a name plate: real button chrome (an outer drop
|
|
747
|
+
shadow, not just the inset highlight the static room/door plaques use),
|
|
748
|
+
a bigger touch target, and a lift on hover/press — so this reads as
|
|
749
|
+
clickable at a glance instead of blending into the wallpaper the way a
|
|
750
|
+
plain brass label would. The leading triangle is a direction-neutral
|
|
751
|
+
"go" cue, the same role an arrow plays on any door-push icon. */
|
|
741
752
|
.dir-door {
|
|
742
|
-
position: relative; display: inline-flex; align-items: center; justify-content: center;
|
|
743
|
-
font-family: ${MONO_STACK}; font-size: .
|
|
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: .
|
|
753
|
+
position: relative; display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
|
|
754
|
+
font-family: ${MONO_STACK}; font-size: .74rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
|
|
755
|
+
color: var(--gilt); background: var(--parchment); border: 1px solid var(--gilt); border-radius: 3px;
|
|
756
|
+
box-shadow: inset 0 0 0 2px var(--parchment-strong), 0 2px 4px rgba(0, 0, 0, .25);
|
|
757
|
+
padding: .5rem .95rem; white-space: nowrap;
|
|
758
|
+
transition: transform .08s ease, box-shadow .08s ease, background .08s ease, color .08s ease;
|
|
759
|
+
}
|
|
760
|
+
.dir-door::before { content: "\\25B8"; font-size: .62em; opacity: .9; }
|
|
761
|
+
.dir-door:hover, .dir-door:focus-visible {
|
|
762
|
+
background: var(--gilt); color: var(--parchment);
|
|
763
|
+
box-shadow: inset 0 0 0 2px var(--parchment-strong), 0 4px 7px rgba(0, 0, 0, .32);
|
|
764
|
+
transform: translateY(-1.5px);
|
|
747
765
|
}
|
|
748
|
-
.dir-door:
|
|
749
|
-
|
|
766
|
+
.dir-door:active {
|
|
767
|
+
transform: translateY(0);
|
|
768
|
+
box-shadow: inset 0 0 0 2px var(--parchment-strong), 0 1px 2px rgba(0, 0, 0, .28);
|
|
769
|
+
}
|
|
770
|
+
.dir-door.stair { border-radius: 50%; width: 2.15rem; height: 2.15rem; padding: 0; font-size: .86rem; letter-spacing: 0; }
|
|
771
|
+
.dir-door.stair::before { content: ""; }
|
|
750
772
|
/* a door onto a room this session has not stood in yet, marked with the
|
|
751
773
|
same gilt dot the manor board prints for a direction it cannot draw a
|
|
752
774
|
room for — one hint shape, two surfaces. */
|
|
753
|
-
.dir-door .unwalked { position: absolute; top: -.
|
|
775
|
+
.dir-door .unwalked { position: absolute; top: -.2rem; right: -.2rem; width: .4rem; height: .4rem; border-radius: 50%; background: var(--gilt); border: 1px solid var(--parchment); }
|
|
754
776
|
|
|
755
777
|
.sprite-row { display: flex; align-items: flex-end; gap: .9rem .7rem; min-height: 2.5rem; }
|
|
756
778
|
/* the floor band: every non-wall-mounted stack, wrapping and pinned to the
|
|
@@ -850,7 +872,7 @@ ${THEME_TOKENS_CSS}
|
|
|
850
872
|
this just keeps the board's own footprint stable and click-to-enlarge
|
|
851
873
|
honest about what it's enlarging. */
|
|
852
874
|
.map-viewport-fixed { width: 190px; margin: 0 auto; cursor: zoom-in; }
|
|
853
|
-
/* the lights-down map lightbox — the same board, the same
|
|
875
|
+
/* the lights-down map lightbox — the same board, the same room-graph svg
|
|
854
876
|
output, just drawn bigger over a dimmed backdrop. Closes on a click
|
|
855
877
|
anywhere outside the enlarged board, or Escape. */
|
|
856
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); }
|
|
@@ -930,6 +952,9 @@ ${THEME_TOKENS_CSS}
|
|
|
930
952
|
.controls-row button { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; padding: .38rem .85rem; border: 1px solid var(--gilt); background: var(--parchment); color: var(--ink); }
|
|
931
953
|
.controls-row button:hover:not(:disabled) { background: var(--parchment-strong); }
|
|
932
954
|
.controls-row button:disabled { opacity: .4; cursor: default; }
|
|
955
|
+
.controls-row select { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; padding: .38rem .85rem; border: 1px solid var(--gilt); background: var(--parchment); color: var(--ink); }
|
|
956
|
+
.controls-row select:hover:not(:disabled) { background: var(--parchment-strong); }
|
|
957
|
+
.controls-row select:disabled { opacity: .4; cursor: default; }
|
|
933
958
|
.controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; color: var(--ink); background: var(--parchment); border: 1px solid var(--gilt); padding: .3rem .6rem; font-variant-numeric: tabular-nums; }
|
|
934
959
|
.goal-line { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); margin-top: .5rem; }
|
|
935
960
|
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .3rem; }
|
|
@@ -970,11 +995,14 @@ ${THEME_TOKENS_CSS}
|
|
|
970
995
|
<div class="eyebrow">tmct · the adventure</div>
|
|
971
996
|
<button id="editModeBtn" type="button" class="mode-toggle" disabled>edit the world</button>
|
|
972
997
|
</div>
|
|
973
|
-
<p class="page-note">${escapeHtml(worldPayload.opening)}</p>
|
|
998
|
+
<p class="page-note" id="pageNote">${escapeHtml(scenarioList[0].worldPayload.opening || "")}</p>
|
|
974
999
|
<div class="stage" id="playStage">
|
|
975
1000
|
<div class="stage-left">
|
|
976
1001
|
<div class="controls-row" id="playControls">
|
|
977
1002
|
<button id="resetBtn" type="button" disabled>reset</button>
|
|
1003
|
+
${scenarioList.length > 1 ? ` <select id="scenarioSelect" aria-label="which world to play" disabled>
|
|
1004
|
+
${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " selected" : ""}>${escapeHtml(s.label || scenarioLabel(s.worldPayload?.name))}</option>`).join("\n")}
|
|
1005
|
+
</select>` : ""}
|
|
978
1006
|
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
979
1007
|
<button id="stepBtn" type="button" disabled>step</button>
|
|
980
1008
|
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
@@ -989,6 +1017,7 @@ ${THEME_TOKENS_CSS}
|
|
|
989
1017
|
<h2>satchel</h2>
|
|
990
1018
|
<div class="chips" id="carryList"></div>
|
|
991
1019
|
</div>
|
|
1020
|
+
<div class="ways-out-label" aria-hidden="true">ways out</div>
|
|
992
1021
|
<div class="room-stage">
|
|
993
1022
|
<div class="room-frame" id="roomFrame">
|
|
994
1023
|
<div class="room-plaque mono" id="roomName"></div>
|
|
@@ -1080,6 +1109,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1080
1109
|
(function () {
|
|
1081
1110
|
"use strict";
|
|
1082
1111
|
const createTicker = ${createTicker.toString()};
|
|
1112
|
+
const createSerialQueue = ${createSerialQueue.toString()};
|
|
1083
1113
|
const spriteClassForObject = ${spriteClassForObject.toString()};
|
|
1084
1114
|
const visibleRoomOf = ${visibleRoomOf.toString()};
|
|
1085
1115
|
const roomSceneObjects = ${roomSceneObjects.toString()};
|
|
@@ -1087,7 +1117,6 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1087
1117
|
const roomSceneLayout = ${roomSceneLayout.toString()};
|
|
1088
1118
|
const roomKindForRoom = ${roomKindForRoom.toString()};
|
|
1089
1119
|
const carriedItems = ${carriedItems.toString()};
|
|
1090
|
-
const visitedRoomGraph = ${visitedRoomGraph.toString()};
|
|
1091
1120
|
const allRoomIds = ${allRoomIds.toString()};
|
|
1092
1121
|
const groundedPlaceholder = ${groundedPlaceholder.toString()};
|
|
1093
1122
|
const exitDoorways = ${exitDoorways.toString()};
|
|
@@ -1095,7 +1124,14 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1095
1124
|
const factsForSubject = ${factsForSubject.toString()};
|
|
1096
1125
|
const renderWorldEditorText = ${renderWorldEditorText.toString()};
|
|
1097
1126
|
const wordBeforeCursor = ${wordBeforeCursor.toString()};
|
|
1127
|
+
const rowsForWorld = ${rowsForWorld.toString()};
|
|
1098
1128
|
const esc = ${escapeHtml.toString()};
|
|
1129
|
+
// The identity Ashcombe Hall's own world facts place as the carrier/viewer
|
|
1130
|
+
// — one named constant standing in for what was six separate "player"
|
|
1131
|
+
// string literals scattered through the room/inventory/map drawing calls
|
|
1132
|
+
// below, so a future scenario shipping a differently-named single
|
|
1133
|
+
// character never falls out of step with the calls that use it.
|
|
1134
|
+
const ACTING_SUBJECT = "player";
|
|
1099
1135
|
const el = (id) => document.getElementById(id);
|
|
1100
1136
|
const roomFrameEl = el("roomFrame");
|
|
1101
1137
|
const roomNameEl = el("roomName");
|
|
@@ -1139,6 +1175,15 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1139
1175
|
const roomDetailSpritesEl = el("roomDetailSprites");
|
|
1140
1176
|
const roomDetailCaptionEl = el("roomDetailCaption");
|
|
1141
1177
|
const legendListEl = el("legendList");
|
|
1178
|
+
const scenarioSelectEl = el("scenarioSelect");
|
|
1179
|
+
const pageNoteEl = el("pageNote");
|
|
1180
|
+
|
|
1181
|
+
// Which of the shipped worlds is loaded. Every read of the world — the
|
|
1182
|
+
// session it is opened over, its opening line, the provenance prefix edit
|
|
1183
|
+
// mode filters on, the key its progress is saved under — goes through this
|
|
1184
|
+
// one index, so picking another world needs no second copy of any of them.
|
|
1185
|
+
let worldIndex = 0;
|
|
1186
|
+
const world = () => ADVENTURE.scenarios[worldIndex].worldPayload;
|
|
1142
1187
|
|
|
1143
1188
|
const params = new URLSearchParams(location.search);
|
|
1144
1189
|
const preview = params.get("preview") === "1";
|
|
@@ -1200,13 +1245,10 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1200
1245
|
|
|
1201
1246
|
// ---- serialize every engine-touching call: the ticker, the chat dock and
|
|
1202
1247
|
// the editor sync all share one in-memory store, and any overlapping pair
|
|
1203
|
-
// could race against the same write.
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
lock = run.catch(() => {});
|
|
1208
|
-
return run;
|
|
1209
|
-
}
|
|
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();
|
|
1210
1252
|
|
|
1211
1253
|
// ---- large-sprite-tier wiring — the gradient-shaded 400px tier
|
|
1212
1254
|
// (data/sprites-large/*.toml) arrives embedded at build time as
|
|
@@ -1268,12 +1310,12 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1268
1310
|
// holding it has actually been visited this session.
|
|
1269
1311
|
function goalStatusLinesFor(rows, state, visitedRoomIds) {
|
|
1270
1312
|
const ids = Array.from(new Set(rows.filter((r) => r.predicate === "mgx:is-objective" && r.object === "true").map((r) => r.subject)));
|
|
1271
|
-
const carriedIds = new Set(carriedItems(rows, state).map((o) => o.subject));
|
|
1313
|
+
const carriedIds = new Set(carriedItems(rows, state, ACTING_SUBJECT).map((o) => o.subject));
|
|
1272
1314
|
const exposedRows = tmctAdventure.exposedFacts(rows, visitedRoomIds);
|
|
1273
1315
|
const exposedState = tmctAdventure.foldWorldState(exposedRows);
|
|
1274
1316
|
return ids.map((id) => {
|
|
1275
1317
|
if (carriedIds.has(id)) return { subject: id, status: "carried", text: "carrying the " + id + " \\u2014 the adventure is won." };
|
|
1276
|
-
const room = visibleRoomOf(exposedRows, exposedState, id);
|
|
1318
|
+
const room = visibleRoomOf(exposedRows, exposedState, id, ACTING_SUBJECT);
|
|
1277
1319
|
return room
|
|
1278
1320
|
? { subject: id, status: "known", text: "last known: the " + id + " is in the " + room + "." }
|
|
1279
1321
|
: { subject: id, status: "unknown", text: "there's a sought-after " + id + " somewhere." };
|
|
@@ -1284,60 +1326,46 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1284
1326
|
// the same non-interactive chip shape the room's own pills use for
|
|
1285
1327
|
// actions, so the sidebar reads as one visual family.
|
|
1286
1328
|
function renderCarrying(rows, state) {
|
|
1287
|
-
const items = carriedItems(rows, state);
|
|
1329
|
+
const items = carriedItems(rows, state, ACTING_SUBJECT);
|
|
1288
1330
|
carryListEl.innerHTML = items.length
|
|
1289
1331
|
? items.map((o) => '<span class="chip">' + esc(o.subject) + "</span>").join("")
|
|
1290
1332
|
: '<span class="empty-note">nothing yet</span>';
|
|
1291
1333
|
}
|
|
1292
1334
|
|
|
1293
1335
|
// ---- room map — ONE svg-building routine shared by play mode's visited-
|
|
1294
|
-
// only map and edit mode's whole-map (
|
|
1336
|
+
// only map and edit mode's whole-map (visitedRoomGraphFor fed allRoomIds
|
|
1295
1337
|
// instead of the exposure set — a parameter, not a second layout), so the
|
|
1296
1338
|
// fixed-size-viewport CSS treatment and the node layout can never drift
|
|
1297
1339
|
// between the two. clickable adds a data-room attribute and a pointer
|
|
1298
1340
|
// cursor per node; play mode's own map stays purely informational.
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
const HINT_DELTA = { north: [0, -1], south: [0, 1], east: [1, 0], west: [-1, 0], up: [0, -1], down: [0, 1] };
|
|
1317
|
-
const hintsSvg = graph.hints.map((hi) => {
|
|
1318
|
-
const from = byRoom.get(hi.from);
|
|
1319
|
-
const d = HINT_DELTA[hi.direction] || [0, 0];
|
|
1320
|
-
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>';
|
|
1321
|
-
}).join("");
|
|
1322
|
-
const nodesSvg = graph.nodes.map((n) => {
|
|
1323
|
-
const cls = "room-node" + (n.current ? " current" : "") + (clickable ? " clickable" : "") + (clickable && n.id === selectedRoomId ? " selected" : "");
|
|
1324
|
-
const attr = clickable ? ' data-room="' + esc(n.id) + '"' : "";
|
|
1325
|
-
return '<g class="' + cls + '"' + attr + '><rect x="' + (cx(n) - roomW / 2) + '" y="' + (cy(n) - roomH / 2) + '" width="' + roomW + '" height="' + roomH + '" rx="3"></rect>'
|
|
1326
|
-
+ '<text x="' + cx(n) + '" y="' + (cy(n) + 2.5) + '">' + esc(n.id) + "</text></g>";
|
|
1327
|
-
}).join("");
|
|
1328
|
-
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") + '">'
|
|
1329
|
-
+ 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 tmctAdventure global 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 tmctAdventure.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 tmctAdventure.directedGridLayout(state, visitedIds, { actingSubject: ACTING_SUBJECT });
|
|
1330
1358
|
}
|
|
1331
1359
|
function renderRoomMap(rows, state, visitedRoomIds) {
|
|
1332
|
-
mapWrapEl.innerHTML =
|
|
1360
|
+
mapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, visitedRoomIds), false) || '<span class="empty-note">nowhere yet</span>';
|
|
1333
1361
|
}
|
|
1334
1362
|
|
|
1335
1363
|
// ---- the map lightbox — clicking the fixed-square play-mode map redraws
|
|
1336
|
-
// the SAME
|
|
1337
|
-
// 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.
|
|
1338
1366
|
function openMapLightbox() {
|
|
1339
1367
|
if (!lastSnapshot) return;
|
|
1340
|
-
const svg =
|
|
1368
|
+
const svg = roomGraphSvgFor(visitedRoomGraphFor(lastSnapshot.state, lastSnapshot.visitedRoomIds), false);
|
|
1341
1369
|
if (!svg) return;
|
|
1342
1370
|
mapLightboxInnerEl.innerHTML = svg;
|
|
1343
1371
|
mapLightboxEl.hidden = false;
|
|
@@ -1445,7 +1473,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1445
1473
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !objLightboxEl.hidden) closeObjectLightbox(); });
|
|
1446
1474
|
|
|
1447
1475
|
function renderEditMap(rows, state) {
|
|
1448
|
-
editMapWrapEl.innerHTML =
|
|
1476
|
+
editMapWrapEl.innerHTML = roomGraphSvgFor(visitedRoomGraphFor(state, allRoomIds(rows)), true) || '<span class="empty-note">this world defines no rooms</span>';
|
|
1449
1477
|
}
|
|
1450
1478
|
editMapWrapEl.addEventListener("click", (e) => {
|
|
1451
1479
|
const g = e.target.closest("[data-room]");
|
|
@@ -1643,8 +1671,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1643
1671
|
// diff + write; this page only ever reads its result back.
|
|
1644
1672
|
|
|
1645
1673
|
function worldOnlyRows(rows) {
|
|
1646
|
-
|
|
1647
|
-
return (rows || []).filter((r) => typeof r.provenance === "string" && r.provenance.indexOf(prefix) === 0);
|
|
1674
|
+
return rowsForWorld(rows, world().name);
|
|
1648
1675
|
}
|
|
1649
1676
|
|
|
1650
1677
|
function renderRoomDetail() {
|
|
@@ -1657,7 +1684,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1657
1684
|
}
|
|
1658
1685
|
roomDetailPanelEl.setAttribute("data-room-kind", roomKindForRoom(editRows, selectedRoomId));
|
|
1659
1686
|
roomDetailTitleEl.textContent = selectedRoomId;
|
|
1660
|
-
const objects = roomSceneObjects(editRows, editState, selectedRoomId);
|
|
1687
|
+
const objects = roomSceneObjects(editRows, editState, selectedRoomId, ACTING_SUBJECT);
|
|
1661
1688
|
roomDetailSpritesEl.innerHTML = objects.length
|
|
1662
1689
|
? objects.map((o) => spriteCardHtml(o.subject, o.spriteClass, resolveObjectSprite(editRows, o))).join("")
|
|
1663
1690
|
: '<span class="empty-note">nothing placed here yet</span>';
|
|
@@ -1674,7 +1701,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1674
1701
|
function renderLegend(rows) {
|
|
1675
1702
|
const subjects = new Set(rows.filter((r) => r.predicate === "rdf:type").map((r) => r.subject));
|
|
1676
1703
|
const classes = new Set(["adventurer"]);
|
|
1677
|
-
subjects.forEach((s) => { if (s !==
|
|
1704
|
+
subjects.forEach((s) => { if (s !== ACTING_SUBJECT) classes.add(spriteClassForObject(rows, s)); });
|
|
1678
1705
|
legendListEl.innerHTML = Array.from(classes).sort().map((cls) => {
|
|
1679
1706
|
const svg = tmctAdventure.resolveSpriteAsset(
|
|
1680
1707
|
cls, spriteAncestryRows(rows, cls), factsForSubject(rows, cls),
|
|
@@ -1792,7 +1819,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1792
1819
|
async function boot({ fresh = false } = {}) {
|
|
1793
1820
|
const saved = !fresh && persist ? await persist.load() : null;
|
|
1794
1821
|
session = await tmctAdventure.createAdventureSession(
|
|
1795
|
-
|
|
1822
|
+
world(),
|
|
1796
1823
|
saved && saved.payload && saved.payload.memoryPayload
|
|
1797
1824
|
? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
|
|
1798
1825
|
: {},
|
|
@@ -1803,12 +1830,14 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1803
1830
|
goalLineEl.textContent = "";
|
|
1804
1831
|
chatlogEl.innerHTML = "";
|
|
1805
1832
|
statusEl.textContent = "";
|
|
1806
|
-
|
|
1833
|
+
if (pageNoteEl) pageNoteEl.textContent = world().opening || "";
|
|
1834
|
+
addChatLine("t", esc(world().opening || "the adventure begins."));
|
|
1807
1835
|
if (saved && snap.turn > 0) {
|
|
1808
1836
|
addChatLine("t", "resumed where you left off (turn " + snap.turn + ") \\u2014 progress kept best-effort on this device; reset starts the manor over.");
|
|
1809
1837
|
}
|
|
1810
1838
|
chatqEl.disabled = false;
|
|
1811
1839
|
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false; editModeBtn.disabled = false;
|
|
1840
|
+
if (scenarioSelectEl) scenarioSelectEl.disabled = false;
|
|
1812
1841
|
}
|
|
1813
1842
|
|
|
1814
1843
|
const ticker = createTicker({
|
|
@@ -1843,11 +1872,34 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1843
1872
|
stepBtn.addEventListener("click", () => ticker.stepOnce());
|
|
1844
1873
|
resetBtn.addEventListener("click", () => ticker.reset());
|
|
1845
1874
|
|
|
1875
|
+
// Each world keeps its own saved progress, so the stamp carries the world's
|
|
1876
|
+
// name and a switch opens the store belonging to the world being switched
|
|
1877
|
+
// to. Without that, one world's snapshot would restore into another's graph.
|
|
1878
|
+
let siteVersion = "";
|
|
1879
|
+
async function openPersistFor(worldName) {
|
|
1880
|
+
if (preview || !tmctAdventure.openPersistedStore) return;
|
|
1881
|
+
if (!siteVersion) siteVersion = await fetchSiteVersion();
|
|
1882
|
+
persist = tmctAdventure.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + worldName });
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
if (scenarioSelectEl) {
|
|
1886
|
+
scenarioSelectEl.addEventListener("change", () => withLock(async () => {
|
|
1887
|
+
const picked = Number(scenarioSelectEl.value);
|
|
1888
|
+
if (!ADVENTURE.scenarios[picked] || picked === worldIndex) return;
|
|
1889
|
+
ticker.pause();
|
|
1890
|
+
// The pending save belongs to the world being left, and it is about to
|
|
1891
|
+
// be written under the world being opened. Cancel it rather than let it
|
|
1892
|
+
// land in the wrong store.
|
|
1893
|
+
clearTimeout(persistSaveTimer);
|
|
1894
|
+
persistSaveTimer = null;
|
|
1895
|
+
worldIndex = picked;
|
|
1896
|
+
await openPersistFor(world().name);
|
|
1897
|
+
await boot();
|
|
1898
|
+
}));
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1846
1901
|
(async () => {
|
|
1847
|
-
|
|
1848
|
-
const siteVersion = await fetchSiteVersion();
|
|
1849
|
-
persist = tmctAdventure.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + ADVENTURE.world.name });
|
|
1850
|
-
}
|
|
1902
|
+
await openPersistFor(world().name);
|
|
1851
1903
|
await boot();
|
|
1852
1904
|
if (preview) ticker.play();
|
|
1853
1905
|
})();
|