@polycode-projects/the-mechanical-code-talker 2.8.13 → 2.9.3
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 +50 -28
- package/bin/tmct.mjs +176 -31
- package/corpus/LICENSES.json +7 -0
- package/corpus/sprites/src/sprite-facts.jsonl +1033 -0
- package/corpus/worlds/manifest.json +9 -9
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +3 -0
- package/corpus/worlds/src/spider-fly.jsonl +17 -0
- package/package.json +37 -34
- package/src/adapters/corpus/wikipedia-live.mjs +145 -0
- package/src/adapters/memory/core.mjs +77 -12
- package/src/adapters/toml-config.mjs +6 -5
- package/src/domain/cli-verbs.mjs +4 -2
- package/src/domain/hanoi-lesson.mjs +10 -0
- package/src/domain/reference-pack.mjs +102 -0
- package/src/domain/spider-fly-world.mjs +54 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/services/adventure-viz.mjs +208 -66
- package/src/services/chat-page-viz.mjs +366 -37
- package/src/services/chat-session.mjs +38 -22
- package/src/services/chat.mjs +199 -20
- package/src/services/fold.mjs +28 -44
- package/src/services/import-file.mjs +7 -6
- package/src/services/init.mjs +10 -5
- package/src/services/ledger-viz.mjs +25 -34
- package/src/services/plan-viz.mjs +22 -26
- package/src/services/sessions.mjs +15 -3
- package/src/services/spider-fly-viz.mjs +52 -49
- package/src/services/sprite-catalog-viz.mjs +187 -3
- package/src/services/viz-theme.mjs +8 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +23 -10
- package/src/surfaces/web/chat-browser-entry.mjs +17 -2
- package/src/surfaces/web/idb-persist.mjs +115 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +155 -25927
- package/src/surfaces/web/sprites-browser-entry.mjs +68 -0
- package/src/tools/memory-fallthrough.mjs +11 -4
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
// an edit implies run through the browser bundle's own `session.applyEdit`
|
|
76
76
|
// (adventure-browser-entry.mjs), never here — this module only renders and
|
|
77
77
|
// reads.
|
|
78
|
-
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
78
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
79
79
|
import { createTicker } from "./viz-ticker.mjs";
|
|
80
80
|
import { worldDigestRows, roomAffordances, foldWorldState } from "./adventure.mjs";
|
|
81
81
|
import { exposedFacts } from "./adventure-autoplay.mjs";
|
|
@@ -344,19 +344,32 @@ export function roomCaptionText(rows, state, here) {
|
|
|
344
344
|
* templates travel as JSON rather than as a bundled fs read. Defaults to
|
|
345
345
|
* `[]` (every sprite falls back to the flat SPRITE_REGISTRY, unchanged from
|
|
346
346
|
* before this module existed) so existing callers that don't pass one keep
|
|
347
|
-
* working.
|
|
348
|
-
*
|
|
349
|
-
*
|
|
347
|
+
* working. `largeSpriteTemplates` is the gradient-shaded 400px tier
|
|
348
|
+
* (data/sprites-large/*.toml, the same resolved array
|
|
349
|
+
* scripts/build-demo-sprites-pack.mjs writes as its whole manifest) —
|
|
350
|
+
* embedded at build time exactly like the icon tier, so the page makes no
|
|
351
|
+
* runtime fetch for it; empty, the icon tier simply stays active, the same
|
|
352
|
+
* fallback the old lazy fetch degraded to. `?preview=1` switches into the
|
|
353
|
+
* small, auto-playing, non-interactive mode the home page's hero iframe
|
|
354
|
+
* embeds, matching spider-fly.html's own dual-purpose file.
|
|
355
|
+
* `engineBundleJs` (the built adventure-browser bundle's own text) inlines
|
|
356
|
+
* the engine into the page instead of the sibling `<script src>`, for the
|
|
357
|
+
* CLI's standalone export — one downloadable file that runs from file://
|
|
358
|
+
* with no sibling assets. Default empty keeps the site build's sibling-file
|
|
359
|
+
* arrangement byte-identical. */
|
|
350
360
|
export function renderAdventureHtml({
|
|
351
361
|
title = DEFAULT_TITLE,
|
|
352
362
|
worldPayload = { facts: [], rules: [], opening: "" },
|
|
353
363
|
spriteTemplates = [],
|
|
364
|
+
largeSpriteTemplates = [],
|
|
365
|
+
engineBundleJs = "",
|
|
354
366
|
} = {}) {
|
|
355
367
|
const pageData = embedJson({
|
|
356
368
|
world: worldPayload,
|
|
357
369
|
previewMaxTicks: PREVIEW_MAX_TICKS,
|
|
358
370
|
tickWaitMs: TICK_WAIT_MS,
|
|
359
371
|
spriteTemplates,
|
|
372
|
+
largeSpriteTemplates,
|
|
360
373
|
});
|
|
361
374
|
|
|
362
375
|
return `<!doctype html>
|
|
@@ -367,22 +380,45 @@ export function renderAdventureHtml({
|
|
|
367
380
|
<title>${escapeHtml(title)}</title>
|
|
368
381
|
<style>
|
|
369
382
|
${THEME_TOKENS_CSS}
|
|
370
|
-
/*
|
|
371
|
-
never replacing them: taught/corpus/entail/alert stay the
|
|
372
|
-
provenance palette every other tmct viz page uses, repurposed here
|
|
373
|
-
the class-badge ring colors (hero/townsfolk/fixture/item) — a visitor
|
|
374
|
-
who already knows what green/blue/gold/red
|
|
375
|
-
this page's "class" system for free.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
:root
|
|
383
|
+
/* Murder-mystery board-game chrome — layered over the shared neutrals/
|
|
384
|
+
accents above, never replacing them: taught/corpus/entail/alert stay the
|
|
385
|
+
SAME provenance palette every other tmct viz page uses, repurposed here
|
|
386
|
+
as the class-badge ring colors (hero/townsfolk/fixture/item) — a visitor
|
|
387
|
+
who already knows what green/blue/gold/red means from ledger.html reads
|
|
388
|
+
this page's "class" system for free. The page-local decorative tokens:
|
|
389
|
+
parchment/gilt (the case-file paper and brass), baize (the board-game
|
|
390
|
+
felt the manor map sits on), and wall/floor (the room scene's papered
|
|
391
|
+
wall and parquet). */
|
|
392
|
+
:root {
|
|
393
|
+
--parchment: #ECE1C8; --parchment-strong: #E1D2A6; --gilt: #93701F;
|
|
394
|
+
--baize: #4A6B52; --baize-line: #33503C; --board-path: #E7DAB8;
|
|
395
|
+
--wall: #E9DDBE; --wall-stripe: rgba(147, 112, 31, .10);
|
|
396
|
+
--floor-a: #D9C398; --floor-b: #CFB884;
|
|
397
|
+
}
|
|
398
|
+
@media (prefers-color-scheme: dark) { :root {
|
|
399
|
+
--parchment: #241E12; --parchment-strong: #2C2416; --gilt: #C0A054;
|
|
400
|
+
--baize: #1E2C22; --baize-line: #4A6852; --board-path: #56604D;
|
|
401
|
+
--wall: #262013; --wall-stripe: rgba(192, 160, 84, .07);
|
|
402
|
+
--floor-a: #2B2314; --floor-b: #241E10;
|
|
403
|
+
} }
|
|
404
|
+
:root[data-theme="dark"] {
|
|
405
|
+
--parchment: #241E12; --parchment-strong: #2C2416; --gilt: #C0A054;
|
|
406
|
+
--baize: #1E2C22; --baize-line: #4A6852; --board-path: #56604D;
|
|
407
|
+
--wall: #262013; --wall-stripe: rgba(192, 160, 84, .07);
|
|
408
|
+
--floor-a: #2B2314; --floor-b: #241E10;
|
|
409
|
+
}
|
|
410
|
+
:root[data-theme="light"] {
|
|
411
|
+
--parchment: #ECE1C8; --parchment-strong: #E1D2A6; --gilt: #93701F;
|
|
412
|
+
--baize: #4A6B52; --baize-line: #33503C; --board-path: #E7DAB8;
|
|
413
|
+
--wall: #E9DDBE; --wall-stripe: rgba(147, 112, 31, .10);
|
|
414
|
+
--floor-a: #D9C398; --floor-b: #CFB884;
|
|
415
|
+
}
|
|
380
416
|
|
|
381
417
|
html { background: var(--bg); }
|
|
382
418
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
383
419
|
.mono { font-family: ${MONO_STACK}; }
|
|
384
420
|
main { max-width: 920px; margin: 0 auto; padding: 1.4rem 1.2rem 2.2rem; }
|
|
385
|
-
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .
|
|
421
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; color: var(--gilt); }
|
|
386
422
|
.titlebar { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: .3rem 0 1rem; }
|
|
387
423
|
h1 { font-size: 1.4rem; margin: 0; text-wrap: balance; }
|
|
388
424
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
@@ -394,19 +430,37 @@ ${THEME_TOKENS_CSS}
|
|
|
394
430
|
.stage { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 1rem; align-items: start; }
|
|
395
431
|
@media (max-width: 760px) { .stage { grid-template-columns: 1fr; } }
|
|
396
432
|
|
|
397
|
-
/* the
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
sitting top-anchored above a dead gap of raw background. */
|
|
433
|
+
/* the room view — a 90s-RPG interior cutaway, all CSS gradients, no
|
|
434
|
+
external asset: a striped papered wall over a gilt dado rail over a
|
|
435
|
+
diagonal-plank floor, framed by the same ornate double rule as before.
|
|
436
|
+
The sprite tiles sit on the floor band (align-items flex-end), so
|
|
437
|
+
furniture and people read as standing IN the room, against its wall,
|
|
438
|
+
rather than floating in a chip strip. */
|
|
404
439
|
#playStage .room-frame { align-self: center; }
|
|
405
|
-
.room-frame {
|
|
440
|
+
.room-frame {
|
|
441
|
+
position: relative; min-height: 250px;
|
|
442
|
+
background:
|
|
443
|
+
repeating-linear-gradient(90deg, var(--wall-stripe) 0 4px, transparent 4px 30px) 0 0 / 100% 58% no-repeat,
|
|
444
|
+
linear-gradient(var(--wall), var(--wall)) 0 0 / 100% 58% no-repeat,
|
|
445
|
+
linear-gradient(var(--gilt), var(--gilt)) 0 58% / 100% 3px no-repeat,
|
|
446
|
+
repeating-linear-gradient(78deg, var(--floor-a) 0 26px, var(--floor-b) 26px 52px);
|
|
447
|
+
border: 1px solid var(--gilt); outline: 1px solid var(--line); outline-offset: 4px;
|
|
448
|
+
padding: 2.6rem 1.1rem .8rem; display: flex; flex-direction: column; justify-content: flex-end; gap: .6rem;
|
|
449
|
+
}
|
|
406
450
|
.room-frame::before, .room-frame::after { content: "\\2766"; position: absolute; font-size: 1.1rem; color: var(--gilt); opacity: .85; line-height: 1; }
|
|
407
451
|
.room-frame::before { top: -.6rem; left: -.35rem; }
|
|
408
452
|
.room-frame::after { bottom: -.6rem; right: -.35rem; transform: rotate(180deg); }
|
|
409
|
-
|
|
453
|
+
/* the brass door plaque naming the room the scene is drawing — filled
|
|
454
|
+
from the same snapshot the caption already reads, never a new fact. */
|
|
455
|
+
.room-plaque {
|
|
456
|
+
position: absolute; top: .55rem; left: 50%; transform: translateX(-50%);
|
|
457
|
+
font-size: .66rem; letter-spacing: .14em; text-transform: uppercase;
|
|
458
|
+
color: var(--gilt); background: var(--parchment); border: 1px solid var(--gilt);
|
|
459
|
+
box-shadow: inset 0 0 0 2px var(--parchment-strong);
|
|
460
|
+
padding: .22rem .9rem; white-space: nowrap;
|
|
461
|
+
}
|
|
462
|
+
.room-plaque:empty { display: none; }
|
|
463
|
+
.sprite-row { display: flex; flex-wrap: wrap; gap: .9rem .7rem; align-items: flex-end; min-height: 2.5rem; }
|
|
410
464
|
|
|
411
465
|
/* the class-badge system — this design's signature element: every sprite
|
|
412
466
|
gets a small ringed portrait frame plus a genre-flavored class word
|
|
@@ -414,8 +468,16 @@ ${THEME_TOKENS_CSS}
|
|
|
414
468
|
the rest of the site already uses for provenance. The sprite LABEL
|
|
415
469
|
underneath always names the real, specific thing (the cabinet, the
|
|
416
470
|
butler) — chrome around honest content, never instead of it. */
|
|
417
|
-
.sprite-card { display: flex; flex-direction: column; align-items: center; width:
|
|
418
|
-
|
|
471
|
+
.sprite-card { display: flex; flex-direction: column; align-items: center; width: 70px; }
|
|
472
|
+
/* a squared inventory-slot tile (the 90s-RPG idiom) instead of the old
|
|
473
|
+
thin circular ring: a raised card with an inner mat, the class color on
|
|
474
|
+
the outer border, and a soft ground shadow so the tile sits ON the
|
|
475
|
+
floor rather than hovering over it. */
|
|
476
|
+
.sprite-frame {
|
|
477
|
+
width: 60px; height: 60px; border-radius: 5px; background: var(--card);
|
|
478
|
+
border: 2px solid var(--line); box-shadow: inset 0 0 0 3px var(--parchment), 0 2px 3px rgba(0, 0, 0, .22);
|
|
479
|
+
display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 8px;
|
|
480
|
+
}
|
|
419
481
|
.sprite-frame[data-cls="adventurer"] { border-color: var(--taught); }
|
|
420
482
|
.sprite-frame[data-cls="person"] { border-color: var(--corpus); }
|
|
421
483
|
.sprite-frame[data-cls="container"], .sprite-frame[data-cls="furniture"] { border-color: var(--entail); }
|
|
@@ -428,7 +490,9 @@ ${THEME_TOKENS_CSS}
|
|
|
428
490
|
.sprite[data-cls="container"], .sprite[data-cls="furniture"] { color: var(--entail); }
|
|
429
491
|
.sprite[data-cls="portable"] { color: var(--alert); }
|
|
430
492
|
.sprite[data-cls="room"] { color: var(--muted); }
|
|
431
|
-
|
|
493
|
+
/* the name plate under each tile gets its own parchment ground so it
|
|
494
|
+
stays legible over the parquet floor the row now sits on. */
|
|
495
|
+
.sprite-label { font-size: .74rem; text-align: center; color: var(--ink); margin-top: .3rem; line-height: 1.15; background: var(--parchment); border: 1px solid var(--line); border-radius: 2px; padding: .02rem .3rem; max-width: 100%; }
|
|
432
496
|
.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); }
|
|
433
497
|
.class-badge[data-cls="adventurer"] { border-color: var(--taught); color: var(--taught); }
|
|
434
498
|
.class-badge[data-cls="person"] { border-color: var(--corpus); color: var(--corpus); }
|
|
@@ -436,11 +500,11 @@ ${THEME_TOKENS_CSS}
|
|
|
436
500
|
.class-badge[data-cls="portable"] { border-color: var(--alert); color: var(--alert); }
|
|
437
501
|
|
|
438
502
|
.side { display: flex; flex-direction: column; gap: .8rem; min-width: 0; }
|
|
439
|
-
.chat, .panel { background: var(--card); border: 1px solid var(--line); border-top:
|
|
503
|
+
.chat, .panel { background: var(--card); border: 1px solid var(--line); border-top: 5px double var(--gilt); padding: .65rem .75rem; }
|
|
440
504
|
.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); }
|
|
441
505
|
.empty-note { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); }
|
|
442
506
|
.chips { display: flex; flex-wrap: wrap; gap: .35rem; }
|
|
443
|
-
.chip { font-family: ${MONO_STACK}; font-size: .72rem; padding: .25rem .6rem; border: 1px solid var(--
|
|
507
|
+
.chip { font-family: ${MONO_STACK}; font-size: .72rem; padding: .25rem .6rem; border: 1px solid var(--gilt); background: var(--parchment); color: var(--ink); border-radius: 3px; }
|
|
444
508
|
|
|
445
509
|
/* the manor map — a FIXED-size viewport regardless of room count or
|
|
446
510
|
layout shape (an operator report: an earlier version's container grew
|
|
@@ -451,17 +515,23 @@ ${THEME_TOKENS_CSS}
|
|
|
451
515
|
itself — an svg's own height:100% resolves against ITS PARENT's
|
|
452
516
|
height, and a percentage against an auto-height parent computes to
|
|
453
517
|
"auto" per spec, silently undoing the fixed-viewport intent below. */
|
|
454
|
-
|
|
518
|
+
/* the manor board — this redesign's signature element: the visited-rooms
|
|
519
|
+
graph drawn as a Cluedo-style game board. Green baize under a brass
|
|
520
|
+
frame; each room a parchment rectangle with its name printed INSIDE;
|
|
521
|
+
the corridors parchment paths between rooms; the player's current room
|
|
522
|
+
ringed in the same taught-green every page uses for "this is live". */
|
|
523
|
+
.map-viewport { height: 190px; box-sizing: content-box; padding: 7px; background: var(--baize); border: 2px solid var(--gilt); box-shadow: inset 0 0 0 1px var(--baize-line); overflow: hidden; display: flex; align-items: center; justify-content: center; }
|
|
455
524
|
.map-viewport > div { width: 100%; height: 100%; }
|
|
456
525
|
.map-viewport svg { width: 100%; height: 100%; display: block; }
|
|
457
|
-
.
|
|
458
|
-
.roommap .room-
|
|
459
|
-
.roommap .room-
|
|
460
|
-
.roommap .room-node
|
|
461
|
-
.roommap .room-node
|
|
526
|
+
.map-viewport .empty-note { color: var(--parchment); opacity: .8; }
|
|
527
|
+
.roommap .room-edge { stroke: var(--board-path); stroke-width: 7; }
|
|
528
|
+
.roommap .room-hint { fill: var(--board-path); opacity: .8; }
|
|
529
|
+
.roommap .room-node rect { fill: var(--parchment); stroke: var(--baize-line); stroke-width: 1.5; }
|
|
530
|
+
.roommap .room-node.current rect { stroke: var(--taught); stroke-width: 2.5; }
|
|
531
|
+
.roommap .room-node text { font-family: ${MONO_STACK}; font-size: 7px; fill: var(--ink); text-anchor: middle; }
|
|
462
532
|
.roommap .room-node.clickable { cursor: pointer; }
|
|
463
|
-
.roommap .room-node.clickable:hover
|
|
464
|
-
.roommap .room-node.selected
|
|
533
|
+
.roommap .room-node.clickable:hover rect { stroke: var(--gilt); stroke-width: 2.5; }
|
|
534
|
+
.roommap .room-node.selected rect { stroke: var(--alert); stroke-width: 2.5; }
|
|
465
535
|
|
|
466
536
|
.goal-status { display: flex; flex-direction: column; gap: .4rem; }
|
|
467
537
|
.goal-status .g { display: flex; align-items: baseline; gap: .4rem; font-size: .86rem; line-height: 1.35; }
|
|
@@ -477,7 +547,7 @@ ${THEME_TOKENS_CSS}
|
|
|
477
547
|
.pills { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .5rem; }
|
|
478
548
|
.pills:empty { display: none; margin-bottom: 0; }
|
|
479
549
|
.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; }
|
|
480
|
-
.pill:hover { border-color: var(--
|
|
550
|
+
.pill:hover { border-color: var(--gilt); background: var(--parchment); }
|
|
481
551
|
/* the room's own prose, relocated (operator feedback) out of a bottom-of-
|
|
482
552
|
page strip and into the flow between the history/pills and the input —
|
|
483
553
|
a quoted manuscript line reporting what "look" would say right now. */
|
|
@@ -488,10 +558,10 @@ ${THEME_TOKENS_CSS}
|
|
|
488
558
|
.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; }
|
|
489
559
|
.chatask input:disabled { opacity: .5; }
|
|
490
560
|
.controls-row { display: flex; align-items: center; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
|
|
491
|
-
.controls-row button { font-family: ${MONO_STACK}; font-size: .
|
|
492
|
-
.controls-row button:hover:not(:disabled) {
|
|
561
|
+
.controls-row button { font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; padding: .38rem .85rem; border: 1px solid var(--gilt); background: var(--parchment); color: var(--ink); }
|
|
562
|
+
.controls-row button:hover:not(:disabled) { background: var(--parchment-strong); }
|
|
493
563
|
.controls-row button:disabled { opacity: .4; cursor: default; }
|
|
494
|
-
.controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .
|
|
564
|
+
.controls-row .turn { margin-left: auto; font-family: ${MONO_STACK}; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase; color: var(--ink); background: var(--parchment); border: 1px solid var(--gilt); padding: .3rem .6rem; font-variant-numeric: tabular-nums; }
|
|
495
565
|
.goal-line { font-family: ${MONO_STACK}; font-size: .78rem; color: var(--muted); margin-top: .5rem; }
|
|
496
566
|
.status { font-family: ${MONO_STACK}; font-size: .74rem; color: var(--muted); margin-top: .3rem; }
|
|
497
567
|
|
|
@@ -528,6 +598,7 @@ ${THEME_TOKENS_CSS}
|
|
|
528
598
|
</div>
|
|
529
599
|
<div class="stage" id="playStage">
|
|
530
600
|
<div class="room-frame" id="roomFrame">
|
|
601
|
+
<div class="room-plaque mono" id="roomName"></div>
|
|
531
602
|
<div class="sprite-row" id="spriteRow"></div>
|
|
532
603
|
</div>
|
|
533
604
|
<aside class="side" aria-label="The adventure's log and chat">
|
|
@@ -592,7 +663,7 @@ ${THEME_TOKENS_CSS}
|
|
|
592
663
|
<script>
|
|
593
664
|
const ADVENTURE = ${pageData};
|
|
594
665
|
</script>
|
|
595
|
-
|
|
666
|
+
${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `<script src="./adventure-browser.bundle.js"></script>`}
|
|
596
667
|
<script>
|
|
597
668
|
(function () {
|
|
598
669
|
"use strict";
|
|
@@ -610,6 +681,7 @@ const ADVENTURE = ${pageData};
|
|
|
610
681
|
const esc = ${escapeHtml.toString()};
|
|
611
682
|
const el = (id) => document.getElementById(id);
|
|
612
683
|
const spriteRow = el("spriteRow");
|
|
684
|
+
const roomNameEl = el("roomName");
|
|
613
685
|
const captionEl = el("caption");
|
|
614
686
|
const goalLineEl = el("goalLine");
|
|
615
687
|
const statusEl = el("status");
|
|
@@ -646,6 +718,51 @@ const ADVENTURE = ${pageData};
|
|
|
646
718
|
let editState = { placements: new Map(), openness: new Map(), exits: new Map() };
|
|
647
719
|
let allStoreRows = [];
|
|
648
720
|
|
|
721
|
+
// ---- persistence — the manor remembers your visit, on this device -------
|
|
722
|
+
// Best-effort IndexedDB (tmctAdventure.openPersistedStore): after any
|
|
723
|
+
// state-changing redraw, the whole Backend-B payload plus the visited-room
|
|
724
|
+
// exposure set snapshots under the "adventure" key, debounced. Restored on
|
|
725
|
+
// boot; reset clears it and starts the world over. The home page's preview
|
|
726
|
+
// iframe never persists — an auto-playing demo must not overwrite, or
|
|
727
|
+
// resurrect as, a visitor's own game.
|
|
728
|
+
let persist = null;
|
|
729
|
+
let persistSaveTimer = null;
|
|
730
|
+
window.tmctAdventureLastSave = null;
|
|
731
|
+
|
|
732
|
+
// The deploy's own version, read off the service worker file the build
|
|
733
|
+
// already stamps (its cache name embeds package.json's version) — the only
|
|
734
|
+
// same-origin place the number exists at runtime. Best-effort: "dev".
|
|
735
|
+
async function fetchSiteVersion() {
|
|
736
|
+
try {
|
|
737
|
+
const res = await fetch("./tmct-sw.js");
|
|
738
|
+
if (!res.ok) return "dev";
|
|
739
|
+
const found = /tmct-precache-v(\\d+\\.\\d+\\.\\d+)/.exec(await res.text());
|
|
740
|
+
return found ? found[1] : "dev";
|
|
741
|
+
} catch {
|
|
742
|
+
return "dev";
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function schedulePersistSave() {
|
|
747
|
+
if (!persist || !session) return;
|
|
748
|
+
clearTimeout(persistSaveTimer);
|
|
749
|
+
persistSaveTimer = setTimeout(() => {
|
|
750
|
+
persistSaveTimer = null;
|
|
751
|
+
if (!session) return;
|
|
752
|
+
const started = performance.now();
|
|
753
|
+
let memoryPayload;
|
|
754
|
+
try {
|
|
755
|
+
memoryPayload = structuredClone(session.memoryDir.payload);
|
|
756
|
+
} catch {
|
|
757
|
+
try { memoryPayload = JSON.parse(JSON.stringify(session.memoryDir.payload)); } catch { return; }
|
|
758
|
+
}
|
|
759
|
+
const visitedRoomIds = lastSnapshot ? lastSnapshot.visitedRoomIds : [];
|
|
760
|
+
persist.save({ memoryPayload: memoryPayload, visitedRoomIds: visitedRoomIds }).then((saved) => {
|
|
761
|
+
if (saved) window.tmctAdventureLastSave = { at: Date.now(), ms: Math.round(performance.now() - started) };
|
|
762
|
+
});
|
|
763
|
+
}, 500);
|
|
764
|
+
}
|
|
765
|
+
|
|
649
766
|
// ---- serialize every engine-touching call: the ticker, the chat dock and
|
|
650
767
|
// the editor sync all share one in-memory store, and any overlapping pair
|
|
651
768
|
// could race against the same write.
|
|
@@ -656,21 +773,16 @@ const ADVENTURE = ${pageData};
|
|
|
656
773
|
return run;
|
|
657
774
|
}
|
|
658
775
|
|
|
659
|
-
// ---- large-sprite-tier wiring —
|
|
660
|
-
// (
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (!data || !Array.isArray(data.templates) || !data.templates.length) return;
|
|
670
|
-
activeSpriteTemplates = data.templates;
|
|
671
|
-
if (lastSnapshot) redraw(lastSnapshot);
|
|
672
|
-
if (document.body.classList.contains("editing")) refreshEditPanels();
|
|
673
|
-
}).catch(() => { /* ADVENTURE.spriteTemplates already covers this page */ });
|
|
776
|
+
// ---- large-sprite-tier wiring — the gradient-shaded 400px tier
|
|
777
|
+
// (data/sprites-large/*.toml) arrives embedded at build time as
|
|
778
|
+
// ADVENTURE.largeSpriteTemplates, resolved through the exact same
|
|
779
|
+
// sprite-templates.mjs resolver the icon tier already used — no runtime
|
|
780
|
+
// fetch. A build without the large tier just leaves the icon tier
|
|
781
|
+
// (ADVENTURE.spriteTemplates) as the working fallback — this page is never
|
|
782
|
+
// blank because one tier is absent, the same posture this project already
|
|
783
|
+
// takes elsewhere.
|
|
784
|
+
const largeTier = ADVENTURE.largeSpriteTemplates;
|
|
785
|
+
const activeSpriteTemplates = Array.isArray(largeTier) && largeTier.length ? largeTier : ADVENTURE.spriteTemplates;
|
|
674
786
|
|
|
675
787
|
// ---- the class-badge system — this redesign's signature element: every
|
|
676
788
|
// sprite gets a small genre-flavored class word underneath its own real
|
|
@@ -731,10 +843,14 @@ const ADVENTURE = ${pageData};
|
|
|
731
843
|
// cursor per node; play mode's own map stays purely informational.
|
|
732
844
|
function roomMapSvg(graph, clickable) {
|
|
733
845
|
if (!graph.nodes.length) return null;
|
|
734
|
-
|
|
846
|
+
// Board-game footprints: each room a named rectangle (the name INSIDE
|
|
847
|
+
// it, the way a Cluedo board prints its rooms), corridors as wide paths
|
|
848
|
+
// between the footprints, direction-only hints as path stubs fading
|
|
849
|
+
// toward rooms not yet visited.
|
|
850
|
+
const cell = 64, roomW = 56, roomH = 26;
|
|
735
851
|
const maxX = Math.max.apply(null, graph.nodes.map((n) => n.x));
|
|
736
852
|
const maxY = Math.max.apply(null, graph.nodes.map((n) => n.y));
|
|
737
|
-
const w = (maxX + 1) * cell, h = (maxY + 1) * cell
|
|
853
|
+
const w = (maxX + 1) * cell, h = (maxY + 1) * cell;
|
|
738
854
|
const cx = (n) => (n.x + 0.5) * cell;
|
|
739
855
|
const cy = (n) => (n.y + 0.5) * cell;
|
|
740
856
|
const byRoom = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
@@ -746,13 +862,13 @@ const ADVENTURE = ${pageData};
|
|
|
746
862
|
const hintsSvg = graph.hints.map((hi) => {
|
|
747
863
|
const from = byRoom.get(hi.from);
|
|
748
864
|
const d = HINT_DELTA[hi.direction] || [0, 0];
|
|
749
|
-
return '<circle class="room-hint" cx="' + (cx(from) + d[0] * cell * 0.42) + '" cy="' + (cy(from) + d[1] * cell * 0.42) + '" r="3"></circle>';
|
|
865
|
+
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>';
|
|
750
866
|
}).join("");
|
|
751
867
|
const nodesSvg = graph.nodes.map((n) => {
|
|
752
868
|
const cls = "room-node" + (n.current ? " current" : "") + (clickable ? " clickable" : "") + (clickable && n.id === selectedRoomId ? " selected" : "");
|
|
753
869
|
const attr = clickable ? ' data-room="' + esc(n.id) + '"' : "";
|
|
754
|
-
return '<g class="' + cls + '"' + attr + '><
|
|
755
|
-
+ '<text x="' + cx(n) + '" y="' + (cy(n) +
|
|
870
|
+
return '<g class="' + cls + '"' + attr + '><rect x="' + (cx(n) - roomW / 2) + '" y="' + (cy(n) - roomH / 2) + '" width="' + roomW + '" height="' + roomH + '" rx="3"></rect>'
|
|
871
|
+
+ '<text x="' + cx(n) + '" y="' + (cy(n) + 2.5) + '">' + esc(n.id) + "</text></g>";
|
|
756
872
|
}).join("");
|
|
757
873
|
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") + '">'
|
|
758
874
|
+ edgesSvg + hintsSvg + nodesSvg + "</svg>";
|
|
@@ -835,12 +951,14 @@ const ADVENTURE = ${pageData};
|
|
|
835
951
|
const objects = roomSceneObjects(snap.rows, snap.state, snap.here);
|
|
836
952
|
const sprites = [{ subject: "you", spriteClass: "adventurer" }, ...objects];
|
|
837
953
|
spriteRow.innerHTML = sprites.map((s) => spriteCardHtml(s.subject, s.spriteClass, resolveObjectSprite(snap.rows, s))).join("");
|
|
954
|
+
roomNameEl.textContent = "the " + snap.here;
|
|
838
955
|
captionEl.textContent = captionFor(snap.rows, snap.state, snap.here);
|
|
839
956
|
turnLabelEl.textContent = "turn: " + snap.turn;
|
|
840
957
|
renderPills(snap.rows, snap.state, snap.here);
|
|
841
958
|
renderCarrying(snap.rows, snap.state);
|
|
842
959
|
renderRoomMap(snap.rows, snap.state, snap.visitedRoomIds);
|
|
843
960
|
renderGoals(snap.rows, snap.state, snap.visitedRoomIds);
|
|
961
|
+
schedulePersistSave();
|
|
844
962
|
}
|
|
845
963
|
|
|
846
964
|
chatformEl.addEventListener("submit", (e) => {
|
|
@@ -1011,8 +1129,14 @@ const ADVENTURE = ${pageData};
|
|
|
1011
1129
|
editModeBtn.addEventListener("click", () => withLock(() =>
|
|
1012
1130
|
(document.body.classList.contains("editing") ? exitEditMode() : enterEditMode())));
|
|
1013
1131
|
|
|
1014
|
-
async function boot() {
|
|
1015
|
-
|
|
1132
|
+
async function boot({ fresh = false } = {}) {
|
|
1133
|
+
const saved = !fresh && persist ? await persist.load() : null;
|
|
1134
|
+
session = await tmctAdventure.createAdventureSession(
|
|
1135
|
+
ADVENTURE.world,
|
|
1136
|
+
saved && saved.payload && saved.payload.memoryPayload
|
|
1137
|
+
? { restoredPayload: saved.payload.memoryPayload, restoredVisitedRoomIds: saved.payload.visitedRoomIds }
|
|
1138
|
+
: {},
|
|
1139
|
+
);
|
|
1016
1140
|
lastTicks = 0;
|
|
1017
1141
|
const snap = await session.snapshot();
|
|
1018
1142
|
redraw(snap);
|
|
@@ -1020,6 +1144,9 @@ const ADVENTURE = ${pageData};
|
|
|
1020
1144
|
chatlogEl.innerHTML = "";
|
|
1021
1145
|
statusEl.textContent = ADVENTURE.world.opening || "";
|
|
1022
1146
|
addChatLine("t", esc(ADVENTURE.world.opening || "the adventure begins."));
|
|
1147
|
+
if (saved && snap.turn > 0) {
|
|
1148
|
+
addChatLine("t", "resumed where you left off (turn " + snap.turn + ") \\u2014 progress kept best-effort on this device; reset starts the manor over.");
|
|
1149
|
+
}
|
|
1023
1150
|
chatqEl.disabled = false;
|
|
1024
1151
|
resetBtn.disabled = false; playBtn.disabled = false; stepBtn.disabled = false; editModeBtn.disabled = false;
|
|
1025
1152
|
}
|
|
@@ -1040,7 +1167,15 @@ const ADVENTURE = ${pageData};
|
|
|
1040
1167
|
stepBtn.disabled = state.animating || state.playing;
|
|
1041
1168
|
resetBtn.disabled = state.animating;
|
|
1042
1169
|
},
|
|
1043
|
-
onReset: () => withLock(
|
|
1170
|
+
onReset: () => withLock(async () => {
|
|
1171
|
+
// Reset means "start the manor over", so the saved snapshot goes too —
|
|
1172
|
+
// without the clear, the next visit would restore the game reset just
|
|
1173
|
+
// threw away.
|
|
1174
|
+
clearTimeout(persistSaveTimer);
|
|
1175
|
+
persistSaveTimer = null;
|
|
1176
|
+
if (persist) await persist.clear();
|
|
1177
|
+
await boot({ fresh: true });
|
|
1178
|
+
}),
|
|
1044
1179
|
hasNext: () => !preview || lastTicks < ADVENTURE.previewMaxTicks,
|
|
1045
1180
|
waitMs: ADVENTURE.tickWaitMs,
|
|
1046
1181
|
});
|
|
@@ -1048,7 +1183,14 @@ const ADVENTURE = ${pageData};
|
|
|
1048
1183
|
stepBtn.addEventListener("click", () => ticker.stepOnce());
|
|
1049
1184
|
resetBtn.addEventListener("click", () => ticker.reset());
|
|
1050
1185
|
|
|
1051
|
-
|
|
1186
|
+
(async () => {
|
|
1187
|
+
if (!preview && tmctAdventure.openPersistedStore) {
|
|
1188
|
+
const siteVersion = await fetchSiteVersion();
|
|
1189
|
+
persist = tmctAdventure.openPersistedStore({ storeKey: "adventure", stamp: siteVersion + ":" + ADVENTURE.world.name });
|
|
1190
|
+
}
|
|
1191
|
+
await boot();
|
|
1192
|
+
if (preview) ticker.play();
|
|
1193
|
+
})();
|
|
1052
1194
|
})();
|
|
1053
1195
|
</script>
|
|
1054
1196
|
</body>
|