@polycode-projects/the-mechanical-code-talker 4.0.1 → 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 +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 +90 -121
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +32 -16
- package/src/services/chat.mjs +101 -33
- package/src/services/code-explorer-viz.mjs +51 -49
- package/src/services/ingest-viz.mjs +15 -57
- package/src/services/ledger-viz.mjs +47 -27
- 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 +87 -198
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +4 -3
- package/src/services/research-viz.mjs +10 -52
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +79 -118
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +271 -221
- 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 +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 +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +28 -28
- 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
|
@@ -30,6 +30,33 @@
|
|
|
30
30
|
// `bear-facing-left.toml` for mgx:faces = left) — the `[match]` table,
|
|
31
31
|
// never the name, is what selects it.
|
|
32
32
|
//
|
|
33
|
+
// A variant can require MORE THAN ONE fact at once, written as repeated
|
|
34
|
+
// `[[match]]` tables (the array-of-tables idiom data/templates/
|
|
35
|
+
// grammar-rules.toml's own `[[rule]]` already uses) instead of a single
|
|
36
|
+
// `[match]` one. Every entry must hold for the variant to be selected, so
|
|
37
|
+
// `bear-facing-left-moving.toml` asks for mgx:faces = left AND
|
|
38
|
+
// mgx:pose = moving and draws the left profile mid-stride. The two spellings
|
|
39
|
+
// are one constraint or many, never a different meaning: a lone `[match]`
|
|
40
|
+
// table is exactly a one-entry list, which is why every file authored before
|
|
41
|
+
// the plural spelling existed still resolves byte-for-byte as it did.
|
|
42
|
+
//
|
|
43
|
+
// Where two satisfied variants overlap, the one requiring MORE facts wins —
|
|
44
|
+
// an instance taught both the facing and the pose gets the combined art, and
|
|
45
|
+
// the same instance taught only the facing gets the plain profile, because
|
|
46
|
+
// the combined variant's second constraint no longer holds. Specificity is
|
|
47
|
+
// counted from the constraints, never read off the filename, so adding a
|
|
48
|
+
// file can't silently reorder what an existing instance resolves to.
|
|
49
|
+
//
|
|
50
|
+
// The two kinds compose rather than exclude each other: a VARIANT may also
|
|
51
|
+
// declare its own `[parameters.*]` tables, and once its `[match]` selects
|
|
52
|
+
// it those parameters fill from the instance's OTHER facts (the facing pair
|
|
53
|
+
// files each carry `[face]` + `[parameters.emotion]`, so a bear taught both
|
|
54
|
+
// mgx:faces = left and mgx:feels = happy renders the left profile wearing
|
|
55
|
+
// the happy face). A satisfied `[match]` is the instance naming this art
|
|
56
|
+
// directly, so a variant is never allowed to fall through the way an
|
|
57
|
+
// unfilled parameterized template is — any placeholder no fact filled is
|
|
58
|
+
// dropped instead, which keeps the returned markup complete.
|
|
59
|
+
//
|
|
33
60
|
// A parameterized template's `[parameters.<name>]` table comes in two
|
|
34
61
|
// shapes, picked by which of `placeholder`/`placeholders` it declares:
|
|
35
62
|
// - single-placeholder (the shape above): one `placeholder` token, and
|
|
@@ -59,9 +86,10 @@
|
|
|
59
86
|
//
|
|
60
87
|
// Specificity order, checked at EACH term of the class's ancestor chain
|
|
61
88
|
// (nearest first, sprite-map.mjs's own classAncestorChain) before moving to
|
|
62
|
-
// the next ancestor:
|
|
63
|
-
//
|
|
64
|
-
//
|
|
89
|
+
// the next ancestor: the satisfied fully-specific variant requiring the most
|
|
90
|
+
// facts (filled from its own [parameters.*] if it declares any) > a
|
|
91
|
+
// parameterized template filled with an observed matching value > a plain
|
|
92
|
+
// class template > (repeat at the next ancestor) > the
|
|
65
93
|
// existing flat spriteRegistry entry for that same term (so a class not yet
|
|
66
94
|
// migrated to its own template keeps resolving exactly as it did before this
|
|
67
95
|
// module existed) > once the chain is exhausted, the same three-step check
|
|
@@ -70,14 +98,77 @@
|
|
|
70
98
|
import { classAncestorChain } from "./sprite-map.mjs";
|
|
71
99
|
import { namespaceSvgIds } from "./svg-instance-ids.mjs";
|
|
72
100
|
|
|
101
|
+
/** The predicate a variant matches on to pick a facing angle. */
|
|
102
|
+
export const FACING_PROPERTY = "mgx:faces";
|
|
103
|
+
|
|
104
|
+
/** The predicate a variant matches on to pick a pose. */
|
|
105
|
+
export const POSE_PROPERTY = "mgx:pose";
|
|
106
|
+
|
|
107
|
+
/** The facing angles a variant's [match] may require of mgx:faces — a
|
|
108
|
+
* turntable read as five steps, of which four are named. Centre is the
|
|
109
|
+
* ABSENT fact, never a word here: a figure with nothing on record about
|
|
110
|
+
* which way it faces already renders its own front-facing art, so spending
|
|
111
|
+
* a value on that would give one picture two names and let an instance ask
|
|
112
|
+
* for the front view two different ways. */
|
|
113
|
+
export const FACING_VALUES = Object.freeze(["left", "half-left", "half-right", "right"]);
|
|
114
|
+
|
|
115
|
+
/** The poses a variant's [match] may require of mgx:pose. At rest is the
|
|
116
|
+
* ABSENT fact, the same reasoning centre-facing gets above: a figure with no
|
|
117
|
+
* pose on record is standing still, and that is what every plain template
|
|
118
|
+
* already draws. "moving" is the one intermediate frame between two rests —
|
|
119
|
+
* the read a walk cycle needs and the only pose that has to exist before
|
|
120
|
+
* movement can be animated at all. */
|
|
121
|
+
export const POSE_VALUES = Object.freeze(["moving"]);
|
|
122
|
+
|
|
73
123
|
/** Every template in `templates` whose `classes` list names `term`. */
|
|
74
124
|
function templatesForClass(term, templates) {
|
|
75
125
|
return (templates || []).filter((t) => Array.isArray(t?.classes) && t.classes.includes(term));
|
|
76
126
|
}
|
|
77
127
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
128
|
+
/** One template's `[match]`/`[[match]]` tables as a flat list of raw entries,
|
|
129
|
+
* whichever of the two spellings it used — the shape every reader wants
|
|
130
|
+
* before it decides anything, so no caller re-answers "did I get a table or
|
|
131
|
+
* a list of them" for itself. */
|
|
132
|
+
function matchEntries(template) {
|
|
133
|
+
const match = template?.match;
|
|
134
|
+
if (!match) return [];
|
|
135
|
+
return Array.isArray(match) ? match : [match];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every `{property, value}` fact one template's `[match]` requires, all of
|
|
140
|
+
* which must hold for the variant to apply. A single `[match]` table yields
|
|
141
|
+
* one constraint, repeated `[[match]]` tables one each, and a template with
|
|
142
|
+
* no `[match]` at all yields none. An entry missing either half is dropped
|
|
143
|
+
* rather than treated as a wildcard — a half-written constraint must never
|
|
144
|
+
* widen what a variant claims to match (spriteTemplateProblems reports it as
|
|
145
|
+
* the authoring mistake it is). Pure.
|
|
146
|
+
*/
|
|
147
|
+
export function matchConstraints(template) {
|
|
148
|
+
return matchEntries(template).filter((c) => c?.property && c.value !== undefined);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function matchSatisfied(constraints, propertyFacts) {
|
|
152
|
+
if (!constraints.length) return false;
|
|
153
|
+
return constraints.every((c) => (propertyFacts || []).some((f) => f.predicate === c.property && f.object === c.value));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The satisfied `[match]` variant among `candidates` requiring the MOST
|
|
157
|
+
* facts, or null when none is satisfied — so a combined facing-and-pose
|
|
158
|
+
* variant outranks the facing-only one it shares an angle with, and drops
|
|
159
|
+
* back to it the moment the instance stops carrying the pose. A tie keeps
|
|
160
|
+
* the earliest candidate, which is the load order the caller handed in. */
|
|
161
|
+
function bestSatisfiedVariant(candidates, propertyFacts) {
|
|
162
|
+
let best = null;
|
|
163
|
+
let bestCount = 0;
|
|
164
|
+
for (const t of candidates) {
|
|
165
|
+
const constraints = matchConstraints(t);
|
|
166
|
+
if (constraints.length <= bestCount) continue;
|
|
167
|
+
if (!matchSatisfied(constraints, propertyFacts)) continue;
|
|
168
|
+
best = t;
|
|
169
|
+
bestCount = constraints.length;
|
|
170
|
+
}
|
|
171
|
+
return best;
|
|
81
172
|
}
|
|
82
173
|
|
|
83
174
|
/** Substitute one matched `[parameters.*.values]` entry into `svg`: a plain
|
|
@@ -125,15 +216,47 @@ function parameterizedFillAll(template, propertyFacts) {
|
|
|
125
216
|
return filledCount > 0 ? svg : null;
|
|
126
217
|
}
|
|
127
218
|
|
|
219
|
+
/** Every placeholder token a template's own `[parameters.*]` tables name,
|
|
220
|
+
* across both shapes — the single `placeholder` and every token in a
|
|
221
|
+
* `placeholders` table. */
|
|
222
|
+
function declaredPlaceholderTokens(template) {
|
|
223
|
+
const tokens = [];
|
|
224
|
+
for (const param of Object.values(template.parameters || {})) {
|
|
225
|
+
if (param?.placeholder) tokens.push(param.placeholder);
|
|
226
|
+
for (const token of Object.values(param?.placeholders || {})) tokens.push(token);
|
|
227
|
+
}
|
|
228
|
+
return tokens;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** `svg` with every token `template` declares that no observed fact filled
|
|
232
|
+
* dropped outright. Only the [match]-selected path wants this: a satisfied
|
|
233
|
+
* [match] means the instance asked for THIS art by name, so the variant
|
|
234
|
+
* can't fall through to a less specific template the way an unfilled
|
|
235
|
+
* parameterized template does, and dropping the leftover token is what
|
|
236
|
+
* keeps the returned markup complete rather than shipping a literal
|
|
237
|
+
* "{{FACE}}" into the page. */
|
|
238
|
+
function withUnfilledPlaceholdersDropped(template, svg) {
|
|
239
|
+
let out = svg;
|
|
240
|
+
for (const token of declaredPlaceholderTokens(template)) out = out.split(token).join("");
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
128
244
|
/** Resolve ONE class term (no ancestor walk here — the caller repeats this
|
|
129
245
|
* at every level of the chain) against the template set, in specificity
|
|
130
246
|
* order: fully-specific match variant > parameterized template filled with
|
|
131
|
-
* an observed value > plain class template.
|
|
132
|
-
*
|
|
247
|
+
* an observed value > plain class template. Among satisfied match variants
|
|
248
|
+
* the most demanding one wins (bestSatisfiedVariant). A match variant that
|
|
249
|
+
* declares its own `[parameters.*]` is filled from them first, so a facing
|
|
250
|
+
* profile also carrying `[face]`/`[parameters.emotion]` renders the mood the
|
|
251
|
+
* instance's own mgx:feels fact names. Returns the SVG string, or null when
|
|
252
|
+
* nothing at this level matches. */
|
|
133
253
|
function resolveAtTerm(term, propertyFacts, templates) {
|
|
134
254
|
const candidates = templatesForClass(term, templates);
|
|
135
|
-
const matched = candidates
|
|
136
|
-
if (matched) return matched.svg;
|
|
255
|
+
const matched = bestSatisfiedVariant(candidates, propertyFacts);
|
|
256
|
+
if (matched && !matched.parameters) return matched.svg;
|
|
257
|
+
if (matched) {
|
|
258
|
+
return withUnfilledPlaceholdersDropped(matched, parameterizedFillAll(matched, propertyFacts) || matched.svg);
|
|
259
|
+
}
|
|
137
260
|
for (const t of candidates) {
|
|
138
261
|
if (t.match || !t.parameters) continue;
|
|
139
262
|
const filled = parameterizedFillAll(t, propertyFacts);
|
|
@@ -187,12 +310,22 @@ export function resolveSpriteAsset(className, factRows, propertyFacts, templates
|
|
|
187
310
|
* names a `property` and exactly one of `placeholder`/`placeholders` (every
|
|
188
311
|
* token named appears in `svg`), its `values` map is non-empty and every
|
|
189
312
|
* entry matches the shape its own `placeholder`/`placeholders` choice
|
|
190
|
-
* expects,
|
|
313
|
+
* expects, every `[match]`/`[[match]]` entry names both `property` and
|
|
314
|
+
* `value`, no two entries demand different values of the SAME property (a
|
|
315
|
+
* constraint set no instance can ever satisfy is dead art, not a stricter
|
|
316
|
+
* variant), a constraint on one of the two CLOSED axes names a value that
|
|
317
|
+
* axis actually has (mgx:faces one of FACING_VALUES, mgx:pose one of
|
|
318
|
+
* POSE_VALUES — every other predicate stays open, since a variant may match
|
|
319
|
+
* on any fact at all, mgx:hasProperty included), and
|
|
191
320
|
* `[face]`/`[parameters.emotion]` are always declared TOGETHER — a face
|
|
192
321
|
* anchor with nothing to select it, or an emotion parameter with nowhere to
|
|
193
322
|
* position its face fragment, is a real authoring mistake either way
|
|
194
323
|
* (sprite-expressions.mjs's own header explains why the face fragment
|
|
195
|
-
* needs the pairing).
|
|
324
|
+
* needs the pairing). Every check reads the tables a template actually
|
|
325
|
+
* declares and none of them cares whether a `[match]` sits alongside, so a
|
|
326
|
+
* facing variant carrying `[match]` + `[face]` + `[parameters.emotion]` is
|
|
327
|
+
* held to exactly the same pairing and placeholder rules as a plain
|
|
328
|
+
* `*-with-emotion.toml` file. */
|
|
196
329
|
export function spriteTemplateProblems(template) {
|
|
197
330
|
const problems = [];
|
|
198
331
|
const t = template || {};
|
|
@@ -233,8 +366,22 @@ export function spriteTemplateProblems(template) {
|
|
|
233
366
|
}
|
|
234
367
|
}
|
|
235
368
|
}
|
|
236
|
-
|
|
237
|
-
|
|
369
|
+
const entries = matchEntries(t);
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
if (!entry?.property || entry?.value === undefined) problems.push("match is missing property or value");
|
|
372
|
+
}
|
|
373
|
+
const requiredBy = new Map();
|
|
374
|
+
for (const { property, value } of matchConstraints(t)) {
|
|
375
|
+
if (requiredBy.has(property) && requiredBy.get(property) !== value) {
|
|
376
|
+
problems.push(`match requires ${property} to be both "${requiredBy.get(property)}" and "${value}" — no instance can satisfy that`);
|
|
377
|
+
}
|
|
378
|
+
requiredBy.set(property, value);
|
|
379
|
+
if (property === FACING_PROPERTY && !FACING_VALUES.includes(value)) {
|
|
380
|
+
problems.push(`match requires ${FACING_PROPERTY} "${value}", which is not one of the turntable's angles (${FACING_VALUES.join(", ")}; the centre view is the absent fact)`);
|
|
381
|
+
}
|
|
382
|
+
if (property === POSE_PROPERTY && !POSE_VALUES.includes(value)) {
|
|
383
|
+
problems.push(`match requires ${POSE_PROPERTY} "${value}", which is not one of the known poses (${POSE_VALUES.join(", ")}; at rest is the absent fact)`);
|
|
384
|
+
}
|
|
238
385
|
}
|
|
239
386
|
if (t.face && !t.parameters?.emotion) {
|
|
240
387
|
problems.push("face is declared without parameters.emotion — a face anchor with nothing to select it is dead data");
|
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
// mgx:acts-toward, mgx:is-objective) — an editor has to show and change
|
|
15
15
|
// exactly the facts a player is never told.
|
|
16
16
|
//
|
|
17
|
-
// No imports: every export here is .toString()-splice-safe,
|
|
18
|
-
// discipline adventure-viz.mjs's own render-glue functions hold (see
|
|
19
|
-
// module's header) — this module's functions get spliced directly into
|
|
20
|
-
// adventure page's inline script the same way.
|
|
17
|
+
// No imports of its own logic: every export here is .toString()-splice-safe,
|
|
18
|
+
// the same discipline adventure-viz.mjs's own render-glue functions hold (see
|
|
19
|
+
// that module's header) — this module's functions get spliced directly into
|
|
20
|
+
// the adventure page's inline script the same way. The one exception,
|
|
21
|
+
// wordBeforeCursor, re-exports viz-theme.mjs's own shared copy (byte-identical
|
|
22
|
+
// to what used to live here, and to mud-editor.mjs's own copy) rather than
|
|
23
|
+
// keep a third copy of the same regex.
|
|
21
24
|
//
|
|
22
25
|
// Two predicate families get different sync strategies, on purpose:
|
|
23
26
|
// - PLACEMENT/OPENNESS (mgx:currently-in/located-in/fixed-in/stands-
|
|
@@ -357,13 +360,4 @@ export function planWorldEditorSync(rows, state, triples) {
|
|
|
357
360
|
|
|
358
361
|
// ---- cursor-driven suggestions ---------------------------------------------
|
|
359
362
|
|
|
360
|
-
|
|
361
|
-
* digits/hyphens, the same token shape this vocabulary's own terms use
|
|
362
|
-
* (kebab-case room names like "drawing-room"). Empty string when the
|
|
363
|
-
* cursor sits after whitespace/punctuation with no word directly behind
|
|
364
|
-
* it. Pure. */
|
|
365
|
-
export function wordBeforeCursor(text, cursorPos) {
|
|
366
|
-
const head = String(text || "").slice(0, cursorPos);
|
|
367
|
-
const m = head.match(/[A-Za-z][A-Za-z0-9-]*$/);
|
|
368
|
-
return m ? m[0].toLowerCase() : "";
|
|
369
|
-
}
|
|
363
|
+
export { wordBeforeCursor } from "./viz-theme.mjs";
|
|
@@ -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, 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
|
|
@@ -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"
|
|
@@ -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
|
|
@@ -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 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 });
|
|
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;
|
|
@@ -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]");
|
|
@@ -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() {
|