@polycode-projects/the-mechanical-code-talker 2.10.5 → 2.11.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 -2
- package/corpus/sprites/src/sprite-facts.jsonl +18 -0
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
- package/data/sprites/book-icon.toml +12 -0
- package/data/sprites/cellar-icon.toml +12 -0
- package/data/sprites/drawing-room-icon.toml +13 -0
- package/data/sprites/garden-icon.toml +12 -0
- package/data/sprites/kitchen-icon.toml +13 -0
- package/data/sprites/library-icon.toml +12 -0
- package/data/sprites/pan-icon.toml +11 -0
- package/data/sprites/study-icon.toml +12 -0
- package/package.json +5 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/memory/trust.mjs +11 -0
- package/src/domain/worlds-pack.mjs +50 -0
- package/src/services/adventure-autoplay.mjs +5 -2
- package/src/services/adventure-viz.mjs +301 -33
- package/src/services/adventure.mjs +162 -14
- package/src/services/chat-page-viz.mjs +265 -189
- package/src/services/chat-session.mjs +15 -5
- package/src/services/chat.mjs +286 -43
- package/src/services/code-explorer-viz.mjs +183 -75
- package/src/services/extract-facts.mjs +118 -28
- package/src/services/ingest-viz.mjs +328 -79
- package/src/services/ledger-viz.mjs +99 -0
- package/src/services/memory-panel-viz.mjs +159 -0
- package/src/services/research.mjs +266 -0
- package/src/services/sentences.mjs +19 -0
- package/src/services/spider-fly-viz.mjs +21 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +28 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
- package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +112 -112
- package/src/surfaces/web/memory-stats.mjs +53 -0
|
@@ -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
|
+
// Eight 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
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
// adventure.mjs's own private `visibleRoomOf` the same way
|
|
29
29
|
// adventure-autoplay.mjs's own `roomOfSubject` already has to), `roomSceneObjects`
|
|
30
30
|
// (every subject actually visible in a room, built over `visibleRoomOf`),
|
|
31
|
+
// `scenePlacement` (an object's rendering plane and, if stacked, what it
|
|
32
|
+
// rests on — instance on-top-of/on-plane facts, then a class-default
|
|
33
|
+
// ancestor walk, then floor), `roomSceneLayout` (the room split into a wall
|
|
34
|
+
// band and floor stacks, over `roomSceneObjects` and `scenePlacement`),
|
|
35
|
+
// `roomKindForRoom` (a room's border treatment, from its own rdf:type),
|
|
31
36
|
// `carriedItems` (every object placed with the player), and `visitedRoomGraph`
|
|
32
37
|
// (a directions-only layout of the rooms a session has actually visited —
|
|
33
38
|
// see its own header for the exposure discipline). None of these import
|
|
@@ -55,9 +60,10 @@
|
|
|
55
60
|
// tick. `#caption`/`#goalLine` keep their existing job as the current-state
|
|
56
61
|
// summary; the chat log is the persistent addition.
|
|
57
62
|
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
63
|
+
// The chat dock is the whole of `.side`; carrying (`carriedItems`), the
|
|
64
|
+
// visited-room map (`visitedRoomGraph`) and goal status (`goalStatusLines`)
|
|
65
|
+
// render into their own panels in `.stage-left` instead, alongside the room
|
|
66
|
+
// view and its controls — all three read `session.snapshot()`'s own
|
|
61
67
|
// `visitedRoomIds`, the manual+auto-play exposure set
|
|
62
68
|
// adventure-browser-entry.mjs threads forward (see that module's header for
|
|
63
69
|
// why the two paths share one set rather than two).
|
|
@@ -169,6 +175,138 @@ export function roomSceneObjects(rows, state, here) {
|
|
|
169
175
|
return out;
|
|
170
176
|
}
|
|
171
177
|
|
|
178
|
+
/** Where `subject` renders in its room's scene: `{plane, stackedOn}`, plane
|
|
179
|
+
* one of "floor"/"surface"/"wall"/"ceiling". Resolution order: a CURRENT
|
|
180
|
+
* `mgx:on-top-of` row (plane "surface", `stackedOn` the object rested on) —
|
|
181
|
+
* "current" meaning its own newest turn is at least as new as `subject`'s
|
|
182
|
+
* own newest PLACEMENT turn (`state.placements`), so `take lamp` off a desk
|
|
183
|
+
* auto-invalidates a stale `lamp mgx:on-top-of desk` row with no extra
|
|
184
|
+
* write, the same staleness rule `foldWorldState` already applies within
|
|
185
|
+
* its own placement map; then a current `mgx:on-plane` row (its own object
|
|
186
|
+
* taken as the plane verbatim); then a class default, found by a breadth-
|
|
187
|
+
* first walk up `rdfs:subClassOf` edges FIRST (a directly-taxonomized
|
|
188
|
+
* individual such as "portrait rdfs:subClassOf painting", to its own
|
|
189
|
+
* superclass) for the first `mgx:default-plane` fact, falling back to a
|
|
190
|
+
* second walk that also follows `rdf:type` edges (an individual to its
|
|
191
|
+
* generic rendering class) only if the first finds nothing — a specific
|
|
192
|
+
* taxonomic fact about the individual must outrank its own coarser
|
|
193
|
+
* `rdf:type`, or which ancestor's default wins becomes an accident of fact
|
|
194
|
+
* order rather than a taxonomy decision; a DIFFERENT ancestor walk from
|
|
195
|
+
* sprite-map.mjs's own `classAncestorChain` (subClassOf-only, and an import
|
|
196
|
+
* this function can't take and stay `.toString()`-splice-safe); then the
|
|
197
|
+
* floor. Pure, fully self-contained in its own function body (no
|
|
198
|
+
* reference to any other name in this module) — reads `rows` fresh rather
|
|
199
|
+
* than a precomputed `positions` map, so it needs nothing beyond what
|
|
200
|
+
* `foldWorldState` already exposes on `state.placements`. */
|
|
201
|
+
export function scenePlacement(rows, state, subject) {
|
|
202
|
+
const snapshotRe = /^(.+)@turn(\d+)$/;
|
|
203
|
+
function newestRowFor(predicate) {
|
|
204
|
+
let best = null;
|
|
205
|
+
for (const row of rows || []) {
|
|
206
|
+
if (row.predicate !== predicate) continue;
|
|
207
|
+
const m = snapshotRe.exec(row.subject);
|
|
208
|
+
const base = m ? m[1] : row.subject;
|
|
209
|
+
if (base !== subject) continue;
|
|
210
|
+
const turn = m ? Number(m[2]) : 0;
|
|
211
|
+
if (!best || turn >= best.turn) best = { object: row.object, turn };
|
|
212
|
+
}
|
|
213
|
+
return best;
|
|
214
|
+
}
|
|
215
|
+
// Two passes, not one BFS over both edge kinds at once: an individual
|
|
216
|
+
// directly `rdfs:subClassOf` a class (e.g. "portrait rdfs:subClassOf
|
|
217
|
+
// painting") is a deliberate, specific taxonomic fact and must win over
|
|
218
|
+
// its own, more generic `rdf:type` (e.g. "portrait rdf:type furniture",
|
|
219
|
+
// the coarse sprite/rendering category) whenever BOTH ancestors carry
|
|
220
|
+
// their own `mgx:default-plane` — a single combined-edge walk would
|
|
221
|
+
// instead return whichever ancestor happens to appear earlier in the
|
|
222
|
+
// fact rows, an accident of file order rather than a taxonomy decision.
|
|
223
|
+
function walkForDefaultPlane(edgePredicates) {
|
|
224
|
+
const seen = new Set([subject]);
|
|
225
|
+
const queue = [subject];
|
|
226
|
+
while (queue.length) {
|
|
227
|
+
const node = queue.shift();
|
|
228
|
+
const defaultRow = (rows || []).find((r) => r.subject === node && r.predicate === "mgx:default-plane");
|
|
229
|
+
if (defaultRow) return defaultRow.object;
|
|
230
|
+
for (const row of rows || []) {
|
|
231
|
+
if (row.subject !== node || !edgePredicates.includes(row.predicate)) continue;
|
|
232
|
+
if (!seen.has(row.object)) { seen.add(row.object); queue.push(row.object); }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function classDefaultPlane() {
|
|
238
|
+
return walkForDefaultPlane(["rdfs:subClassOf"]) ?? walkForDefaultPlane(["rdf:type", "rdfs:subClassOf"]);
|
|
239
|
+
}
|
|
240
|
+
const placementTurn = state?.placements?.get(subject)?.turn ?? 0;
|
|
241
|
+
const onTopOf = newestRowFor("mgx:on-top-of");
|
|
242
|
+
if (onTopOf && onTopOf.turn >= placementTurn) return { plane: "surface", stackedOn: onTopOf.object };
|
|
243
|
+
const onPlane = newestRowFor("mgx:on-plane");
|
|
244
|
+
if (onPlane && onPlane.turn >= placementTurn) return { plane: onPlane.object, stackedOn: null };
|
|
245
|
+
const defaultPlane = classDefaultPlane();
|
|
246
|
+
if (defaultPlane) return { plane: defaultPlane, stackedOn: null };
|
|
247
|
+
return { plane: "floor", stackedOn: null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The room scene laid out for drawing: `{wall, floor}`. `wall` is every
|
|
251
|
+
* wall- or ceiling-mounted visible object (deterministic, subject order).
|
|
252
|
+
* `floor` is one entry per floor-standing stack, `{items: [top..base]}` —
|
|
253
|
+
* a `mgx:on-top-of` chain is resolved transitively (an item on an item on a
|
|
254
|
+
* floor base becomes one three-level stack), and an item whose own
|
|
255
|
+
* `stackedOn` target is not itself VISIBLE in this room (or is itself
|
|
256
|
+
* wall/ceiling-mounted, so has no floor footprint to stack onto) demotes to
|
|
257
|
+
* its own single-item floor stack rather than vanishing. Built over
|
|
258
|
+
* `roomSceneObjects` and `scenePlacement`, so it can never draw an object
|
|
259
|
+
* the flat room view wouldn't already show. Pure. */
|
|
260
|
+
export function roomSceneLayout(rows, state, here) {
|
|
261
|
+
const objects = roomSceneObjects(rows, state, here);
|
|
262
|
+
const bySubject = new Map(objects.map((o) => [o.subject, o]));
|
|
263
|
+
const placementOf = new Map(objects.map((o) => [o.subject, scenePlacement(rows, state, o.subject)]));
|
|
264
|
+
|
|
265
|
+
const wall = [];
|
|
266
|
+
const stackedOnBy = new Map(); // base subject -> [subjects resting directly on it]
|
|
267
|
+
const bases = [];
|
|
268
|
+
|
|
269
|
+
for (const o of objects) {
|
|
270
|
+
const placement = placementOf.get(o.subject);
|
|
271
|
+
if (placement.plane === "wall" || placement.plane === "ceiling") { wall.push(o); continue; }
|
|
272
|
+
const targetPlacement = placement.stackedOn ? placementOf.get(placement.stackedOn) : null;
|
|
273
|
+
const targetHasFloorFootprint = targetPlacement && targetPlacement.plane !== "wall" && targetPlacement.plane !== "ceiling";
|
|
274
|
+
if (placement.plane === "surface" && placement.stackedOn !== o.subject && targetHasFloorFootprint) {
|
|
275
|
+
if (!stackedOnBy.has(placement.stackedOn)) stackedOnBy.set(placement.stackedOn, []);
|
|
276
|
+
stackedOnBy.get(placement.stackedOn).push(o.subject);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
bases.push(o.subject);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
wall.sort((a, b) => a.subject.localeCompare(b.subject));
|
|
283
|
+
bases.sort((a, b) => a.localeCompare(b));
|
|
284
|
+
for (const list of stackedOnBy.values()) list.sort((a, b) => a.localeCompare(b));
|
|
285
|
+
|
|
286
|
+
const visitedForStack = new Set();
|
|
287
|
+
function stackFrom(base) {
|
|
288
|
+
if (visitedForStack.has(base)) return [];
|
|
289
|
+
visitedForStack.add(base);
|
|
290
|
+
const items = [];
|
|
291
|
+
for (const child of stackedOnBy.get(base) || []) items.push(...stackFrom(child));
|
|
292
|
+
items.push(bySubject.get(base));
|
|
293
|
+
return items;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { wall, floor: bases.map((base) => ({ items: stackFrom(base) })) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** A room's kind for its border treatment: "outdoor" (`rdf:type
|
|
300
|
+
* outdoor-space`), "underground" (`rdf:type underground-space`), else
|
|
301
|
+
* "indoor" — the default, covering every room the current shipped world
|
|
302
|
+
* defines. Pure, self-contained. */
|
|
303
|
+
export function roomKindForRoom(rows, roomId) {
|
|
304
|
+
const isRoomTyped = (kind) => (rows || []).some((r) => r.subject === roomId && r.predicate === "rdf:type" && r.object === kind);
|
|
305
|
+
if (isRoomTyped("outdoor-space")) return "outdoor";
|
|
306
|
+
if (isRoomTyped("underground-space")) return "underground";
|
|
307
|
+
return "indoor";
|
|
308
|
+
}
|
|
309
|
+
|
|
172
310
|
/** Every object currently `mgx:located-in` "player", sorted, each with its
|
|
173
311
|
* sprite class — the exact placement `worldDigestRows`'/`inventoryAnswer`'s
|
|
174
312
|
* own "carries the" branch already reads, just returned as a plain list
|
|
@@ -394,24 +532,36 @@ ${THEME_TOKENS_CSS}
|
|
|
394
532
|
--baize: #4A6B52; --baize-line: #33503C; --board-path: #E7DAB8;
|
|
395
533
|
--wall: #E9DDBE; --wall-stripe: rgba(147, 112, 31, .10);
|
|
396
534
|
--floor-a: #D9C398; --floor-b: #CFB884;
|
|
535
|
+
--sky: #BFE0EE; --hedge: #3F6B45;
|
|
536
|
+
--stone: #6E6A63; --stone-b: rgba(110, 106, 99, .18);
|
|
537
|
+
--stone-floor-a: #504C46; --stone-floor-b: #464340;
|
|
397
538
|
}
|
|
398
539
|
@media (prefers-color-scheme: dark) { :root {
|
|
399
540
|
--parchment: #241E12; --parchment-strong: #2C2416; --gilt: #C0A054;
|
|
400
541
|
--baize: #1E2C22; --baize-line: #4A6852; --board-path: #56604D;
|
|
401
542
|
--wall: #262013; --wall-stripe: rgba(192, 160, 84, .07);
|
|
402
543
|
--floor-a: #2B2314; --floor-b: #241E10;
|
|
544
|
+
--sky: #16232B; --hedge: #2E4A33;
|
|
545
|
+
--stone: #3A3834; --stone-b: rgba(255, 255, 255, .05);
|
|
546
|
+
--stone-floor-a: #17150F; --stone-floor-b: #100E0A;
|
|
403
547
|
} }
|
|
404
548
|
:root[data-theme="dark"] {
|
|
405
549
|
--parchment: #241E12; --parchment-strong: #2C2416; --gilt: #C0A054;
|
|
406
550
|
--baize: #1E2C22; --baize-line: #4A6852; --board-path: #56604D;
|
|
407
551
|
--wall: #262013; --wall-stripe: rgba(192, 160, 84, .07);
|
|
408
552
|
--floor-a: #2B2314; --floor-b: #241E10;
|
|
553
|
+
--sky: #16232B; --hedge: #2E4A33;
|
|
554
|
+
--stone: #3A3834; --stone-b: rgba(255, 255, 255, .05);
|
|
555
|
+
--stone-floor-a: #17150F; --stone-floor-b: #100E0A;
|
|
409
556
|
}
|
|
410
557
|
:root[data-theme="light"] {
|
|
411
558
|
--parchment: #ECE1C8; --parchment-strong: #E1D2A6; --gilt: #93701F;
|
|
412
559
|
--baize: #4A6B52; --baize-line: #33503C; --board-path: #E7DAB8;
|
|
413
560
|
--wall: #E9DDBE; --wall-stripe: rgba(147, 112, 31, .10);
|
|
414
561
|
--floor-a: #D9C398; --floor-b: #CFB884;
|
|
562
|
+
--sky: #BFE0EE; --hedge: #3F6B45;
|
|
563
|
+
--stone: #6E6A63; --stone-b: rgba(110, 106, 99, .18);
|
|
564
|
+
--stone-floor-a: #504C46; --stone-floor-b: #464340;
|
|
415
565
|
}
|
|
416
566
|
|
|
417
567
|
html { background: var(--bg); }
|
|
@@ -427,14 +577,23 @@ ${THEME_TOKENS_CSS}
|
|
|
427
577
|
.mode-toggle:hover:not(:disabled) { background: var(--parchment-strong); }
|
|
428
578
|
.mode-toggle:disabled { opacity: .5; cursor: default; }
|
|
429
579
|
|
|
430
|
-
|
|
580
|
+
/* the room-view + map column runs roughly two-thirds width, the manor's
|
|
581
|
+
own account (chat log + pills + caption) a third. */
|
|
582
|
+
.stage { display: grid; grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr); gap: 1rem; align-items: start; }
|
|
431
583
|
@media (max-width: 760px) { .stage { grid-template-columns: 1fr; } }
|
|
432
584
|
|
|
433
|
-
/* the
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
585
|
+
/* the room's own opening prose, relocated (operator feedback) out of a
|
|
586
|
+
bottom-of-page strip and into a spider-fly-style note directly under the
|
|
587
|
+
titlebar — the same "sight-read the world before you touch anything"
|
|
588
|
+
framing spider-fly.html's own header paragraph already gives its board. */
|
|
589
|
+
.page-note { background: var(--parchment); border-left: 3px solid var(--gilt); padding: .6rem .85rem; font-size: .9rem; font-style: italic; margin: 0 0 1rem; }
|
|
590
|
+
.page-note:empty { display: none; margin: 0; }
|
|
591
|
+
|
|
592
|
+
/* the left column: quest, the room itself, the reset/play/step/turn strip,
|
|
593
|
+
the goal/status lines, satchel, the command box and the manor map, all
|
|
594
|
+
as full-width strips sharing the room's own width — the column's total
|
|
595
|
+
height tracks the side column instead of leaving a gap under a lone,
|
|
596
|
+
short room panel. */
|
|
438
597
|
.stage-left { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
|
439
598
|
|
|
440
599
|
/* the room view — a 90s-RPG interior cutaway, all CSS gradients, no
|
|
@@ -456,6 +615,24 @@ ${THEME_TOKENS_CSS}
|
|
|
456
615
|
.room-frame::before, .room-frame::after { content: "\\2766"; position: absolute; font-size: 1.1rem; color: var(--gilt); opacity: .85; line-height: 1; }
|
|
457
616
|
.room-frame::before { top: -.6rem; left: -.35rem; }
|
|
458
617
|
.room-frame::after { bottom: -.6rem; right: -.35rem; transform: rotate(180deg); }
|
|
618
|
+
/* outdoor: a sky band over a hedge-green divider, no gilt — cellar and
|
|
619
|
+
study/library etc. stay on the indoor gradient above (the default), so
|
|
620
|
+
only a room actually typed outdoor-space/underground-space repaints. */
|
|
621
|
+
.room-frame[data-room-kind="outdoor"] {
|
|
622
|
+
background:
|
|
623
|
+
linear-gradient(var(--sky), var(--sky)) 0 0 / 100% 58% no-repeat,
|
|
624
|
+
linear-gradient(var(--hedge), var(--hedge)) 0 58% / 100% 3px no-repeat,
|
|
625
|
+
repeating-linear-gradient(78deg, var(--floor-a) 0 26px, var(--floor-b) 26px 52px);
|
|
626
|
+
border-color: var(--hedge);
|
|
627
|
+
}
|
|
628
|
+
/* underground: stone-grey stripes over a darker floor, no gilt rail. */
|
|
629
|
+
.room-frame[data-room-kind="underground"] {
|
|
630
|
+
background:
|
|
631
|
+
repeating-linear-gradient(90deg, var(--stone-b) 0 4px, transparent 4px 30px) 0 0 / 100% 58% no-repeat,
|
|
632
|
+
linear-gradient(var(--stone), var(--stone)) 0 0 / 100% 58% no-repeat,
|
|
633
|
+
repeating-linear-gradient(78deg, var(--stone-floor-a) 0 26px, var(--stone-floor-b) 26px 52px);
|
|
634
|
+
border-color: var(--stone);
|
|
635
|
+
}
|
|
459
636
|
/* the brass door plaque naming the room the scene is drawing — filled
|
|
460
637
|
from the same snapshot the caption already reads, never a new fact. */
|
|
461
638
|
.room-plaque {
|
|
@@ -466,7 +643,35 @@ ${THEME_TOKENS_CSS}
|
|
|
466
643
|
padding: .22rem .9rem; white-space: nowrap;
|
|
467
644
|
}
|
|
468
645
|
.room-plaque:empty { display: none; }
|
|
469
|
-
|
|
646
|
+
/* the room-kind icon — filled through the exact same property-aware
|
|
647
|
+
sprite resolver the room's own object cards use, keyed on the room's own
|
|
648
|
+
name (so library/kitchen/garden reach their large TOMLs, everything else
|
|
649
|
+
falls back through room's own ancestor chain to the generic room icon). */
|
|
650
|
+
.room-kind-icon { position: absolute; top: .55rem; right: .6rem; width: 28px; height: 28px; opacity: .92; }
|
|
651
|
+
.room-kind-icon svg { width: 100%; height: 100%; display: block; }
|
|
652
|
+
.room-kind-icon:empty { display: none; }
|
|
653
|
+
.sprite-row { display: flex; align-items: flex-end; gap: .9rem .7rem; min-height: 2.5rem; }
|
|
654
|
+
/* the floor band: every non-wall-mounted stack, wrapping and pinned to the
|
|
655
|
+
left edge of the room, as the flat sprite row always has. */
|
|
656
|
+
.floor-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .9rem .7rem; }
|
|
657
|
+
/* the adventurer's own card, pinned to the right edge of the room view —
|
|
658
|
+
margin-left: auto inside the shared flex row pushes it there regardless
|
|
659
|
+
of how many floor stacks sit to its left. */
|
|
660
|
+
.you-slot { margin-left: auto; display: flex; align-items: flex-end; flex: none; }
|
|
661
|
+
/* one on-top-of chain, drawn as a column: the topmost item first, the
|
|
662
|
+
floor-standing base last — shrink-wrapped so the stack's own width is
|
|
663
|
+
whichever level is widest, never the room's full width. */
|
|
664
|
+
.sprite-stack { display: flex; flex-direction: column; align-items: center; gap: .3rem; width: max-content; }
|
|
665
|
+
/* the wall band: centered, absolutely laid over the papered-wall portion
|
|
666
|
+
of the room frame (roughly its top 8%-52% — the gilt dado sits at 58%),
|
|
667
|
+
items bottom-aligned so a hung painting's own frame reads as flush
|
|
668
|
+
against the dado rail beneath it. */
|
|
669
|
+
.wall-row {
|
|
670
|
+
position: absolute; left: 1.1rem; right: 1.1rem; top: 8%; height: 44%;
|
|
671
|
+
display: flex; justify-content: center; align-items: flex-end; gap: .9rem .7rem;
|
|
672
|
+
flex-wrap: wrap; pointer-events: none;
|
|
673
|
+
}
|
|
674
|
+
.wall-row:empty { display: none; }
|
|
470
675
|
|
|
471
676
|
/* the class-badge system — this design's signature element: every sprite
|
|
472
677
|
gets a small ringed portrait frame plus a genre-flavored class word
|
|
@@ -587,12 +792,14 @@ ${THEME_TOKENS_CSS}
|
|
|
587
792
|
.edit-status.pending { color: var(--entail); }
|
|
588
793
|
.edit-status.ok { color: var(--taught); }
|
|
589
794
|
.roomdetail .sprite-row { min-height: 3.2rem; }
|
|
795
|
+
.roomdetail[data-room-kind="outdoor"] { border-top-color: var(--hedge); }
|
|
796
|
+
.roomdetail[data-room-kind="underground"] { border-top-color: var(--stone); }
|
|
590
797
|
#legendList { display: flex; flex-wrap: wrap; gap: .5rem .3rem; }
|
|
591
798
|
|
|
592
799
|
body.preview .side, body.preview .stage-left > .panel, body.preview .controls-row, body.preview .status { display: none; }
|
|
593
800
|
body.preview main { padding: 0; max-width: none; }
|
|
594
801
|
body.preview .stage { display: block; }
|
|
595
|
-
body.preview .eyebrow, body.preview h1, body.preview .mode-toggle, body.preview #editStage { display: none; }
|
|
802
|
+
body.preview .eyebrow, body.preview h1, body.preview .mode-toggle, body.preview #editStage, body.preview .page-note { display: none; }
|
|
596
803
|
</style>
|
|
597
804
|
</head>
|
|
598
805
|
<body>
|
|
@@ -602,19 +809,21 @@ ${THEME_TOKENS_CSS}
|
|
|
602
809
|
<h1>A room, drawn from exactly what the text already says is there</h1>
|
|
603
810
|
<button id="editModeBtn" type="button" class="mode-toggle" disabled>edit the world</button>
|
|
604
811
|
</div>
|
|
812
|
+
<p class="page-note">${escapeHtml(worldPayload.opening)}</p>
|
|
605
813
|
<div class="stage" id="playStage">
|
|
606
814
|
<div class="stage-left">
|
|
607
815
|
<div class="panel goals">
|
|
608
816
|
<h2>quest</h2>
|
|
609
817
|
<div id="goalList"></div>
|
|
610
818
|
</div>
|
|
611
|
-
<div class="panel carrying">
|
|
612
|
-
<h2>satchel</h2>
|
|
613
|
-
<div class="chips" id="carryList"></div>
|
|
614
|
-
</div>
|
|
615
819
|
<div class="room-frame" id="roomFrame">
|
|
616
820
|
<div class="room-plaque mono" id="roomName"></div>
|
|
617
|
-
<div class="
|
|
821
|
+
<div class="room-kind-icon" id="roomKindIcon"></div>
|
|
822
|
+
<div class="wall-row" id="wallRow"></div>
|
|
823
|
+
<div class="sprite-row" id="spriteRow">
|
|
824
|
+
<div class="floor-row" id="floorRow"></div>
|
|
825
|
+
<div class="you-slot" id="youSlot"></div>
|
|
826
|
+
</div>
|
|
618
827
|
</div>
|
|
619
828
|
<div class="controls-row" id="playControls">
|
|
620
829
|
<button id="resetBtn" type="button" disabled>reset</button>
|
|
@@ -622,13 +831,14 @@ ${THEME_TOKENS_CSS}
|
|
|
622
831
|
<button id="stepBtn" type="button" disabled>step</button>
|
|
623
832
|
<span class="turn mono" id="turnLabel">turn: 0</span>
|
|
624
833
|
</div>
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
<div class="
|
|
628
|
-
<h2>
|
|
629
|
-
<div class="
|
|
630
|
-
|
|
631
|
-
|
|
834
|
+
<div class="goal-line" id="goalLine"></div>
|
|
835
|
+
<div class="status" id="status">loading the engine…</div>
|
|
836
|
+
<div class="panel carrying">
|
|
837
|
+
<h2>satchel</h2>
|
|
838
|
+
<div class="chips" id="carryList"></div>
|
|
839
|
+
</div>
|
|
840
|
+
<div class="panel command">
|
|
841
|
+
<h2>speak to the manor</h2>
|
|
632
842
|
<form class="chatask" id="chatform">
|
|
633
843
|
<span class="prompt mono">tmct></span>
|
|
634
844
|
<input id="chatq" type="text" placeholder="go north" aria-label="Type a command, or ask a question" disabled>
|
|
@@ -638,6 +848,14 @@ ${THEME_TOKENS_CSS}
|
|
|
638
848
|
<h2>the manor, so far</h2>
|
|
639
849
|
<div class="map-viewport"><div id="mapWrap"></div></div>
|
|
640
850
|
</div>
|
|
851
|
+
</div>
|
|
852
|
+
<aside class="side" aria-label="The adventure's log and chat">
|
|
853
|
+
<div class="chat">
|
|
854
|
+
<h2>the manor's own account</h2>
|
|
855
|
+
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
856
|
+
<div class="pills" id="pills"></div>
|
|
857
|
+
<div class="caption" id="caption"></div>
|
|
858
|
+
</div>
|
|
641
859
|
</aside>
|
|
642
860
|
</div>
|
|
643
861
|
|
|
@@ -653,7 +871,7 @@ ${THEME_TOKENS_CSS}
|
|
|
653
871
|
<h2>the whole manor</h2>
|
|
654
872
|
<div class="map-viewport"><div id="editMapWrap"></div></div>
|
|
655
873
|
</div>
|
|
656
|
-
<div class="panel roomdetail">
|
|
874
|
+
<div class="panel roomdetail" id="roomDetailPanel">
|
|
657
875
|
<h2 id="roomDetailTitle">click a room</h2>
|
|
658
876
|
<div class="sprite-row" id="roomDetailSprites"></div>
|
|
659
877
|
<div class="caption" id="roomDetailCaption"></div>
|
|
@@ -664,9 +882,6 @@ ${THEME_TOKENS_CSS}
|
|
|
664
882
|
</div>
|
|
665
883
|
</aside>
|
|
666
884
|
</div>
|
|
667
|
-
|
|
668
|
-
<div class="goal-line" id="goalLine"></div>
|
|
669
|
-
<div class="status" id="status">loading the engine…</div>
|
|
670
885
|
</main>
|
|
671
886
|
<script>
|
|
672
887
|
const ADVENTURE = ${pageData};
|
|
@@ -679,6 +894,9 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
679
894
|
const spriteClassForObject = ${spriteClassForObject.toString()};
|
|
680
895
|
const visibleRoomOf = ${visibleRoomOf.toString()};
|
|
681
896
|
const roomSceneObjects = ${roomSceneObjects.toString()};
|
|
897
|
+
const scenePlacement = ${scenePlacement.toString()};
|
|
898
|
+
const roomSceneLayout = ${roomSceneLayout.toString()};
|
|
899
|
+
const roomKindForRoom = ${roomKindForRoom.toString()};
|
|
682
900
|
const carriedItems = ${carriedItems.toString()};
|
|
683
901
|
const visitedRoomGraph = ${visitedRoomGraph.toString()};
|
|
684
902
|
const allRoomIds = ${allRoomIds.toString()};
|
|
@@ -688,8 +906,12 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
688
906
|
const wordBeforeCursor = ${wordBeforeCursor.toString()};
|
|
689
907
|
const esc = ${escapeHtml.toString()};
|
|
690
908
|
const el = (id) => document.getElementById(id);
|
|
691
|
-
const
|
|
909
|
+
const roomFrameEl = el("roomFrame");
|
|
692
910
|
const roomNameEl = el("roomName");
|
|
911
|
+
const roomKindIconEl = el("roomKindIcon");
|
|
912
|
+
const wallRowEl = el("wallRow");
|
|
913
|
+
const floorRowEl = el("floorRow");
|
|
914
|
+
const youSlotEl = el("youSlot");
|
|
693
915
|
const captionEl = el("caption");
|
|
694
916
|
const goalLineEl = el("goalLine");
|
|
695
917
|
const statusEl = el("status");
|
|
@@ -709,6 +931,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
709
931
|
const editorPillsEl = el("editorPills");
|
|
710
932
|
const editorStatusEl = el("editorStatus");
|
|
711
933
|
const editMapWrapEl = el("editMapWrap");
|
|
934
|
+
const roomDetailPanelEl = el("roomDetailPanel");
|
|
712
935
|
const roomDetailTitleEl = el("roomDetailTitle");
|
|
713
936
|
const roomDetailSpritesEl = el("roomDetailSprites");
|
|
714
937
|
const roomDetailCaptionEl = el("roomDetailCaption");
|
|
@@ -806,6 +1029,19 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
806
1029
|
+ '<div class="class-badge" data-cls="' + esc(cls) + '">' + esc(badgeFor(cls)) + "</div></div>";
|
|
807
1030
|
}
|
|
808
1031
|
|
|
1032
|
+
// A stacked item ABOVE the floor base: the frame only, no label/badge
|
|
1033
|
+
// block underneath — stacking full cards (each carrying its own name +
|
|
1034
|
+
// class badge under the icon) put that text between every pair of icons,
|
|
1035
|
+
// reading as a large floating gap even though the CSS gap between the
|
|
1036
|
+
// card boxes themselves was always the intended small one. The base
|
|
1037
|
+
// item (bottom of the stack, standing on the floor) keeps its full
|
|
1038
|
+
// spriteCardHtml identification; every item resting on something else
|
|
1039
|
+
// shows an aria-label in its place, so the name is still available to
|
|
1040
|
+
// assistive tech even though the sighted layout stays compact.
|
|
1041
|
+
function stackedSpriteCardHtml(label, cls, svg) {
|
|
1042
|
+
return '<div class="sprite-card" aria-label="' + esc(label) + '"><div class="sprite-frame" data-cls="' + esc(cls) + '"><div class="sprite" data-cls="' + esc(cls) + '">' + svg + "</div></div></div>";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
809
1045
|
function captionFor(rows, state, here) {
|
|
810
1046
|
const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
|
|
811
1047
|
const lines = tmctAdventure.worldDigestRows(rows, state)
|
|
@@ -936,6 +1172,15 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
936
1172
|
chatqEl.value = btn.textContent;
|
|
937
1173
|
chatqEl.focus();
|
|
938
1174
|
});
|
|
1175
|
+
// A double-click runs the pill's own command immediately — the single
|
|
1176
|
+
// click above still only fills the input, so a visitor who meant to edit
|
|
1177
|
+
// it first loses nothing.
|
|
1178
|
+
pillsEl.addEventListener("dblclick", (e) => {
|
|
1179
|
+
const btn = e.target.closest(".pill");
|
|
1180
|
+
if (!btn || !chatqEl || !chatformEl) return;
|
|
1181
|
+
chatqEl.value = btn.textContent;
|
|
1182
|
+
chatformEl.requestSubmit();
|
|
1183
|
+
});
|
|
939
1184
|
|
|
940
1185
|
// ---- sprite resolution — property/instance-aware (sprite-templates.mjs's
|
|
941
1186
|
// resolveSpriteAsset), keyed on the OBJECT'S OWN NAME (via
|
|
@@ -954,12 +1199,33 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
954
1199
|
);
|
|
955
1200
|
}
|
|
956
1201
|
|
|
1202
|
+
// ---- the room-kind icon — filled through the SAME property-aware
|
|
1203
|
+
// resolver the sprite cards use, keyed on the room's own name, so a room
|
|
1204
|
+
// with its own large-tier template (library, kitchen, garden) shows it and
|
|
1205
|
+
// everything else falls back through room's own ancestor chain to the
|
|
1206
|
+
// generic room icon.
|
|
1207
|
+
function roomKindIconSvg(rows, here) {
|
|
1208
|
+
return tmctAdventure.resolveSpriteAsset(
|
|
1209
|
+
here, spriteAncestryRows(rows, here), factsForSubject(rows, here),
|
|
1210
|
+
activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY, { instanceKey: "roomkind-" + here },
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
957
1214
|
function redraw(snap) {
|
|
958
1215
|
lastSnapshot = snap;
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
1216
|
+
const layout = roomSceneLayout(snap.rows, snap.state, snap.here);
|
|
1217
|
+
wallRowEl.innerHTML = layout.wall.map((s) => spriteCardHtml(s.subject, s.spriteClass, resolveObjectSprite(snap.rows, s))).join("");
|
|
1218
|
+
floorRowEl.innerHTML = layout.floor.map((stack) => {
|
|
1219
|
+
const baseIndex = stack.items.length - 1;
|
|
1220
|
+
return '<div class="sprite-stack">' + stack.items.map((s, i) => {
|
|
1221
|
+
const svg = resolveObjectSprite(snap.rows, s);
|
|
1222
|
+
return i === baseIndex ? spriteCardHtml(s.subject, s.spriteClass, svg) : stackedSpriteCardHtml(s.subject, s.spriteClass, svg);
|
|
1223
|
+
}).join("") + "</div>";
|
|
1224
|
+
}).join("");
|
|
1225
|
+
youSlotEl.innerHTML = spriteCardHtml("you", "adventurer", resolveObjectSprite(snap.rows, { subject: "you", spriteClass: "adventurer" }));
|
|
962
1226
|
roomNameEl.textContent = "the " + snap.here;
|
|
1227
|
+
roomFrameEl.setAttribute("data-room-kind", roomKindForRoom(snap.rows, snap.here));
|
|
1228
|
+
roomKindIconEl.innerHTML = roomKindIconSvg(snap.rows, snap.here);
|
|
963
1229
|
captionEl.textContent = captionFor(snap.rows, snap.state, snap.here);
|
|
964
1230
|
turnLabelEl.textContent = "turn: " + snap.turn;
|
|
965
1231
|
renderPills(snap.rows, snap.state, snap.here);
|
|
@@ -1007,11 +1273,13 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1007
1273
|
|
|
1008
1274
|
function renderRoomDetail() {
|
|
1009
1275
|
if (!selectedRoomId) {
|
|
1276
|
+
roomDetailPanelEl.setAttribute("data-room-kind", "");
|
|
1010
1277
|
roomDetailTitleEl.textContent = "click a room";
|
|
1011
1278
|
roomDetailSpritesEl.innerHTML = "";
|
|
1012
1279
|
roomDetailCaptionEl.textContent = "";
|
|
1013
1280
|
return;
|
|
1014
1281
|
}
|
|
1282
|
+
roomDetailPanelEl.setAttribute("data-room-kind", roomKindForRoom(editRows, selectedRoomId));
|
|
1015
1283
|
roomDetailTitleEl.textContent = selectedRoomId;
|
|
1016
1284
|
const objects = roomSceneObjects(editRows, editState, selectedRoomId);
|
|
1017
1285
|
roomDetailSpritesEl.innerHTML = objects.length
|
|
@@ -1150,7 +1418,7 @@ ${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `
|
|
|
1150
1418
|
redraw(snap);
|
|
1151
1419
|
goalLineEl.textContent = "";
|
|
1152
1420
|
chatlogEl.innerHTML = "";
|
|
1153
|
-
statusEl.textContent =
|
|
1421
|
+
statusEl.textContent = "";
|
|
1154
1422
|
addChatLine("t", esc(ADVENTURE.world.opening || "the adventure begins."));
|
|
1155
1423
|
if (saved && snap.turn > 0) {
|
|
1156
1424
|
addChatLine("t", "resumed where you left off (turn " + snap.turn + ") \\u2014 progress kept best-effort on this device; reset starts the manor over.");
|