@polycode-projects/the-mechanical-code-talker 2.8.1 → 2.8.4
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/package.json +2 -1
- package/src/domain/game-config.mjs +10 -4
- package/src/domain/hanoi-lesson.mjs +53 -0
- package/src/domain/spider-fly-world.mjs +16 -0
- package/src/services/adventure-editor.mjs +361 -0
- package/src/services/adventure-viz.mjs +422 -51
- package/src/services/ledger-viz.mjs +239 -3
- package/src/services/plan-pddl.mjs +245 -0
- package/src/services/plan-viz.mjs +324 -67
- package/src/services/spider-fly-turn.mjs +120 -3
- package/src/services/spider-fly-viz.mjs +341 -22
- package/src/services/spider-fly.mjs +337 -143
- package/src/surfaces/web/adventure-browser-entry.mjs +34 -3
- package/src/surfaces/web/memory-ask-browser.bundle.js +10 -4
- package/src/surfaces/web/plan-browser-entry.mjs +114 -0
- package/src/surfaces/web/spider-fly-browser-entry.mjs +33 -1
|
@@ -61,10 +61,27 @@
|
|
|
61
61
|
// `visitedRoomIds`, the manual+auto-play exposure set
|
|
62
62
|
// adventure-browser-entry.mjs threads forward (see that module's header for
|
|
63
63
|
// why the two paths share one set rather than two).
|
|
64
|
+
//
|
|
65
|
+
// Edit mode (PLAN_GAMES_UPLIFT_V3.md Part C.4 item 4's operator addendum)
|
|
66
|
+
// adds: `allRoomIds` (every room the world defines, feeding the SAME
|
|
67
|
+
// `visitedRoomGraph` layout play mode's visited-only map already uses, just
|
|
68
|
+
// with the visitation filter dropped — a parameter, not a second map
|
|
69
|
+
// implementation), `suggestionsForTerm` (cursor-driven pills, calling
|
|
70
|
+
// adventure-editor.mjs's `wordBeforeCursor` plus skos-view.mjs's
|
|
71
|
+
// `relatedForTerm` and sprite-map.mjs's `classAncestorChain` — NOT spliced,
|
|
72
|
+
// for the same reason roomCaptionText/pillsForRoom aren't, see below), and
|
|
73
|
+
// `renderWorldEditorText`/`wordBeforeCursor` (adventure-editor.mjs's own
|
|
74
|
+
// splice-safe render/cursor helpers, spliced in directly). The store writes
|
|
75
|
+
// an edit implies run through the browser bundle's own `session.applyEdit`
|
|
76
|
+
// (adventure-browser-entry.mjs), never here — this module only renders and
|
|
77
|
+
// reads.
|
|
64
78
|
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
65
79
|
import { createTicker } from "./viz-ticker.mjs";
|
|
66
80
|
import { worldDigestRows, roomAffordances, foldWorldState } from "./adventure.mjs";
|
|
67
81
|
import { exposedFacts } from "./adventure-autoplay.mjs";
|
|
82
|
+
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
83
|
+
import { classAncestorChain } from "../domain/sprite-map.mjs";
|
|
84
|
+
import { renderWorldEditorText, wordBeforeCursor } from "./adventure-editor.mjs";
|
|
68
85
|
|
|
69
86
|
const DEFAULT_TITLE = "tmct — the adventure";
|
|
70
87
|
const PREVIEW_MAX_TICKS = 30;
|
|
@@ -220,6 +237,19 @@ export function visitedRoomGraph(state, visitedRoomIds) {
|
|
|
220
237
|
return { nodes, edges, hints };
|
|
221
238
|
}
|
|
222
239
|
|
|
240
|
+
/** Every room the world DEFINES at all (every subject the fact rows type as
|
|
241
|
+
* `room`), regardless of whether the current session has ever visited it —
|
|
242
|
+
* the edit mode's own "whole map" is exactly `visitedRoomGraph(state,
|
|
243
|
+
* allRoomIds(rows))`: the SAME directional-grid layout play mode's visited-
|
|
244
|
+
* only map uses, just fed every room instead of the exposure-filtered set,
|
|
245
|
+
* so a room with no visited neighbour still lays out (as a disconnected
|
|
246
|
+
* block) rather than vanishing. Pure. */
|
|
247
|
+
export function allRoomIds(rows) {
|
|
248
|
+
return [...new Set((rows || [])
|
|
249
|
+
.filter((r) => r.predicate === "rdf:type" && r.object === "room")
|
|
250
|
+
.map((r) => r.subject))];
|
|
251
|
+
}
|
|
252
|
+
|
|
223
253
|
/** One status line per exposed `mgx:is-objective` fact, using the SAME
|
|
224
254
|
* three-state logic adventure-autoplay.mjs's own goal inference already
|
|
225
255
|
* distinguishes, restricted to what `visitedRoomIds` actually supports:
|
|
@@ -260,6 +290,33 @@ export function pillsForRoom(rows, state, here) {
|
|
|
260
290
|
return roomAffordances(rows, state, here);
|
|
261
291
|
}
|
|
262
292
|
|
|
293
|
+
/** Edit mode's cursor-driven suggestion pills for one typed `term`: the
|
|
294
|
+
* lateral SKOS neighbourhood (`relatedForTerm`'s own synonyms/related
|
|
295
|
+
* concepts) plus the vertical rdfs:subClassOf ancestor chain
|
|
296
|
+
* (`classAncestorChain`, ancestors only — the term itself is dropped),
|
|
297
|
+
* deduplicated and capped so a well-connected term never floods the pill
|
|
298
|
+
* row. `[]` on an honest miss (neither lookup returns anything) — never a
|
|
299
|
+
* fabricated suggestion. NOT .toString()-splice-safe (it calls two other
|
|
300
|
+
* modules' exports) — the in-page script mirrors this same combination
|
|
301
|
+
* against the browser bundle's own `tmctAdventure.relatedForTerm`/
|
|
302
|
+
* `tmctAdventure.classAncestorChain`, the same reach-through-the-global
|
|
303
|
+
* pattern `pillsFor`/`captionFor` already use for their own adventure.mjs
|
|
304
|
+
* calls (see this module's header). */
|
|
305
|
+
export function suggestionsForTerm(rows, term) {
|
|
306
|
+
const t = String(term || "").trim().toLowerCase();
|
|
307
|
+
if (!t) return [];
|
|
308
|
+
const out = [];
|
|
309
|
+
const seen = new Set([t]);
|
|
310
|
+
const push = (label) => { if (label && !seen.has(label)) { seen.add(label); out.push(label); } };
|
|
311
|
+
const related = relatedForTerm(rows, t);
|
|
312
|
+
if (related) {
|
|
313
|
+
for (const syn of related.synonyms) push(syn);
|
|
314
|
+
for (const rel of related.related) push(rel.prefLabel);
|
|
315
|
+
}
|
|
316
|
+
for (const ancestor of classAncestorChain(t, rows).slice(1)) push(ancestor);
|
|
317
|
+
return out.slice(0, 8);
|
|
318
|
+
}
|
|
319
|
+
|
|
263
320
|
/** A short caption for `here`, built ONLY from the rows worldDigestRows
|
|
264
321
|
* itself already produces (the exact same view the chat reply's own digest
|
|
265
322
|
* reads) — every row already reads as a plain sentence
|
|
@@ -310,45 +367,104 @@ export function renderAdventureHtml({
|
|
|
310
367
|
<title>${escapeHtml(title)}</title>
|
|
311
368
|
<style>
|
|
312
369
|
${THEME_TOKENS_CSS}
|
|
370
|
+
/* RPG chrome tokens — layered over the shared neutrals/accents above,
|
|
371
|
+
never replacing them: taught/corpus/entail/alert stay the SAME
|
|
372
|
+
provenance palette every other tmct viz page uses, repurposed here as
|
|
373
|
+
the class-badge ring colors (hero/townsfolk/fixture/item) — a visitor
|
|
374
|
+
who already knows what green/blue/gold/red mean from ledger.html reads
|
|
375
|
+
this page's "class" system for free. */
|
|
376
|
+
:root { --parchment: #ECE1C8; --parchment-strong: #E1D2A6; --gilt: #93701F; }
|
|
377
|
+
@media (prefers-color-scheme: dark) { :root { --parchment: #241E12; --parchment-strong: #2C2416; --gilt: #C0A054; } }
|
|
378
|
+
:root[data-theme="dark"] { --parchment: #241E12; --parchment-strong: #2C2416; --gilt: #C0A054; }
|
|
379
|
+
:root[data-theme="light"] { --parchment: #ECE1C8; --parchment-strong: #E1D2A6; --gilt: #93701F; }
|
|
380
|
+
|
|
313
381
|
html { background: var(--bg); }
|
|
314
382
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
315
383
|
.mono { font-family: ${MONO_STACK}; }
|
|
316
|
-
main { max-width:
|
|
384
|
+
main { max-width: 920px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
|
|
317
385
|
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
318
|
-
|
|
386
|
+
.titlebar { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: .3rem 0 1rem; }
|
|
387
|
+
h1 { font-size: 1.4rem; margin: 0; text-wrap: balance; }
|
|
319
388
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
320
389
|
button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
|
321
|
-
.
|
|
390
|
+
.mode-toggle { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; padding: .4rem .8rem; border: 1px solid var(--gilt); background: var(--parchment); color: var(--ink); white-space: nowrap; }
|
|
391
|
+
.mode-toggle:hover:not(:disabled) { background: var(--parchment-strong); }
|
|
392
|
+
.mode-toggle:disabled { opacity: .5; cursor: default; }
|
|
393
|
+
|
|
394
|
+
.stage { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 1rem; align-items: start; }
|
|
322
395
|
@media (max-width: 760px) { .stage { grid-template-columns: 1fr; } }
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
396
|
+
|
|
397
|
+
/* the portrait frame — an ornate double-rule border with two small corner
|
|
398
|
+
fleurons (real glyphs, no external asset), sized to its OWN content
|
|
399
|
+
(the sprite row) rather than a fixed tall box, per operator feedback on
|
|
400
|
+
an earlier, mostly-empty version of this panel. */
|
|
401
|
+
.room-frame { position: relative; background: var(--parchment); border: 1px solid var(--gilt); outline: 1px solid var(--line); outline-offset: 4px; padding: 1.1rem; display: flex; flex-direction: column; gap: .6rem; }
|
|
402
|
+
.room-frame::before, .room-frame::after { content: "\\2766"; position: absolute; font-size: 1.1rem; color: var(--gilt); opacity: .85; line-height: 1; }
|
|
403
|
+
.room-frame::before { top: -.6rem; left: -.35rem; }
|
|
404
|
+
.room-frame::after { bottom: -.6rem; right: -.35rem; transform: rotate(180deg); }
|
|
405
|
+
.sprite-row { display: flex; flex-wrap: wrap; gap: .9rem .7rem; align-items: flex-start; min-height: 2.5rem; }
|
|
406
|
+
|
|
407
|
+
/* the class-badge system — this design's signature element: every sprite
|
|
408
|
+
gets a small ringed portrait frame plus a genre-flavored class word
|
|
409
|
+
(hero/townsfolk/fixture/item), colored through the SAME data-cls tokens
|
|
410
|
+
the rest of the site already uses for provenance. The sprite LABEL
|
|
411
|
+
underneath always names the real, specific thing (the cabinet, the
|
|
412
|
+
butler) — chrome around honest content, never instead of it. */
|
|
413
|
+
.sprite-card { display: flex; flex-direction: column; align-items: center; width: 62px; }
|
|
414
|
+
.sprite-frame { width: 52px; height: 52px; border-radius: 50%; background: var(--card); border: 2px solid var(--line); display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 7px; }
|
|
415
|
+
.sprite-frame[data-cls="adventurer"] { border-color: var(--taught); }
|
|
416
|
+
.sprite-frame[data-cls="person"] { border-color: var(--corpus); }
|
|
417
|
+
.sprite-frame[data-cls="container"], .sprite-frame[data-cls="furniture"] { border-color: var(--entail); }
|
|
418
|
+
.sprite-frame[data-cls="portable"] { border-color: var(--alert); }
|
|
419
|
+
.sprite-frame[data-cls="room"] { border-color: var(--muted); }
|
|
420
|
+
.sprite { width: 100%; height: 100%; }
|
|
326
421
|
.sprite svg { width: 100%; height: 100%; display: block; }
|
|
327
422
|
.sprite[data-cls="adventurer"] { color: var(--taught); }
|
|
328
423
|
.sprite[data-cls="person"] { color: var(--corpus); }
|
|
329
424
|
.sprite[data-cls="container"], .sprite[data-cls="furniture"] { color: var(--entail); }
|
|
330
425
|
.sprite[data-cls="portable"] { color: var(--alert); }
|
|
331
426
|
.sprite[data-cls="room"] { color: var(--muted); }
|
|
332
|
-
.sprite-label { font-
|
|
333
|
-
.
|
|
427
|
+
.sprite-label { font-size: .74rem; text-align: center; color: var(--ink); margin-top: .3rem; line-height: 1.15; }
|
|
428
|
+
.class-badge { font-family: ${MONO_STACK}; font-size: .55rem; letter-spacing: .06em; text-transform: uppercase; margin-top: .15rem; padding: .04rem .4rem; border-radius: 999px; border: 1px solid var(--muted); color: var(--muted); }
|
|
429
|
+
.class-badge[data-cls="adventurer"] { border-color: var(--taught); color: var(--taught); }
|
|
430
|
+
.class-badge[data-cls="person"] { border-color: var(--corpus); color: var(--corpus); }
|
|
431
|
+
.class-badge[data-cls="container"], .class-badge[data-cls="furniture"] { border-color: var(--entail); color: var(--entail); }
|
|
432
|
+
.class-badge[data-cls="portable"] { border-color: var(--alert); color: var(--alert); }
|
|
433
|
+
|
|
334
434
|
.side { display: flex; flex-direction: column; gap: .8rem; min-width: 0; }
|
|
335
|
-
.chat, .panel { background: var(--card); border: 1px solid var(--line); padding: .
|
|
336
|
-
.chat h2, .panel h2 { font-family: ${
|
|
435
|
+
.chat, .panel { background: var(--card); border: 1px solid var(--line); border-top: 2px solid var(--gilt); padding: .65rem .75rem; }
|
|
436
|
+
.chat h2, .panel h2 { font-family: ${SERIF_STACK}; font-variant: small-caps; font-size: .8rem; letter-spacing: .06em; color: var(--gilt); font-weight: 600; margin: 0 0 .5rem; padding-bottom: .3rem; border-bottom: 1px solid var(--line); }
|
|
337
437
|
.empty-note { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); }
|
|
338
438
|
.chips { display: flex; flex-wrap: wrap; gap: .35rem; }
|
|
339
439
|
.chip { font-family: ${MONO_STACK}; font-size: .72rem; padding: .25rem .6rem; border: 1px solid var(--line); background: var(--bg); color: var(--ink); border-radius: 999px; }
|
|
340
|
-
|
|
440
|
+
|
|
441
|
+
/* the manor map — a FIXED-size viewport regardless of room count or
|
|
442
|
+
layout shape (an operator report: an earlier version's container grew
|
|
443
|
+
and shrank with the graph, shoving the panels below it around as the
|
|
444
|
+
game progressed). The svg scales to fit via preserveAspectRatio, same
|
|
445
|
+
graph, same nodes, just letterboxed into a stable box. */
|
|
446
|
+
/* the wrapper div gets an explicit height too, not just .map-viewport
|
|
447
|
+
itself — an svg's own height:100% resolves against ITS PARENT's
|
|
448
|
+
height, and a percentage against an auto-height parent computes to
|
|
449
|
+
"auto" per spec, silently undoing the fixed-viewport intent below. */
|
|
450
|
+
.map-viewport { height: 190px; overflow: hidden; display: flex; align-items: center; justify-content: center; }
|
|
451
|
+
.map-viewport > div { width: 100%; height: 100%; }
|
|
452
|
+
.map-viewport svg { width: 100%; height: 100%; display: block; }
|
|
341
453
|
.roommap .room-edge { stroke: var(--line); stroke-width: 1.5; }
|
|
342
454
|
.roommap .room-hint { fill: var(--muted); opacity: .45; }
|
|
343
455
|
.roommap .room-node circle { fill: var(--card); stroke: var(--line); stroke-width: 1.5; }
|
|
344
456
|
.roommap .room-node.current circle { stroke: var(--taught); fill: var(--taught-soft); stroke-width: 2; }
|
|
345
457
|
.roommap .room-node text { font-family: ${MONO_STACK}; font-size: 6.5px; fill: var(--ink); text-anchor: middle; }
|
|
458
|
+
.roommap .room-node.clickable { cursor: pointer; }
|
|
459
|
+
.roommap .room-node.clickable:hover circle { stroke: var(--gilt); stroke-width: 2.5; }
|
|
460
|
+
.roommap .room-node.selected circle { stroke: var(--alert); stroke-width: 2.5; }
|
|
461
|
+
|
|
346
462
|
.goal-status { display: flex; flex-direction: column; gap: .4rem; }
|
|
347
463
|
.goal-status .g { display: flex; align-items: baseline; gap: .4rem; font-size: .86rem; line-height: 1.35; }
|
|
348
464
|
.goal-status .dot { width: .5rem; height: .5rem; border-radius: 50%; flex: none; background: var(--muted); }
|
|
349
465
|
.goal-status .g.known .dot { background: var(--corpus); }
|
|
350
466
|
.goal-status .g.carried .dot { background: var(--taught); }
|
|
351
|
-
.chatlog { display: flex; flex-direction: column; gap: .4rem; max-height:
|
|
467
|
+
.chatlog { display: flex; flex-direction: column; gap: .4rem; max-height: 260px; overflow-y: auto; margin-bottom: .5rem; }
|
|
352
468
|
.chatlog .u { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); }
|
|
353
469
|
.chatlog .u::before { content: "tmct> "; color: var(--taught); }
|
|
354
470
|
.chatlog .a { font-size: .88rem; line-height: 1.4; white-space: pre-wrap; }
|
|
@@ -358,6 +474,11 @@ ${THEME_TOKENS_CSS}
|
|
|
358
474
|
.pills:empty { display: none; margin-bottom: 0; }
|
|
359
475
|
.pill { font-family: ${MONO_STACK}; font-size: .72rem; padding: .25rem .6rem; border: 1px solid var(--line); background: var(--bg); color: var(--ink); border-radius: 999px; }
|
|
360
476
|
.pill:hover { border-color: var(--taught); }
|
|
477
|
+
/* the room's own prose, relocated (operator feedback) out of a bottom-of-
|
|
478
|
+
page strip and into the flow between the history/pills and the input —
|
|
479
|
+
a quoted manuscript line reporting what "look" would say right now. */
|
|
480
|
+
.caption { background: var(--parchment); border-left: 3px solid var(--gilt); padding: .55rem .7rem; font-size: .86rem; font-style: italic; margin: 0 0 .5rem; }
|
|
481
|
+
.caption:empty { display: none; margin: 0; }
|
|
361
482
|
.chatask { display: flex; align-items: center; gap: .5rem; border-top: 1px solid var(--line); padding-top: .5rem; }
|
|
362
483
|
.chatask .prompt { color: var(--taught); font-size: .78rem; font-family: ${MONO_STACK}; }
|
|
363
484
|
.chatask input { flex: 1; font-family: ${MONO_STACK}; font-size: .78rem; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .32rem .55rem; min-width: 0; }
|
|
@@ -369,47 +490,87 @@ ${THEME_TOKENS_CSS}
|
|
|
369
490
|
.controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
370
491
|
.goal-line { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); margin-top: .5rem; }
|
|
371
492
|
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .3rem; }
|
|
493
|
+
|
|
494
|
+
/* edit mode */
|
|
495
|
+
body:not(.editing) #editStage { display: none; }
|
|
496
|
+
body.editing #playStage, body.editing #playControls { display: none; }
|
|
497
|
+
.editor-stage { margin-top: .5rem; }
|
|
498
|
+
.edittext textarea { width: 100%; box-sizing: border-box; min-height: 380px; font-family: ${MONO_STACK}; font-size: .82rem; line-height: 1.55; background: var(--bg); color: var(--ink); border: 1px solid var(--line); padding: .6rem; resize: vertical; }
|
|
499
|
+
.edit-status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .4rem; min-height: 1.1em; }
|
|
500
|
+
.edit-status.pending { color: var(--entail); }
|
|
501
|
+
.edit-status.ok { color: var(--taught); }
|
|
502
|
+
.roomdetail .sprite-row { min-height: 3.2rem; }
|
|
503
|
+
#legendList { display: flex; flex-wrap: wrap; gap: .5rem .3rem; }
|
|
504
|
+
|
|
372
505
|
body.preview .side, body.preview .controls-row, body.preview .status { display: none; }
|
|
373
506
|
body.preview main { padding: 0; max-width: none; }
|
|
374
507
|
body.preview .stage { display: block; }
|
|
375
|
-
body.preview .eyebrow, body.preview h1 { display: none; }
|
|
508
|
+
body.preview .eyebrow, body.preview h1, body.preview .mode-toggle, body.preview #editStage { display: none; }
|
|
376
509
|
</style>
|
|
377
510
|
</head>
|
|
378
511
|
<body>
|
|
379
512
|
<main>
|
|
380
513
|
<div class="eyebrow">tmct · the adventure</div>
|
|
381
|
-
<
|
|
382
|
-
|
|
514
|
+
<div class="titlebar">
|
|
515
|
+
<h1>A room, drawn from exactly what the text already says is there</h1>
|
|
516
|
+
<button id="editModeBtn" type="button" class="mode-toggle" disabled>edit the world</button>
|
|
517
|
+
</div>
|
|
518
|
+
<div class="stage" id="playStage">
|
|
383
519
|
<div class="room-frame" id="roomFrame">
|
|
384
520
|
<div class="sprite-row" id="spriteRow"></div>
|
|
385
521
|
</div>
|
|
386
522
|
<aside class="side" aria-label="The adventure's log and chat">
|
|
387
523
|
<div class="chat">
|
|
388
|
-
<h2>
|
|
524
|
+
<h2>the manor's own account</h2>
|
|
389
525
|
<div class="chatlog" id="chatlog" aria-live="polite"></div>
|
|
390
526
|
<div class="pills" id="pills"></div>
|
|
527
|
+
<div class="caption" id="caption"></div>
|
|
391
528
|
<form class="chatask" id="chatform">
|
|
392
529
|
<span class="prompt mono">tmct></span>
|
|
393
530
|
<input id="chatq" type="text" placeholder="go north" aria-label="Type a command, or ask a question" disabled>
|
|
394
531
|
</form>
|
|
395
532
|
</div>
|
|
396
533
|
<div class="panel carrying">
|
|
397
|
-
<h2>
|
|
534
|
+
<h2>satchel</h2>
|
|
398
535
|
<div class="chips" id="carryList"></div>
|
|
399
536
|
</div>
|
|
400
537
|
<div class="panel roommap">
|
|
401
|
-
<h2>
|
|
402
|
-
<div id="mapWrap"></div>
|
|
538
|
+
<h2>the manor, so far</h2>
|
|
539
|
+
<div class="map-viewport"><div id="mapWrap"></div></div>
|
|
403
540
|
</div>
|
|
404
541
|
<div class="panel goals">
|
|
405
|
-
<h2>
|
|
542
|
+
<h2>quest</h2>
|
|
406
543
|
<div id="goalList"></div>
|
|
407
544
|
</div>
|
|
408
545
|
</aside>
|
|
409
546
|
</div>
|
|
410
|
-
|
|
547
|
+
|
|
548
|
+
<div class="stage editor-stage" id="editStage" aria-label="The world editor">
|
|
549
|
+
<div class="panel edittext">
|
|
550
|
+
<h2>the world, in plain sentences</h2>
|
|
551
|
+
<textarea id="editorText" spellcheck="false" aria-label="The world's own facts as plain sentences, one per line — edit one and it flows back into the running world"></textarea>
|
|
552
|
+
<div class="pills" id="editorPills" aria-label="Ontologically related terms for the word before the cursor"></div>
|
|
553
|
+
<div class="edit-status" id="editorStatus"></div>
|
|
554
|
+
</div>
|
|
555
|
+
<aside class="side" aria-label="The whole manor, a clicked room, and the legend">
|
|
556
|
+
<div class="panel roommap">
|
|
557
|
+
<h2>the whole manor</h2>
|
|
558
|
+
<div class="map-viewport"><div id="editMapWrap"></div></div>
|
|
559
|
+
</div>
|
|
560
|
+
<div class="panel roomdetail">
|
|
561
|
+
<h2 id="roomDetailTitle">click a room</h2>
|
|
562
|
+
<div class="sprite-row" id="roomDetailSprites"></div>
|
|
563
|
+
<div class="caption" id="roomDetailCaption"></div>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="panel legend">
|
|
566
|
+
<h2>legend</h2>
|
|
567
|
+
<div id="legendList"></div>
|
|
568
|
+
</div>
|
|
569
|
+
</aside>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
411
572
|
<div class="goal-line" id="goalLine"></div>
|
|
412
|
-
<div class="controls-row">
|
|
573
|
+
<div class="controls-row" id="playControls">
|
|
413
574
|
<button id="resetBtn" type="button" disabled>reset</button>
|
|
414
575
|
<button id="playBtn" type="button" disabled>▶ play</button>
|
|
415
576
|
<button id="stepBtn" type="button" disabled>step</button>
|
|
@@ -430,8 +591,11 @@ const ADVENTURE = ${pageData};
|
|
|
430
591
|
const roomSceneObjects = ${roomSceneObjects.toString()};
|
|
431
592
|
const carriedItems = ${carriedItems.toString()};
|
|
432
593
|
const visitedRoomGraph = ${visitedRoomGraph.toString()};
|
|
594
|
+
const allRoomIds = ${allRoomIds.toString()};
|
|
433
595
|
const spriteAncestryRows = ${spriteAncestryRows.toString()};
|
|
434
596
|
const factsForSubject = ${factsForSubject.toString()};
|
|
597
|
+
const renderWorldEditorText = ${renderWorldEditorText.toString()};
|
|
598
|
+
const wordBeforeCursor = ${wordBeforeCursor.toString()};
|
|
435
599
|
const esc = ${escapeHtml.toString()};
|
|
436
600
|
const el = (id) => document.getElementById(id);
|
|
437
601
|
const spriteRow = el("spriteRow");
|
|
@@ -449,6 +613,15 @@ const ADVENTURE = ${pageData};
|
|
|
449
613
|
const carryListEl = el("carryList");
|
|
450
614
|
const mapWrapEl = el("mapWrap");
|
|
451
615
|
const goalListEl = el("goalList");
|
|
616
|
+
const editModeBtn = el("editModeBtn");
|
|
617
|
+
const editorTextEl = el("editorText");
|
|
618
|
+
const editorPillsEl = el("editorPills");
|
|
619
|
+
const editorStatusEl = el("editorStatus");
|
|
620
|
+
const editMapWrapEl = el("editMapWrap");
|
|
621
|
+
const roomDetailTitleEl = el("roomDetailTitle");
|
|
622
|
+
const roomDetailSpritesEl = el("roomDetailSprites");
|
|
623
|
+
const roomDetailCaptionEl = el("roomDetailCaption");
|
|
624
|
+
const legendListEl = el("legendList");
|
|
452
625
|
|
|
453
626
|
const params = new URLSearchParams(location.search);
|
|
454
627
|
const preview = params.get("preview") === "1";
|
|
@@ -456,6 +629,51 @@ const ADVENTURE = ${pageData};
|
|
|
456
629
|
|
|
457
630
|
let session = null;
|
|
458
631
|
let lastTicks = 0;
|
|
632
|
+
let lastSnapshot = null;
|
|
633
|
+
let selectedRoomId = null;
|
|
634
|
+
let editRows = [];
|
|
635
|
+
let editState = { placements: new Map(), openness: new Map(), exits: new Map() };
|
|
636
|
+
let allStoreRows = [];
|
|
637
|
+
|
|
638
|
+
// ---- serialize every engine-touching call: the ticker, the chat dock and
|
|
639
|
+
// the editor sync all share one in-memory store, and any overlapping pair
|
|
640
|
+
// could race against the same write.
|
|
641
|
+
let lock = Promise.resolve();
|
|
642
|
+
function withLock(fn) {
|
|
643
|
+
const run = lock.then(fn, fn);
|
|
644
|
+
lock = run.catch(() => {});
|
|
645
|
+
return run;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---- large-sprite-tier wiring — fetches the gradient-shaded 400px tier
|
|
649
|
+
// (public/sprites-pack/manifest.json, built by scripts/build-demo-sprites-
|
|
650
|
+
// pack.mjs from data/sprites-large/*.toml) at page load, resolved through
|
|
651
|
+
// the exact same sprite-templates.mjs resolver the build-time icon tier
|
|
652
|
+
// already used. A failed fetch (offline preview, a stripped static host)
|
|
653
|
+
// just leaves the build-time icon tier (ADVENTURE.spriteTemplates) as the
|
|
654
|
+
// working fallback — this page is never blank because one asset had a
|
|
655
|
+
// hiccup, the same posture this project already takes elsewhere.
|
|
656
|
+
let activeSpriteTemplates = ADVENTURE.spriteTemplates;
|
|
657
|
+
fetch("./sprites-pack/manifest.json").then((r) => (r && r.ok ? r.json() : null)).then((data) => {
|
|
658
|
+
if (!data || !Array.isArray(data.templates) || !data.templates.length) return;
|
|
659
|
+
activeSpriteTemplates = data.templates;
|
|
660
|
+
if (lastSnapshot) redraw(lastSnapshot);
|
|
661
|
+
if (document.body.classList.contains("editing")) refreshEditPanels();
|
|
662
|
+
}).catch(() => { /* ADVENTURE.spriteTemplates already covers this page */ });
|
|
663
|
+
|
|
664
|
+
// ---- the class-badge system — this redesign's signature element: every
|
|
665
|
+
// sprite gets a small genre-flavored class word underneath its own real
|
|
666
|
+
// name, colored through the same data-cls tokens the rest of the site
|
|
667
|
+
// already uses for provenance (see the CSS block's own comment). Chrome
|
|
668
|
+
// wraps the content; the content itself — the sprite's real subject name,
|
|
669
|
+
// the caption text — is never replaced by it.
|
|
670
|
+
const CLASS_BADGE = { adventurer: "hero", person: "townsfolk", container: "fixture", furniture: "fixture", portable: "item", room: "room" };
|
|
671
|
+
const badgeFor = (cls) => CLASS_BADGE[cls] || cls;
|
|
672
|
+
function spriteCardHtml(label, cls, svg) {
|
|
673
|
+
return '<div class="sprite-card"><div class="sprite-frame" data-cls="' + esc(cls) + '"><div class="sprite" data-cls="' + esc(cls) + '">' + svg + '</div></div>'
|
|
674
|
+
+ '<div class="sprite-label">' + esc(label) + '</div>'
|
|
675
|
+
+ '<div class="class-badge" data-cls="' + esc(cls) + '">' + esc(badgeFor(cls)) + "</div></div>";
|
|
676
|
+
}
|
|
459
677
|
|
|
460
678
|
function captionFor(rows, state, here) {
|
|
461
679
|
const hereCased = here.charAt(0).toUpperCase() + here.slice(1);
|
|
@@ -494,13 +712,14 @@ const ADVENTURE = ${pageData};
|
|
|
494
712
|
: '<span class="empty-note">nothing yet</span>';
|
|
495
713
|
}
|
|
496
714
|
|
|
497
|
-
// ----
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
715
|
+
// ---- room map — ONE svg-building routine shared by play mode's visited-
|
|
716
|
+
// only map and edit mode's whole-map (visitedRoomGraph fed allRoomIds
|
|
717
|
+
// instead of the exposure set — a parameter, not a second layout), so the
|
|
718
|
+
// fixed-size-viewport CSS treatment and the node layout can never drift
|
|
719
|
+
// between the two. clickable adds a data-room attribute and a pointer
|
|
720
|
+
// cursor per node; play mode's own map stays purely informational.
|
|
721
|
+
function roomMapSvg(graph, clickable) {
|
|
722
|
+
if (!graph.nodes.length) return null;
|
|
504
723
|
const cell = 56, radius = 15;
|
|
505
724
|
const maxX = Math.max.apply(null, graph.nodes.map((n) => n.x));
|
|
506
725
|
const maxY = Math.max.apply(null, graph.nodes.map((n) => n.y));
|
|
@@ -519,13 +738,27 @@ const ADVENTURE = ${pageData};
|
|
|
519
738
|
return '<circle class="room-hint" cx="' + (cx(from) + d[0] * cell * 0.42) + '" cy="' + (cy(from) + d[1] * cell * 0.42) + '" r="3"></circle>';
|
|
520
739
|
}).join("");
|
|
521
740
|
const nodesSvg = graph.nodes.map((n) => {
|
|
522
|
-
const cls = "room-node" + (n.current ? " current" : "");
|
|
523
|
-
|
|
741
|
+
const cls = "room-node" + (n.current ? " current" : "") + (clickable ? " clickable" : "") + (clickable && n.id === selectedRoomId ? " selected" : "");
|
|
742
|
+
const attr = clickable ? ' data-room="' + esc(n.id) + '"' : "";
|
|
743
|
+
return '<g class="' + cls + '"' + attr + '><circle cx="' + cx(n) + '" cy="' + cy(n) + '" r="' + radius + '"></circle>'
|
|
524
744
|
+ '<text x="' + cx(n) + '" y="' + (cy(n) + radius + 9) + '">' + esc(n.id) + "</text></g>";
|
|
525
745
|
}).join("");
|
|
526
|
-
|
|
746
|
+
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") + '">'
|
|
527
747
|
+ edgesSvg + hintsSvg + nodesSvg + "</svg>";
|
|
528
748
|
}
|
|
749
|
+
function renderRoomMap(rows, state, visitedRoomIds) {
|
|
750
|
+
mapWrapEl.innerHTML = roomMapSvg(visitedRoomGraph(state, visitedRoomIds), false) || '<span class="empty-note">nowhere yet</span>';
|
|
751
|
+
}
|
|
752
|
+
function renderEditMap(rows, state) {
|
|
753
|
+
editMapWrapEl.innerHTML = roomMapSvg(visitedRoomGraph(state, allRoomIds(rows)), true) || '<span class="empty-note">this world defines no rooms</span>';
|
|
754
|
+
}
|
|
755
|
+
editMapWrapEl.addEventListener("click", (e) => {
|
|
756
|
+
const g = e.target.closest("[data-room]");
|
|
757
|
+
if (!g) return;
|
|
758
|
+
selectedRoomId = g.getAttribute("data-room");
|
|
759
|
+
renderEditMap(editRows, editState);
|
|
760
|
+
renderRoomDetail();
|
|
761
|
+
});
|
|
529
762
|
|
|
530
763
|
// ---- goal panel — one line per exposed objective, a colored dot carrying
|
|
531
764
|
// the same three-state read goalStatusLinesFor's own status field names.
|
|
@@ -572,28 +805,25 @@ const ADVENTURE = ${pageData};
|
|
|
572
805
|
// ---- sprite resolution — property/instance-aware (sprite-templates.mjs's
|
|
573
806
|
// resolveSpriteAsset), keyed on the OBJECT'S OWN NAME (via
|
|
574
807
|
// spriteAncestryRows' synthetic subClassOf edge to its declared class) so
|
|
575
|
-
// a named object (cabinet, butler) can carry its own data/sprites
|
|
808
|
+
// a named object (cabinet, butler) can carry its own data/sprites*.toml
|
|
576
809
|
// template while the data-cls attribute still reflects the DECLARED class
|
|
577
810
|
// (container/furniture/portable/person/room) — the CSS accent-color rules
|
|
578
811
|
// stay keyed on that declared class unchanged. The player's own "you" row
|
|
579
812
|
// has no backing fact row, so it resolves directly by its fixed
|
|
580
813
|
// "adventurer" class with no ancestry/property rows to check.
|
|
581
814
|
function resolveObjectSprite(rows, s) {
|
|
582
|
-
if (s.subject === "you") return tmctAdventure.resolveSpriteAsset("adventurer", [], [],
|
|
815
|
+
if (s.subject === "you") return tmctAdventure.resolveSpriteAsset("adventurer", [], [], activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY, { instanceKey: "you" });
|
|
583
816
|
return tmctAdventure.resolveSpriteAsset(
|
|
584
817
|
s.subject, spriteAncestryRows(rows, s.subject), factsForSubject(rows, s.subject),
|
|
585
|
-
|
|
818
|
+
activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY, { instanceKey: s.subject },
|
|
586
819
|
);
|
|
587
820
|
}
|
|
588
821
|
|
|
589
822
|
function redraw(snap) {
|
|
823
|
+
lastSnapshot = snap;
|
|
590
824
|
const objects = roomSceneObjects(snap.rows, snap.state, snap.here);
|
|
591
825
|
const sprites = [{ subject: "you", spriteClass: "adventurer" }, ...objects];
|
|
592
|
-
spriteRow.innerHTML = sprites.map((s) =>
|
|
593
|
-
const svg = resolveObjectSprite(snap.rows, s);
|
|
594
|
-
return '<div><div class="sprite" data-cls="' + esc(s.spriteClass) + '">' + svg + '</div>'
|
|
595
|
-
+ '<div class="sprite-label">' + esc(s.subject) + "</div></div>";
|
|
596
|
-
}).join("");
|
|
826
|
+
spriteRow.innerHTML = sprites.map((s) => spriteCardHtml(s.subject, s.spriteClass, resolveObjectSprite(snap.rows, s))).join("");
|
|
597
827
|
captionEl.textContent = captionFor(snap.rows, snap.state, snap.here);
|
|
598
828
|
turnLabelEl.textContent = "turn: " + snap.turn;
|
|
599
829
|
renderPills(snap.rows, snap.state, snap.here);
|
|
@@ -602,16 +832,6 @@ const ADVENTURE = ${pageData};
|
|
|
602
832
|
renderGoals(snap.rows, snap.state, snap.visitedRoomIds);
|
|
603
833
|
}
|
|
604
834
|
|
|
605
|
-
// ---- serialize every engine-touching call: the ticker and the chat dock
|
|
606
|
-
// share one in-memory store, and an overlapping tick()/turn() pair could
|
|
607
|
-
// race against the same @turnN write.
|
|
608
|
-
let lock = Promise.resolve();
|
|
609
|
-
function withLock(fn) {
|
|
610
|
-
const run = lock.then(fn, fn);
|
|
611
|
-
lock = run.catch(() => {});
|
|
612
|
-
return run;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
835
|
chatformEl.addEventListener("submit", (e) => {
|
|
616
836
|
e.preventDefault();
|
|
617
837
|
const q = chatqEl.value.trim();
|
|
@@ -629,6 +849,157 @@ const ADVENTURE = ${pageData};
|
|
|
629
849
|
});
|
|
630
850
|
});
|
|
631
851
|
|
|
852
|
+
// ---- edit mode ---------------------------------------------------------
|
|
853
|
+
//
|
|
854
|
+
// The textarea is seeded from renderWorldEditorText over the world's OWN
|
|
855
|
+
// facts (session.applyEdit's own return already scopes writes to the
|
|
856
|
+
// world's provenance tag; here we scope READS the same way — the live
|
|
857
|
+
// store also carries the default persona's background corpus, and this
|
|
858
|
+
// page must never show, or risk retracting, a fact that isn't part of
|
|
859
|
+
// THIS world). Typing debounces two independent things at different
|
|
860
|
+
// paces: cursor-suggestion pills (fast, ~180ms — cheap, in-memory lookups
|
|
861
|
+
// per the investigation behind this feature) and the actual store sync
|
|
862
|
+
// (slower, ~400ms — so a mid-word keystroke state is never what gets
|
|
863
|
+
// read as "the user's intent"). session.applyEdit does the real parse +
|
|
864
|
+
// diff + write; this page only ever reads its result back.
|
|
865
|
+
|
|
866
|
+
function worldOnlyRows(rows) {
|
|
867
|
+
const prefix = "world:" + ADVENTURE.world.name;
|
|
868
|
+
return (rows || []).filter((r) => typeof r.provenance === "string" && r.provenance.indexOf(prefix) === 0);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function renderRoomDetail() {
|
|
872
|
+
if (!selectedRoomId) {
|
|
873
|
+
roomDetailTitleEl.textContent = "click a room";
|
|
874
|
+
roomDetailSpritesEl.innerHTML = "";
|
|
875
|
+
roomDetailCaptionEl.textContent = "";
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
roomDetailTitleEl.textContent = selectedRoomId;
|
|
879
|
+
const objects = roomSceneObjects(editRows, editState, selectedRoomId);
|
|
880
|
+
roomDetailSpritesEl.innerHTML = objects.length
|
|
881
|
+
? objects.map((o) => spriteCardHtml(o.subject, o.spriteClass, resolveObjectSprite(editRows, o))).join("")
|
|
882
|
+
: '<span class="empty-note">nothing placed here yet</span>';
|
|
883
|
+
roomDetailCaptionEl.textContent = captionFor(editRows, editState, selectedRoomId);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// ---- legend — every known object/character class this world actually
|
|
887
|
+
// uses, by its real icon. Scoped to worldOnlyRows so the background
|
|
888
|
+
// corpus's own (unrelated) rdf:type vocabulary never floods the list.
|
|
889
|
+
function renderLegend(rows) {
|
|
890
|
+
const subjects = new Set(rows.filter((r) => r.predicate === "rdf:type").map((r) => r.subject));
|
|
891
|
+
const classes = new Set(["adventurer"]);
|
|
892
|
+
subjects.forEach((s) => { if (s !== "player") classes.add(spriteClassForObject(rows, s)); });
|
|
893
|
+
legendListEl.innerHTML = Array.from(classes).sort().map((cls) => {
|
|
894
|
+
const svg = tmctAdventure.resolveSpriteAsset(cls, [], [], activeSpriteTemplates, tmctAdventure.SPRITE_REGISTRY, { instanceKey: "legend-" + cls });
|
|
895
|
+
return spriteCardHtml(cls, cls, svg);
|
|
896
|
+
}).join("");
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function refreshEditPanels() {
|
|
900
|
+
renderEditMap(editRows, editState);
|
|
901
|
+
renderLegend(editRows);
|
|
902
|
+
renderRoomDetail();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function wordRangeBeforeCursor(text, cursorPos) {
|
|
906
|
+
const word = wordBeforeCursor(text, cursorPos);
|
|
907
|
+
return [cursorPos - word.length, cursorPos];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ---- cursor-driven suggestion pills — the lateral SKOS neighbourhood
|
|
911
|
+
// (tmctAdventure.relatedForTerm) plus the vertical is-a ancestor chain
|
|
912
|
+
// (tmctAdventure.classAncestorChain), mirroring adventure-viz.mjs's own
|
|
913
|
+
// suggestionsForTerm against the browser bundle's global (the same
|
|
914
|
+
// reach-through-the-global pattern captionFor/pillsFor already use).
|
|
915
|
+
// Reads the FULL store (allStoreRows), not worldOnlyRows — a term's
|
|
916
|
+
// synonym/related-concept facts mostly live in the default background
|
|
917
|
+
// corpus, not in Ashcombe Hall's own vocabulary. Empty on an honest miss.
|
|
918
|
+
function renderSuggestionPills() {
|
|
919
|
+
const term = wordBeforeCursor(editorTextEl.value, editorTextEl.selectionStart);
|
|
920
|
+
if (!term) { editorPillsEl.innerHTML = ""; return; }
|
|
921
|
+
const related = tmctAdventure.relatedForTerm(allStoreRows, term);
|
|
922
|
+
const chain = tmctAdventure.classAncestorChain(term, allStoreRows);
|
|
923
|
+
const seen = new Set([term]);
|
|
924
|
+
const out = [];
|
|
925
|
+
const push = (label) => { if (label && !seen.has(label)) { seen.add(label); out.push(label); } };
|
|
926
|
+
if (related) { related.synonyms.forEach(push); related.related.forEach((r) => push(r.prefLabel)); }
|
|
927
|
+
chain.slice(1).forEach(push);
|
|
928
|
+
editorPillsEl.innerHTML = out.slice(0, 8)
|
|
929
|
+
.map((s) => '<button type="button" class="pill" data-insert="' + esc(s) + '">' + esc(s) + "</button>").join("");
|
|
930
|
+
}
|
|
931
|
+
editorPillsEl.addEventListener("click", (e) => {
|
|
932
|
+
const btn = e.target.closest(".pill");
|
|
933
|
+
if (!btn) return;
|
|
934
|
+
const pos = editorTextEl.selectionStart;
|
|
935
|
+
const range = wordRangeBeforeCursor(editorTextEl.value, pos);
|
|
936
|
+
const value = editorTextEl.value;
|
|
937
|
+
const insert = btn.getAttribute("data-insert");
|
|
938
|
+
editorTextEl.value = value.slice(0, range[0]) + insert + value.slice(pos);
|
|
939
|
+
const newPos = range[0] + insert.length;
|
|
940
|
+
editorTextEl.setSelectionRange(newPos, newPos);
|
|
941
|
+
editorTextEl.focus();
|
|
942
|
+
onEditorChanged();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
async function applyEditorText() {
|
|
946
|
+
if (!session) return;
|
|
947
|
+
editorStatusEl.className = "edit-status pending";
|
|
948
|
+
editorStatusEl.textContent = "reading the manor\\u2019s ledger\\u2026";
|
|
949
|
+
const result = await session.applyEdit(editorTextEl.value);
|
|
950
|
+
const snap = await session.snapshot();
|
|
951
|
+
allStoreRows = snap.rows;
|
|
952
|
+
editRows = worldOnlyRows(snap.rows);
|
|
953
|
+
editState = tmctAdventure.foldWorldState(editRows);
|
|
954
|
+
if (result.unrecognized.length) {
|
|
955
|
+
const lines = result.unrecognized.map((u) => u.line).join(", ");
|
|
956
|
+
editorStatusEl.className = "edit-status pending";
|
|
957
|
+
editorStatusEl.textContent = result.unrecognized.length + " line" + (result.unrecognized.length === 1 ? "" : "s")
|
|
958
|
+
+ " not understood yet (line " + lines + ") \\u2014 left untouched until fixed.";
|
|
959
|
+
} else {
|
|
960
|
+
editorStatusEl.className = "edit-status ok";
|
|
961
|
+
editorStatusEl.textContent = (result.added || result.removed)
|
|
962
|
+
? "synced \\u2014 " + result.added + " fact(s) added, " + result.removed + " retracted."
|
|
963
|
+
: "synced \\u2014 no change.";
|
|
964
|
+
}
|
|
965
|
+
refreshEditPanels();
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
let suggestTimer = null;
|
|
969
|
+
let syncTimer = null;
|
|
970
|
+
function scheduleSuggestions() { clearTimeout(suggestTimer); suggestTimer = setTimeout(renderSuggestionPills, 180); }
|
|
971
|
+
function scheduleSync() { clearTimeout(syncTimer); syncTimer = setTimeout(() => withLock(applyEditorText), 400); }
|
|
972
|
+
function onEditorChanged() { scheduleSuggestions(); scheduleSync(); }
|
|
973
|
+
editorTextEl.addEventListener("input", onEditorChanged);
|
|
974
|
+
editorTextEl.addEventListener("click", scheduleSuggestions);
|
|
975
|
+
editorTextEl.addEventListener("keyup", (e) => {
|
|
976
|
+
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].indexOf(e.key) !== -1) scheduleSuggestions();
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
async function enterEditMode() {
|
|
980
|
+
ticker.pause();
|
|
981
|
+
const snap = await session.snapshot();
|
|
982
|
+
allStoreRows = snap.rows;
|
|
983
|
+
editRows = worldOnlyRows(snap.rows);
|
|
984
|
+
editState = tmctAdventure.foldWorldState(editRows);
|
|
985
|
+
editorTextEl.value = renderWorldEditorText(editRows, editState);
|
|
986
|
+
editorStatusEl.className = "edit-status";
|
|
987
|
+
editorStatusEl.textContent = "";
|
|
988
|
+
editorPillsEl.innerHTML = "";
|
|
989
|
+
selectedRoomId = null;
|
|
990
|
+
document.body.classList.add("editing");
|
|
991
|
+
refreshEditPanels();
|
|
992
|
+
editModeBtn.textContent = "back to playing";
|
|
993
|
+
}
|
|
994
|
+
async function exitEditMode() {
|
|
995
|
+
document.body.classList.remove("editing");
|
|
996
|
+
editModeBtn.textContent = "edit the world";
|
|
997
|
+
const snap = await session.snapshot();
|
|
998
|
+
redraw(snap);
|
|
999
|
+
}
|
|
1000
|
+
editModeBtn.addEventListener("click", () => withLock(() =>
|
|
1001
|
+
(document.body.classList.contains("editing") ? exitEditMode() : enterEditMode())));
|
|
1002
|
+
|
|
632
1003
|
async function boot() {
|
|
633
1004
|
session = await tmctAdventure.createAdventureSession(ADVENTURE.world);
|
|
634
1005
|
lastTicks = 0;
|
|
@@ -639,7 +1010,7 @@ const ADVENTURE = ${pageData};
|
|
|
639
1010
|
statusEl.textContent = ADVENTURE.world.opening || "";
|
|
640
1011
|
addChatLine("t", esc(ADVENTURE.world.opening || "the adventure begins."));
|
|
641
1012
|
chatqEl.disabled = false;
|
|
642
|
-
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false;
|
|
1013
|
+
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false; editModeBtn.disabled = false;
|
|
643
1014
|
}
|
|
644
1015
|
|
|
645
1016
|
const ticker = createTicker({
|