@polycode-projects/the-mechanical-code-talker 5.0.4 → 5.0.6
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 +72 -19
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +53 -0
- package/src/domain/ask.mjs +2 -4
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +18 -0
- package/src/domain/memory/causal-stability.mjs +82 -0
- package/src/domain/town-square-world.mjs +36 -0
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/chat-page-viz.mjs +5 -2
- package/src/services/chat.mjs +104 -16
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/ingest-viz.mjs +5 -4
- package/src/services/ledger-viz.mjs +5 -2
- package/src/services/mud-viz.mjs +4 -3
- package/src/services/mudiii-scene.mjs +136 -12
- package/src/services/mudiii-turn.mjs +250 -1
- package/src/services/mudiii-viz.mjs +599 -108
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +3 -2
- package/src/services/predator-prey.mjs +133 -17
- package/src/services/research-viz.mjs +5 -4
- package/src/services/spider-fly-viz.mjs +3 -2
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +91 -91
- package/src/surfaces/web/mudiii-browser-entry.mjs +154 -34
|
@@ -8,13 +8,10 @@
|
|
|
8
8
|
// mud.html's burrow survey.
|
|
9
9
|
//
|
|
10
10
|
// This module owns the PAGE SHELL only. The 3D scene itself — the actual
|
|
11
|
-
// three.js renderer, the model loader, the per-frame camera update —
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// does not exist in every worktree yet (see the guarded import below), so
|
|
16
|
-
// this file also ships a "" stub until it lands — no edit needed here when
|
|
17
|
-
// it does.
|
|
11
|
+
// three.js renderer, the model loader, the per-frame camera update — lives in
|
|
12
|
+
// src/services/mudiii-scene.mjs, reached through exactly one frozen function:
|
|
13
|
+
// `mudiiiSceneScript(opts) -> string`, a standalone inline <script> this page
|
|
14
|
+
// embeds next to its own.
|
|
18
15
|
//
|
|
19
16
|
// The contract runs both ways. Scene -> shell: the scene script calls
|
|
20
17
|
// `window.mudiiiHandleSceneClick(cellId)` on a raycast hit. Shell -> scene:
|
|
@@ -49,23 +46,21 @@
|
|
|
49
46
|
// was actually asking for.
|
|
50
47
|
import {
|
|
51
48
|
THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText, scenarioLabel,
|
|
52
|
-
rowsForWorld, appendLogLine,
|
|
49
|
+
rowsForWorld, appendLogLine, wordBeforeCursor, demoEyebrowHtml, EYEBROW_LINKS_CSS,
|
|
53
50
|
} from "./viz-theme.mjs";
|
|
54
|
-
import { createTicker, createSerialQueue } from "./viz-ticker.mjs";
|
|
51
|
+
import { createTicker, createSerialQueue, prefersReducedMotion } from "./viz-ticker.mjs";
|
|
55
52
|
import { renderMudEditorText, gridWorldEditorState } from "./mud-editor.mjs";
|
|
56
53
|
import {
|
|
57
54
|
pillCandidates, matchPills, pillCompleteMarkup, createPillComplete, PILL_COMPLETE_CSS,
|
|
58
55
|
} from "./pill-complete.mjs";
|
|
59
56
|
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
57
|
+
import { believedFactSentence } from "./mudiii-turn.mjs";
|
|
60
58
|
|
|
61
59
|
// The scene module and this one import each other: this file embeds the
|
|
62
60
|
// scene's generated IIFE, and the scene splices this file's pure geometry
|
|
63
|
-
// helpers into it.
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// must NOT be a top-level `await import()`: two modules awaiting each other at
|
|
67
|
-
// evaluation time never settle, and the failure is a silent hang rather than
|
|
68
|
-
// an error.
|
|
61
|
+
// helpers into it. The cycle holds because every binding crossing it is a
|
|
62
|
+
// hoisted function declaration that nothing calls at module-evaluation time —
|
|
63
|
+
// the same shape world-teach.mjs and adventure.mjs already rely on.
|
|
69
64
|
import { mudiiiSceneScript } from "./mudiii-scene.mjs";
|
|
70
65
|
|
|
71
66
|
const DEFAULT_TITLE = "tmct — mudiii";
|
|
@@ -89,6 +84,23 @@ const DEFAULT_MAX_TURNS = 400;
|
|
|
89
84
|
const DEFAULT_GRID_SIZE = 12;
|
|
90
85
|
const DEFAULT_FACING = "south";
|
|
91
86
|
const CAMERA_MODES = ["follow", "pov", "overhead"];
|
|
87
|
+
// The ring reads ABSOLUTE, not relative to whichever way an agent happens to
|
|
88
|
+
// face: every other direction word on this page is a compass point (a told
|
|
89
|
+
// fact says "the goblin is east", the map is north-up), and driveRequest takes
|
|
90
|
+
// a compass point directly — a cardinal steps and faces that way, an
|
|
91
|
+
// intercardinal turns on the spot. Ordered north-first, clockwise.
|
|
92
|
+
const RING_POINTS = [
|
|
93
|
+
"north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest",
|
|
94
|
+
];
|
|
95
|
+
// mud.html's own glyph vocabulary, widened to the four diagonals. The reading
|
|
96
|
+
// differs from mud's: its ring lights every available exit, because a room has
|
|
97
|
+
// a fixed handful. An open grid grants a step almost everywhere, so lighting
|
|
98
|
+
// what is available would light nearly all of it and say nothing. The one lit
|
|
99
|
+
// glyph here is the followed agent's own facing.
|
|
100
|
+
const DIR_GLYPH = Object.freeze({
|
|
101
|
+
north: "▲ N", northeast: "↗", east: "E ▶", southeast: "↘",
|
|
102
|
+
south: "▼ S", southwest: "↙", west: "◀ W", northwest: "↖",
|
|
103
|
+
});
|
|
92
104
|
|
|
93
105
|
const MUDIII_NOTE_LINES = [
|
|
94
106
|
"One fox and a handful of goblins share a town square, rendered in three dimensions rather than mud.html's flat rooms. The foxes slider picks how many predators are cast; the goblins slider adds more prey. Nothing here is a player either — you watch from whichever camera you pick.",
|
|
@@ -336,6 +348,30 @@ export function mapDotsFor(agents, items, gridSize) {
|
|
|
336
348
|
return dots;
|
|
337
349
|
}
|
|
338
350
|
|
|
351
|
+
/** Every static prop's own filled cell for the 2D map panel, as percentage
|
|
352
|
+
* coordinates within the same square board `mapDotsFor` draws into
|
|
353
|
+
* (`{ id, xPct, yPct, sizePct }[]`). `props` is `propPlacementsFrom`'s own
|
|
354
|
+
* output. A block is drawn from the cell's own top-left corner and fills it,
|
|
355
|
+
* so the offset is `- 1` where `mapDotsFor`'s dot, centred on the cell, takes
|
|
356
|
+
* `- 0.5`. A placement with no parseable cell is dropped rather than drawn at
|
|
357
|
+
* a guessed position. Pure, self-contained. */
|
|
358
|
+
export function mapBlocksFor(props, gridSize) {
|
|
359
|
+
const size = Number(gridSize);
|
|
360
|
+
if (!Number.isFinite(size) || size <= 0) return [];
|
|
361
|
+
const blocks = [];
|
|
362
|
+
for (const prop of props || []) {
|
|
363
|
+
const match = /^cell-(\d+)-(\d+)$/.exec(String(prop && prop.cell != null ? prop.cell : ""));
|
|
364
|
+
if (!match) continue;
|
|
365
|
+
blocks.push({
|
|
366
|
+
id: prop.id,
|
|
367
|
+
xPct: ((Number(match[1]) - 1) / size) * 100,
|
|
368
|
+
yPct: ((Number(match[2]) - 1) / size) * 100,
|
|
369
|
+
sizePct: 100 / size,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return blocks;
|
|
373
|
+
}
|
|
374
|
+
|
|
339
375
|
/** One HUD card's own field set, read off `agent` (`{ id, role, goal, mood,
|
|
340
376
|
* plan, mass, belief }`, this turn's slice of the tick payload) and a
|
|
341
377
|
* resolved `mudiiiConfig` (DEFAULT_GAME_CONFIG.mudiii's own shape). `massPct`
|
|
@@ -381,6 +417,7 @@ export function clipForAction(role, action, clipMap) {
|
|
|
381
417
|
const kindFor = {
|
|
382
418
|
wander: "walk",
|
|
383
419
|
forage: "walk",
|
|
420
|
+
driven: "walk",
|
|
384
421
|
chase: "run",
|
|
385
422
|
evade: "run",
|
|
386
423
|
"eat-agent": role === "predator" ? "attack" : "death",
|
|
@@ -412,7 +449,11 @@ export function agentCardMarkup(slot) {
|
|
|
412
449
|
<div class="hud-meter" id="${w}-meter"><div class="hud-meter-fill" id="${w}-meter-fill"></div></div>
|
|
413
450
|
<p class="hud-goal" id="${w}-goal"></p>
|
|
414
451
|
<p class="hud-plan mono" id="${w}-plan"></p>
|
|
415
|
-
<
|
|
452
|
+
<button type="button" class="hud-belief-toggle" id="${w}-belief-toggle"
|
|
453
|
+
aria-expanded="false" aria-controls="${w}-detail" hidden>
|
|
454
|
+
<span class="hud-belief mono" id="${w}-belief"></span>
|
|
455
|
+
</button>
|
|
456
|
+
<div class="hud-detail mono" id="${w}-detail" hidden></div>
|
|
416
457
|
</div>`;
|
|
417
458
|
}
|
|
418
459
|
|
|
@@ -481,7 +522,7 @@ ${PILL_COMPLETE_CSS}
|
|
|
481
522
|
<body>
|
|
482
523
|
<main>
|
|
483
524
|
<header class="mudiii-topbar">
|
|
484
|
-
<h1 class="eyebrow"
|
|
525
|
+
<h1 class="eyebrow">${demoEyebrowHtml("mudiii", "mudiii")}</h1>
|
|
485
526
|
<a class="mudiii-topbar-help" href="./help.html" target="_blank" rel="noopener"
|
|
486
527
|
title="how this demo works, in a new tab" aria-label="how this demo works, opens in a new tab">?</a>
|
|
487
528
|
</header>
|
|
@@ -497,64 +538,80 @@ ${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " select
|
|
|
497
538
|
<button type="button" class="deck-info-btn" id="deckInfoBtn" aria-expanded="false" aria-controls="deckInfoPopup" aria-label="about this demo">?</button>
|
|
498
539
|
<span class="mono deck-turns" id="globalTurnCount">turns: 0</span>
|
|
499
540
|
</div>
|
|
500
|
-
<div class="deck-
|
|
501
|
-
<
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
<
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
<
|
|
524
|
-
|
|
525
|
-
|
|
541
|
+
<div class="deck-body">
|
|
542
|
+
<div class="deck-sliders">
|
|
543
|
+
<label class="deck-slider">foxes
|
|
544
|
+
<input type="range" id="playerCountSlider" min="0" max="${PLAYER_COUNTS.length - 1}" step="1"
|
|
545
|
+
value="${Math.max(0, PLAYER_COUNTS.indexOf(DEFAULT_PLAYER_COUNT))}"
|
|
546
|
+
list="playerCountTicks" aria-valuetext="${DEFAULT_PLAYER_COUNT} foxes">
|
|
547
|
+
<datalist id="playerCountTicks">${PLAYER_COUNTS.map((n, i) => `<option value="${i}" label="${n}"></option>`).join("")}</datalist>
|
|
548
|
+
<span class="mono" id="playerCountValue">${DEFAULT_PLAYER_COUNT}</span>
|
|
549
|
+
</label>
|
|
550
|
+
<label class="deck-slider">goblins
|
|
551
|
+
<input type="range" id="npcCountSlider" min="${NPC_COUNT_MIN}" max="${NPC_COUNT_MAX}" step="1"
|
|
552
|
+
value="${DEFAULT_NPC_COUNT}"
|
|
553
|
+
list="npcCountTicks" aria-valuetext="${DEFAULT_NPC_COUNT} goblins">
|
|
554
|
+
<datalist id="npcCountTicks">${Array.from({ length: NPC_COUNT_MAX - NPC_COUNT_MIN + 1 }, (_, i) => {
|
|
555
|
+
const n = NPC_COUNT_MIN + i;
|
|
556
|
+
return NPC_COUNT_LABELLED.includes(n) ? `<option value="${n}" label="${n}"></option>` : `<option value="${n}"></option>`;
|
|
557
|
+
}).join("")}</datalist>
|
|
558
|
+
<span class="mono" id="npcCountValue">${DEFAULT_NPC_COUNT}</span>
|
|
559
|
+
</label>
|
|
560
|
+
<label class="deck-slider">delay
|
|
561
|
+
<input type="range" id="delaySlider" min="80" max="2000" step="20" value="${DEFAULT_DELAY_MS}">
|
|
562
|
+
<span class="mono" id="delayValue">${DEFAULT_DELAY_MS}ms</span>
|
|
563
|
+
</label>
|
|
564
|
+
<label class="deck-slider">max turns
|
|
565
|
+
<input type="range" id="maxTurnsSlider" min="20" max="2000" step="20" value="${DEFAULT_MAX_TURNS}">
|
|
566
|
+
<span class="mono" id="maxTurnsValue">${DEFAULT_MAX_TURNS}</span>
|
|
567
|
+
</label>
|
|
568
|
+
</div>
|
|
569
|
+
<section class="map-panel" id="mapPanel" aria-label="the town square, from above">
|
|
570
|
+
<div class="map-panel-head">
|
|
571
|
+
<span class="map-panel-title">the square, from above</span>
|
|
572
|
+
<span class="mono map-panel-turn" id="mapPanelTurn">turn 0</span>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="map-panel-board" id="mapPanelBoard"></div>
|
|
575
|
+
<div class="map-legend">
|
|
576
|
+
<span class="map-key"><i class="map-swatch map-swatch-predator"></i>predator</span>
|
|
577
|
+
<span class="map-key"><i class="map-swatch map-swatch-prey"></i>prey</span>
|
|
578
|
+
<span class="map-key"><i class="map-swatch map-swatch-food"></i>food</span>
|
|
579
|
+
<span class="map-key"><i class="map-swatch map-swatch-prop"></i>building</span>
|
|
580
|
+
</div>
|
|
581
|
+
</section>
|
|
526
582
|
</div>
|
|
527
583
|
<div class="deck-camera">
|
|
528
584
|
<label class="deck-slider">follow
|
|
529
|
-
<select id="agentSelect" class="deck-select" aria-label="which agent to follow"
|
|
585
|
+
<select id="agentSelect" class="deck-select" aria-label="which agent to follow"
|
|
586
|
+
aria-describedby="agentSelectHint">
|
|
530
587
|
${openingAgents.map((a) => ` <option value="${escapeHtml(a.id)}">${escapeHtml(a.id)}</option>`).join("\n")}
|
|
531
588
|
</select>
|
|
532
589
|
</label>
|
|
590
|
+
<span class="deck-hint" id="agentSelectHint" hidden>pause to swap</span>
|
|
533
591
|
<div class="camera-mode" id="cameraMode" role="group" aria-label="camera mode">
|
|
534
592
|
<button type="button" data-mode="follow" aria-pressed="true">follow</button>
|
|
535
593
|
<button type="button" data-mode="pov" aria-pressed="false">pov</button>
|
|
536
594
|
<button type="button" data-mode="overhead" aria-pressed="false">overhead</button>
|
|
537
595
|
</div>
|
|
538
596
|
<button type="button" class="pill affordance" id="foodPill" data-command="place food" aria-pressed="false">place food</button>
|
|
597
|
+
<label class="deck-teach" title="With this on, a sentence like "The fox is at cell-3-4." writes a fact into the square instead of running as a command.">
|
|
598
|
+
<input type="checkbox" id="teachToggle">
|
|
599
|
+
teach
|
|
600
|
+
</label>
|
|
539
601
|
</div>
|
|
540
602
|
<div class="deck-info-popup mudiii-note" id="deckInfoPopup" role="dialog" aria-label="about this demo" hidden>
|
|
541
603
|
${MUDIII_NOTE_LINES.map((line) => `<p>${escapeHtml(line)}</p>`).join("\n ")}
|
|
542
604
|
<button type="button" class="deck-info-popup-close" id="deckInfoClose" aria-label="close">×</button>
|
|
543
605
|
</div>
|
|
544
606
|
</section>
|
|
545
|
-
<section class="map-panel" id="mapPanel" aria-label="the town square, from above">
|
|
546
|
-
<div class="map-panel-head">
|
|
547
|
-
<span class="map-panel-title">the square, from above</span>
|
|
548
|
-
<span class="mono map-panel-turn" id="mapPanelTurn">turn 0</span>
|
|
549
|
-
</div>
|
|
550
|
-
<div class="map-panel-board" id="mapPanelBoard"></div>
|
|
551
|
-
</section>
|
|
552
607
|
</div>
|
|
553
608
|
<section class="scene-stage" id="sceneStage" aria-label="the town square, in three dimensions">
|
|
554
609
|
<canvas id="sceneCanvas"></canvas>
|
|
610
|
+
<div class="dir-ring" id="driveRing" role="group" aria-label="walk the agent the camera follows" hidden>
|
|
611
|
+
${RING_POINTS.map((point) => ` <span class="dir-slot dir-${point}"><button type="button" class="dir-pill" data-drive="${point}" title="walk ${point}" aria-label="walk ${point}" aria-pressed="false">${escapeHtml(DIR_GLYPH[point])}</button></span>`).join("\n")}
|
|
612
|
+
</div>
|
|
555
613
|
<p class="scene-status" id="sceneStatus" role="status"></p>
|
|
556
614
|
</section>
|
|
557
|
-
<div class="hud-row" id="hudRow" aria-label="every agent's own status"></div>
|
|
558
615
|
<div class="edit-stage" id="mudiiiEditStage" aria-label="the square's own facts, in plain sentences">
|
|
559
616
|
<section class="edit-text" aria-label="the world's facts as editable sentences">
|
|
560
617
|
<h2>the square, in plain sentences</h2>
|
|
@@ -584,11 +641,12 @@ ${openingAgents.map((a) => ` <option value="${escapeHtml(a.id)}">${es
|
|
|
584
641
|
<span class="prompt mono">tmct></span>
|
|
585
642
|
${pillCompleteMarkup({
|
|
586
643
|
inputId: "chatInput",
|
|
587
|
-
inputHtml: '<input id="chatInput" type="text" placeholder="@fox
|
|
644
|
+
inputHtml: '<input id="chatInput" type="text" placeholder="@fox the goblin is east" aria-label="type a command" disabled>',
|
|
588
645
|
})}
|
|
589
646
|
</form>
|
|
590
647
|
</div>
|
|
591
648
|
</section>
|
|
649
|
+
<div class="hud-row" id="hudRow" aria-label="every agent's own status"></div>
|
|
592
650
|
</main>
|
|
593
651
|
<script>
|
|
594
652
|
const MUDIII_PAGE_DATA = ${pageData};
|
|
@@ -615,6 +673,7 @@ const MUDIII_STYLE = `
|
|
|
615
673
|
.mono { font-family: ${MONO_STACK}; }
|
|
616
674
|
main { max-width: 1280px; margin: 0 auto; padding: 1.1rem 1.2rem 2.4rem; }
|
|
617
675
|
.eyebrow { font-family: ${MONO_STACK}; font-weight: 500; font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: var(--square-ink); opacity: .85; margin: 0; }
|
|
676
|
+
${EYEBROW_LINKS_CSS}
|
|
618
677
|
h2 { font-family: ${DISPLAY_STACK}; font-size: 1rem; margin: 0; }
|
|
619
678
|
h3 { font-family: ${MONO_STACK}; font-size: .58rem; margin: 0 0 .3rem; text-transform: uppercase; letter-spacing: .12em; color: var(--square-stone-dark); }
|
|
620
679
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
@@ -628,7 +687,7 @@ const MUDIII_STYLE = `
|
|
|
628
687
|
}
|
|
629
688
|
.mudiii-topbar-help:hover { border-color: var(--square-accent); }
|
|
630
689
|
|
|
631
|
-
.deck-row {
|
|
690
|
+
.deck-row { margin-bottom: 1rem; }
|
|
632
691
|
.deck {
|
|
633
692
|
position: relative;
|
|
634
693
|
background: var(--parchment); border: 1px solid var(--square-stone-dark); border-radius: 4px;
|
|
@@ -651,14 +710,26 @@ const MUDIII_STYLE = `
|
|
|
651
710
|
font-family: ${MONO_STACK}; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em;
|
|
652
711
|
padding: .32rem .7rem; border: 1px solid var(--square-stone-dark); border-radius: 3px;
|
|
653
712
|
background: rgba(255,255,255,.5); color: var(--square-ink);
|
|
713
|
+
/* A select is as wide as its longest option, and a square's label runs to
|
|
714
|
+
"town square (12x12, 1 fox, 3 goblins)" — on a phone that alone made the
|
|
715
|
+
whole page scroll sideways. */
|
|
716
|
+
min-width: 0; max-width: 100%;
|
|
654
717
|
}
|
|
718
|
+
#scenarioSelect { flex: 1 1 9rem; }
|
|
655
719
|
.deck-select:hover { border-color: var(--square-accent); }
|
|
720
|
+
.deck-select:disabled { opacity: .45; cursor: default; }
|
|
721
|
+
.deck-select:disabled:hover { border-color: var(--square-stone-dark); }
|
|
722
|
+
.deck-hint { font-family: ${MONO_STACK}; font-size: .58rem; text-transform: uppercase; letter-spacing: .08em; color: var(--square-stone-dark); }
|
|
723
|
+
.deck-hint[hidden] { display: none; }
|
|
656
724
|
.deck-play { background: var(--square-ink) !important; color: var(--parchment); border-color: var(--square-ink) !important; padding: .38rem 1.1rem !important; }
|
|
657
725
|
.deck-play[aria-pressed="true"] { background: var(--square-accent) !important; border-color: var(--square-accent) !important; color: var(--square-ink); }
|
|
658
726
|
.deck-turns { margin-left: auto; font-size: .74rem; color: var(--square-stone-dark); background: var(--square-stone-dark); background: rgba(43,35,24,.9); color: var(--square-accent); border-radius: 2px; padding: .1rem .5rem; }
|
|
659
|
-
.deck-
|
|
660
|
-
.deck-
|
|
661
|
-
.deck-slider
|
|
727
|
+
.deck-body { display: flex; gap: .7rem; align-items: flex-start; }
|
|
728
|
+
.deck-sliders { display: flex; flex-wrap: wrap; gap: 1rem; flex: 1 1 auto; min-width: 0; }
|
|
729
|
+
.deck-slider { display: flex; align-items: center; gap: .35rem; font-family: ${MONO_STACK}; font-size: .62rem; text-transform: uppercase; letter-spacing: .08em; color: var(--square-stone-dark); min-width: 0; }
|
|
730
|
+
.deck-slider input[type="range"] { accent-color: var(--square-accent); flex: 1 1 4rem; min-width: 2.5rem; width: auto; max-width: 8rem; }
|
|
731
|
+
.deck-teach { display: flex; align-items: center; gap: .3rem; font-family: ${MONO_STACK}; font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--square-stone-dark); cursor: pointer; }
|
|
732
|
+
.deck-teach input[type="checkbox"] { accent-color: var(--square-accent); }
|
|
662
733
|
.camera-mode { display: inline-flex; gap: .25rem; }
|
|
663
734
|
.camera-mode button[aria-pressed="true"] { background: var(--square-accent); border-color: var(--square-accent); color: var(--square-ink); }
|
|
664
735
|
.deck-info-popup {
|
|
@@ -678,17 +749,42 @@ const MUDIII_STYLE = `
|
|
|
678
749
|
|
|
679
750
|
.map-panel {
|
|
680
751
|
background: var(--square-stone-dark); color: var(--parchment);
|
|
681
|
-
border: 1px solid var(--square-accent); border-radius: 4px; padding: .
|
|
682
|
-
display: flex; flex-direction: column; gap: .
|
|
752
|
+
border: 1px solid var(--square-accent); border-radius: 4px; padding: .5rem .55rem .55rem;
|
|
753
|
+
display: flex; flex-direction: column; gap: .35rem; min-width: 0;
|
|
754
|
+
flex: 0 0 50%; max-width: 50%;
|
|
755
|
+
}
|
|
756
|
+
.map-panel-head { display: flex; justify-content: space-between; align-items: baseline; gap: .4rem; }
|
|
757
|
+
.map-panel-title { font-family: ${MONO_STACK}; font-size: .54rem; text-transform: uppercase; letter-spacing: .1em; opacity: .85; }
|
|
758
|
+
.map-panel-turn { font-size: .58rem; opacity: .7; }
|
|
759
|
+
.map-panel-board {
|
|
760
|
+
position: relative; flex: 1; min-height: 110px; aspect-ratio: 1;
|
|
761
|
+
--map-cell-pct: 8.3333%;
|
|
762
|
+
background-color: rgba(124,154,91,.25);
|
|
763
|
+
background-image:
|
|
764
|
+
repeating-linear-gradient(90deg, rgba(233,217,182,.22) 0 1px, transparent 1px var(--map-cell-pct)),
|
|
765
|
+
repeating-linear-gradient(180deg, rgba(233,217,182,.22) 0 1px, transparent 1px var(--map-cell-pct));
|
|
766
|
+
border: 1px solid rgba(233,217,182,.35); border-radius: 3px;
|
|
683
767
|
}
|
|
684
|
-
.map-
|
|
685
|
-
.map-panel-title { font-family: ${MONO_STACK}; font-size: .58rem; text-transform: uppercase; letter-spacing: .12em; opacity: .85; }
|
|
686
|
-
.map-panel-turn { font-size: .62rem; opacity: .7; }
|
|
687
|
-
.map-panel-board { position: relative; flex: 1; min-height: 200px; background: rgba(124,154,91,.25); border: 1px solid rgba(233,217,182,.35); border-radius: 3px; }
|
|
768
|
+
.map-block { position: absolute; box-sizing: border-box; background: rgba(89,80,63,.9); border: 1px solid rgba(0,0,0,.35); border-radius: 1px; }
|
|
688
769
|
.map-dot { position: absolute; width: .55rem; height: .55rem; margin: -.28rem 0 0 -.28rem; border-radius: 50%; border: 1px solid rgba(0,0,0,.4); }
|
|
689
770
|
.map-dot-predator { background: var(--square-predator); }
|
|
690
771
|
.map-dot-prey { background: var(--square-prey); }
|
|
691
772
|
.map-dot-crumb, .map-dot-morsel, .map-dot-item { background: var(--square-accent); width: .34rem; height: .34rem; margin: -.17rem 0 0 -.17rem; }
|
|
773
|
+
.map-label {
|
|
774
|
+
position: absolute; margin: -.66rem 0 0 .26rem; font-size: .44rem; line-height: 1; letter-spacing: .02em;
|
|
775
|
+
color: var(--parchment); text-shadow: 0 1px 2px rgba(0,0,0,.85); white-space: nowrap; pointer-events: none;
|
|
776
|
+
}
|
|
777
|
+
.map-label-left { margin-left: -.3rem; transform: translateX(-100%); }
|
|
778
|
+
/* Plain inline-block swatches, never .map-dot: that class is absolutely
|
|
779
|
+
positioned with a centring margin, so a legend reusing it would position
|
|
780
|
+
against the board and disappear. */
|
|
781
|
+
.map-legend { display: flex; flex-wrap: wrap; gap: .12rem .5rem; font-family: ${MONO_STACK}; font-size: .5rem; text-transform: uppercase; letter-spacing: .08em; opacity: .85; }
|
|
782
|
+
.map-key { display: inline-flex; align-items: center; gap: .24rem; }
|
|
783
|
+
.map-swatch { display: inline-block; width: .45rem; height: .45rem; border-radius: 50%; border: 1px solid rgba(0,0,0,.4); }
|
|
784
|
+
.map-swatch-predator { background: var(--square-predator); }
|
|
785
|
+
.map-swatch-prey { background: var(--square-prey); }
|
|
786
|
+
.map-swatch-food { background: var(--square-accent); }
|
|
787
|
+
.map-swatch-prop { background: rgba(89,80,63,.9); border-radius: 1px; }
|
|
692
788
|
|
|
693
789
|
.scene-stage { position: relative; margin-bottom: 1rem; border: 1px solid var(--square-stone-dark); border-radius: 4px; overflow: hidden; background: #10161B; min-height: 360px; }
|
|
694
790
|
.scene-stage canvas { display: block; width: 100%; height: 360px; }
|
|
@@ -699,7 +795,31 @@ const MUDIII_STYLE = `
|
|
|
699
795
|
}
|
|
700
796
|
.scene-status:empty { display: none; }
|
|
701
797
|
|
|
702
|
-
|
|
798
|
+
/* Each press sits where it points, mud.html's own ring idiom. */
|
|
799
|
+
.dir-ring { position: absolute; inset: .3rem; pointer-events: none; }
|
|
800
|
+
.dir-ring[hidden] { display: none; }
|
|
801
|
+
.dir-slot { position: absolute; pointer-events: auto; }
|
|
802
|
+
.dir-north { top: 0; left: 50%; transform: translateX(-50%); }
|
|
803
|
+
.dir-south { bottom: 0; left: 50%; transform: translateX(-50%); }
|
|
804
|
+
.dir-west { left: 0; top: 50%; transform: translateY(-50%); }
|
|
805
|
+
.dir-east { right: 0; top: 50%; transform: translateY(-50%); }
|
|
806
|
+
.dir-northwest { top: 0; left: 0; }
|
|
807
|
+
.dir-northeast { top: 0; right: 0; }
|
|
808
|
+
.dir-southwest { bottom: 0; left: 0; }
|
|
809
|
+
.dir-southeast { bottom: 0; right: 0; }
|
|
810
|
+
.dir-pill {
|
|
811
|
+
font-family: ${MONO_STACK}; font-size: .58rem; letter-spacing: .06em; line-height: 1;
|
|
812
|
+
padding: .24rem .42rem; border-radius: 2px; border: 1px solid var(--square-stone-dark);
|
|
813
|
+
background: var(--parchment); color: var(--square-ink); white-space: nowrap;
|
|
814
|
+
}
|
|
815
|
+
.dir-pill:hover:not(:disabled) { border-color: var(--square-accent); background: var(--square-accent); }
|
|
816
|
+
.dir-pill:disabled { opacity: .35; cursor: default; }
|
|
817
|
+
.dir-pill[aria-pressed="true"] {
|
|
818
|
+
background: var(--square-accent); border-color: var(--square-stone-dark);
|
|
819
|
+
box-shadow: 0 0 0 2px rgba(217,138,43,.4);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.hud-row { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: 1rem; }
|
|
703
823
|
.hud-card {
|
|
704
824
|
flex: 1 1 220px; min-width: 200px; max-width: 320px;
|
|
705
825
|
background: var(--parchment); border: 1px solid var(--square-stone-dark); border-radius: 4px;
|
|
@@ -712,11 +832,18 @@ const MUDIII_STYLE = `
|
|
|
712
832
|
.hud-meter-fill { height: 100%; background: var(--square-accent); width: 0%; transition: width .3s ease; }
|
|
713
833
|
.hud-goal { margin: 0; font-size: .74rem; }
|
|
714
834
|
.hud-plan, .hud-belief { margin: 0; font-size: .62rem; color: var(--square-stone-dark); }
|
|
835
|
+
.hud-belief-toggle { display: flex; align-items: baseline; gap: .25rem; width: 100%; text-align: left; padding: 0; border: 0; background: none; }
|
|
836
|
+
.hud-belief-toggle[hidden] { display: none; }
|
|
837
|
+
.hud-belief-toggle .hud-belief { flex: 1; min-width: 0; }
|
|
838
|
+
.hud-belief-toggle::after { content: "\\25BE"; font-size: .55rem; color: var(--square-stone-dark); }
|
|
839
|
+
.hud-belief-toggle[aria-expanded="true"]::after { content: "\\25B4"; }
|
|
840
|
+
.hud-belief-toggle:hover .hud-belief, .hud-belief-toggle:hover::after { color: var(--square-ink); }
|
|
841
|
+
.hud-detail { display: flex; flex-direction: column; gap: .1rem; font-size: .6rem; color: var(--square-stone-dark); border-top: 1px solid rgba(0,0,0,.12); padding-top: .25rem; }
|
|
842
|
+
.hud-detail[hidden] { display: none; }
|
|
715
843
|
@media (prefers-reduced-motion: reduce) { .hud-meter-fill { transition: none; } }
|
|
716
844
|
|
|
717
845
|
.edit-stage { display: none; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 1rem; align-items: start; margin-bottom: 1rem; }
|
|
718
846
|
body.editing .mudiii-chat, body.editing .scene-stage, body.editing .hud-row, body.editing .deck-row .map-panel { display: none; }
|
|
719
|
-
body.editing .deck-row { grid-template-columns: 1fr; }
|
|
720
847
|
body.editing .edit-stage { display: grid; }
|
|
721
848
|
#editModeBtn[aria-pressed="true"] { background: var(--square-accent); border-color: var(--square-accent); }
|
|
722
849
|
.edit-text, .edit-panel {
|
|
@@ -767,12 +894,53 @@ const MUDIII_STYLE = `
|
|
|
767
894
|
.pill:hover:not(:disabled) { border-color: var(--square-accent); background: var(--square-accent); }
|
|
768
895
|
.pill.affordance { border-style: dashed; border-color: var(--square-stone); }
|
|
769
896
|
.pill.affordance[aria-pressed="true"] { background: var(--square-accent); border-style: solid; }
|
|
897
|
+
/* The tick and the cross live in ::before so the tag stays on the screen and
|
|
898
|
+
out of the submitted sentence — a clicked lie must be indistinguishable
|
|
899
|
+
from a typed one by the time the lane reads it. */
|
|
900
|
+
.pill[data-role="dyn-addr"][data-active="1"] { border-color: var(--taught); color: var(--taught); }
|
|
901
|
+
.pill[data-role="dyn-claim"][data-truth="true"] { border-color: var(--taught); }
|
|
902
|
+
.pill[data-role="dyn-claim"][data-truth="true"]::before { content: "\\2713 "; opacity: .55; }
|
|
903
|
+
.pill[data-role="dyn-claim"][data-truth="false"] { border-style: dashed; border-color: var(--alert); }
|
|
904
|
+
.pill[data-role="dyn-claim"][data-truth="false"]::before { content: "\\2715 "; opacity: .6; }
|
|
770
905
|
|
|
771
906
|
@media (max-width: 900px) {
|
|
772
|
-
.deck-row { grid-template-columns: 1fr; }
|
|
773
907
|
.edit-stage { grid-template-columns: 1fr; }
|
|
774
908
|
#editorText { min-height: 16rem; }
|
|
775
909
|
}
|
|
910
|
+
|
|
911
|
+
/* A landscape phone and a narrow desktop window are both under 900px but
|
|
912
|
+
want different slider/map arrangements, so the split has to key off
|
|
913
|
+
orientation as well as width. */
|
|
914
|
+
@media (max-width: 900px) and (orientation: landscape) {
|
|
915
|
+
.deck-sliders { display: grid; grid-template-columns: 1fr 1fr; gap: .4rem 1rem; }
|
|
916
|
+
.deck-body { align-items: stretch; }
|
|
917
|
+
.map-panel { flex-basis: 33%; max-width: 33%; }
|
|
918
|
+
/* The square aspect-ratio that suits a tall portrait column would blow
|
|
919
|
+
the map back up to full column width in a short landscape viewport —
|
|
920
|
+
here it follows the two-row slider stack's own height instead. */
|
|
921
|
+
.map-panel-board { aspect-ratio: auto; min-height: 90px; }
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/* Half the deck is right on a phone and absurd on a 2000px window: a
|
|
925
|
+
percentage has no ceiling, so a square board grew to roughly 950px tall
|
|
926
|
+
and pushed the 3D view off the screen. Wide viewports get an absolute
|
|
927
|
+
size instead, and keep the square — there is room for it here.
|
|
928
|
+
|
|
929
|
+
The map is also taller than the sliders beside it, which left a tall
|
|
930
|
+
empty stripe of parchment under them. On a wide screen the deck becomes
|
|
931
|
+
one grid so the map can stand beside the sliders AND the camera row at
|
|
932
|
+
once, and the space under the controls closes up. display:contents lifts
|
|
933
|
+
.deck-sliders and .map-panel out of .deck-body so both are grid items of
|
|
934
|
+
the deck itself. */
|
|
935
|
+
@media (min-width: 901px) {
|
|
936
|
+
.deck { display: grid; grid-template-columns: minmax(0, 1fr) 240px; grid-template-rows: auto 1fr auto; column-gap: .8rem; }
|
|
937
|
+
.deck-controls { grid-column: 1 / -1; grid-row: 1; }
|
|
938
|
+
.deck-body { display: contents; }
|
|
939
|
+
.deck-sliders { grid-column: 1; grid-row: 2; align-self: start; }
|
|
940
|
+
.deck-camera { grid-column: 1; grid-row: 3; align-self: end; }
|
|
941
|
+
.map-panel { grid-column: 2; grid-row: 2 / 4; flex: 0 0 auto; max-width: none; align-self: start; }
|
|
942
|
+
}
|
|
943
|
+
|
|
776
944
|
`;
|
|
777
945
|
|
|
778
946
|
/** The inlined page script, spliced the same way mud-viz.mjs's own
|
|
@@ -788,12 +956,14 @@ function pageScript() {
|
|
|
788
956
|
const DATA = MUDIII_PAGE_DATA;
|
|
789
957
|
const createTicker = ${createTicker.toString()};
|
|
790
958
|
const createSerialQueue = ${createSerialQueue.toString()};
|
|
959
|
+
const prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
791
960
|
const escapeHtml = ${escapeHtml.toString()};
|
|
792
961
|
const esc = escapeHtml;
|
|
793
962
|
const appendLogLine = ${appendLogLine.toString()};
|
|
794
963
|
const rowsForWorld = ${rowsForWorld.toString()};
|
|
795
964
|
const renderMudEditorText = ${renderMudEditorText.toString()};
|
|
796
965
|
const gridWorldEditorState = ${gridWorldEditorState.toString()};
|
|
966
|
+
const wordBeforeCursor = ${wordBeforeCursor.toString()};
|
|
797
967
|
const pillCandidates = ${pillCandidates.toString()};
|
|
798
968
|
const matchPills = ${matchPills.toString()};
|
|
799
969
|
const createPillComplete = ${createPillComplete.toString()};
|
|
@@ -806,29 +976,35 @@ function pageScript() {
|
|
|
806
976
|
const cameraRigFor = ${cameraRigFor.toString()};
|
|
807
977
|
const nextCameraSelection = ${nextCameraSelection.toString()};
|
|
808
978
|
const mapDotsFor = ${mapDotsFor.toString()};
|
|
979
|
+
const mapBlocksFor = ${mapBlocksFor.toString()};
|
|
809
980
|
const hudCardFieldsFor = ${hudCardFieldsFor.toString()};
|
|
810
981
|
const clipForAction = ${clipForAction.toString()};
|
|
811
982
|
const agentCardMarkup = ${agentCardMarkup.toString()};
|
|
983
|
+
const believedFactSentence = ${believedFactSentence.toString()};
|
|
812
984
|
|
|
813
985
|
const el = (id) => document.getElementById(id);
|
|
986
|
+
const SEED_COMMANDS = ["tick", "what does the fox see", "where is the goblin", "what can I do"];
|
|
814
987
|
let scenarioIndex = 0;
|
|
815
988
|
const scenario = function () { return DATA.scenarios[scenarioIndex]; };
|
|
816
989
|
const gridSizeOf = function () { return scenario().gridSize || DATA.gridSize; };
|
|
817
|
-
const rosterOf = function (s, role) {
|
|
818
|
-
return (s.agents || []).filter(function (a) { return !role || a.role === role; }).map(function (a) { return a.id; });
|
|
819
|
-
};
|
|
820
990
|
|
|
821
|
-
// ---- roster
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
// the
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
991
|
+
// ---- roster minting -----------------------------------------------------
|
|
992
|
+
// The page asks for a COUNT and names the ids it is about to get back: the
|
|
993
|
+
// engine mints <prefix>-1..N at seeded cells, so a slider can call for more
|
|
994
|
+
// animals than the scenario's own opening cast carries and still be met. The
|
|
995
|
+
// prefix is read off the scenario's own first agent of that role, so a square
|
|
996
|
+
// that casts something other than foxes and goblins still names its cast
|
|
997
|
+
// correctly. Drawing from the scenario's list instead capped every square at
|
|
998
|
+
// whatever its layout happened to build, and a shuffled draw would leave two
|
|
999
|
+
// loads of the same square with different casts.
|
|
1000
|
+
function rosterPrefixFor(s, role) {
|
|
1001
|
+
const first = (s.agents || []).find(function (a) { return a && a.role === role; });
|
|
1002
|
+
return first ? roleOfAgentId(first.id) : role;
|
|
1003
|
+
}
|
|
1004
|
+
function mintRoster(prefix, count) {
|
|
1005
|
+
const ids = [];
|
|
1006
|
+
for (let i = 1; i <= count; i += 1) ids.push(prefix + "-" + i);
|
|
1007
|
+
return ids;
|
|
832
1008
|
}
|
|
833
1009
|
|
|
834
1010
|
let cast = [];
|
|
@@ -842,8 +1018,14 @@ function pageScript() {
|
|
|
842
1018
|
let tickQueue = createSerialQueue();
|
|
843
1019
|
function serializeTick(fn) { return tickQueue.run(fn); }
|
|
844
1020
|
let camera = { mode: "follow", selectedId: null, status: null };
|
|
1021
|
+
// The mode a despawn fallback took away, held until the visitor picks
|
|
1022
|
+
// another agent. Without it, choosing someone new after a fox ate your
|
|
1023
|
+
// goblin leaves the camera overhead and the follow button unlit.
|
|
1024
|
+
let cameraModeBeforeFallback = null;
|
|
845
1025
|
let foodArmed = false;
|
|
846
1026
|
let livePills = [];
|
|
1027
|
+
let selectedAddresseeId = null;
|
|
1028
|
+
const expandedAgents = new Set();
|
|
847
1029
|
let pillComplete = null;
|
|
848
1030
|
let autoOn = false;
|
|
849
1031
|
let editing = false;
|
|
@@ -887,8 +1069,12 @@ function pageScript() {
|
|
|
887
1069
|
if (typeof result.turn === "number") globalTurn = result.turn;
|
|
888
1070
|
if (result.agents) agentsById = result.agents;
|
|
889
1071
|
if (result.items) itemsById = result.items;
|
|
890
|
-
callScene("applyTick", {
|
|
891
|
-
|
|
1072
|
+
callScene("applyTick", {
|
|
1073
|
+
agents: result.agents, items: result.items, ecology: result.ecology, rungs: result.rungs,
|
|
1074
|
+
});
|
|
1075
|
+
const nextCamera = nextCameraSelection(camera, agentsList(), result.ecology || []);
|
|
1076
|
+
if (nextCamera.status && camera.mode !== "overhead") cameraModeBeforeFallback = camera.mode;
|
|
1077
|
+
camera = nextCamera;
|
|
892
1078
|
callScene("setCamera", camera);
|
|
893
1079
|
if (camera.status) setSceneStatus(camera.status);
|
|
894
1080
|
}
|
|
@@ -911,6 +1097,12 @@ function pageScript() {
|
|
|
911
1097
|
const playBtn = el("autoToggle");
|
|
912
1098
|
playBtn.setAttribute("aria-pressed", state.playing ? "true" : "false");
|
|
913
1099
|
playBtn.textContent = state.playing ? "\\u23F8 pause" : "\\u25B6 play";
|
|
1100
|
+
// The follow control reads the ticker's own state, never a second
|
|
1101
|
+
// "am I playing" the page keeps for itself, so the two can never
|
|
1102
|
+
// disagree. It closes while the board plays because a redraw lands
|
|
1103
|
+
// on top of the open dropdown and loses the pick.
|
|
1104
|
+
el("agentSelect").disabled = state.playing;
|
|
1105
|
+
el("agentSelectHint").hidden = !state.playing;
|
|
914
1106
|
},
|
|
915
1107
|
hasNext: hasNext,
|
|
916
1108
|
wait: liveWait,
|
|
@@ -956,20 +1148,72 @@ function pageScript() {
|
|
|
956
1148
|
el("chatLogPopupClose").addEventListener("click", function () { el("chatLogPopup").hidden = true; });
|
|
957
1149
|
|
|
958
1150
|
// ---- the pill rail and its typeahead ----------------------------------
|
|
1151
|
+
// The deception rail is tmct.page.pillsForMudiii's own output, rendered and
|
|
1152
|
+
// nothing more: an address pill per live agent, then a true and a false
|
|
1153
|
+
// claim about every individual the addressee could act on. The false cell is
|
|
1154
|
+
// the board's own point reflection, so a lie is always in bounds and never
|
|
1155
|
+
// accidentally true. Which one a pill carries is shown by a glyph in CSS
|
|
1156
|
+
// ::before, never in the submitted text — a clicked lie reads exactly like a
|
|
1157
|
+
// typed one once it is in the input.
|
|
1158
|
+
//
|
|
1159
|
+
// The fixed seeds ahead of it are the town square's OWN verbs, checked
|
|
1160
|
+
// against the lane's regexes rather than borrowed from another page: this
|
|
1161
|
+
// world has no "look".
|
|
959
1162
|
function renderChatPills() {
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
livePills = pills;
|
|
963
|
-
el("chatPills").innerHTML = pills.map(function (p) {
|
|
1163
|
+
const seeds = SEED_COMMANDS.map(function (c) { return { command: c, label: c }; });
|
|
1164
|
+
const seedHtml = seeds.map(function (p) {
|
|
964
1165
|
return '<button type="button" class="pill" data-command="' + esc(p.command) + '">' + esc(p.label) + "</button>";
|
|
965
1166
|
}).join("");
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1167
|
+
const rail = window.tmct.page.pillsForMudiii(agentsById, itemsById, selectedAddresseeId, { gridSize: gridSizeOf() });
|
|
1168
|
+
selectedAddresseeId = rail.addresseeId;
|
|
1169
|
+
const addrHtml = rail.addressPills.map(function (p) {
|
|
1170
|
+
return '<button type="button" class="pill" data-role="dyn-addr" data-id="' + esc(p.id) + '"'
|
|
1171
|
+
+ (p.id === selectedAddresseeId ? ' data-active="1"' : "") + ">" + esc(p.label) + "</button>";
|
|
1172
|
+
}).join("");
|
|
1173
|
+
const claims = rail.claimPills.map(function (p) {
|
|
1174
|
+
return { command: p.sentence, label: p.text, truth: p.truth };
|
|
1175
|
+
});
|
|
1176
|
+
const claimHtml = claims.map(function (p) {
|
|
1177
|
+
return '<button type="button" class="pill" data-role="dyn-claim" data-truth="' + (p.truth ? "true" : "false")
|
|
1178
|
+
+ '" data-command="' + esc(p.command) + '">' + esc(p.label) + "</button>";
|
|
1179
|
+
}).join("");
|
|
1180
|
+
livePills = seeds.concat(claims);
|
|
1181
|
+
el("chatPills").innerHTML = seedHtml + addrHtml + claimHtml;
|
|
970
1182
|
if (pillComplete) pillComplete.refresh();
|
|
971
1183
|
}
|
|
972
1184
|
|
|
1185
|
+
// A pill APPENDS rather than replacing, so two clicks compose one line. The
|
|
1186
|
+
// second click of a double is what submits: the text it would have appended
|
|
1187
|
+
// went in on the first click of that same pair, which is why nothing is
|
|
1188
|
+
// appended again here.
|
|
1189
|
+
function appendToChatInput(text) {
|
|
1190
|
+
const input = el("chatInput");
|
|
1191
|
+
const head = input.value.replace(/\\s+$/, "");
|
|
1192
|
+
input.value = (head ? head + " " : "") + text;
|
|
1193
|
+
input.focus();
|
|
1194
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
el("chatPills").addEventListener("click", function (e) {
|
|
1198
|
+
const btn = e.target.closest(".pill");
|
|
1199
|
+
if (!btn) return;
|
|
1200
|
+
if (btn.getAttribute("data-role") === "dyn-addr") {
|
|
1201
|
+
selectedAddresseeId = btn.getAttribute("data-id");
|
|
1202
|
+
renderChatPills();
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const command = btn.getAttribute("data-command");
|
|
1206
|
+
if (!command) return;
|
|
1207
|
+
if (e.detail > 1) {
|
|
1208
|
+
const input = el("chatInput");
|
|
1209
|
+
const line = input.value.trim();
|
|
1210
|
+
input.value = "";
|
|
1211
|
+
if (line) sendCommand(line);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
appendToChatInput(command);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
973
1217
|
function wirePillComplete() {
|
|
974
1218
|
pillComplete = createPillComplete({
|
|
975
1219
|
input: el("chatInput"),
|
|
@@ -1000,7 +1244,8 @@ function pageScript() {
|
|
|
1000
1244
|
// still refused entirely client-side — nothing is written, and the food
|
|
1001
1245
|
// pill stays armed for another try.
|
|
1002
1246
|
window.mudiiiHandleSceneClick = function (cellId) {
|
|
1003
|
-
if (!
|
|
1247
|
+
if (!session || editing) return;
|
|
1248
|
+
if (!foodArmed) { walkFollowedTo(cellId); return; }
|
|
1004
1249
|
const reason = blockedCellReason(cellId, props, agentsList());
|
|
1005
1250
|
if (reason) { setSceneStatus(reason); return; }
|
|
1006
1251
|
sendCommand("put food at " + cellId).then(function () {
|
|
@@ -1009,6 +1254,82 @@ function pageScript() {
|
|
|
1009
1254
|
});
|
|
1010
1255
|
};
|
|
1011
1256
|
|
|
1257
|
+
// ---- driving one agent by hand ------------------------------------------
|
|
1258
|
+
// Every press here spends a turn: driveAgent runs the SAME whole-world tick
|
|
1259
|
+
// autoplay runs, so the ecology pass runs and every other agent decides and
|
|
1260
|
+
// moves with it. That is what the status line has to say, or the ring reads
|
|
1261
|
+
// as a free nudge that costs nothing.
|
|
1262
|
+
function followedAgentId() {
|
|
1263
|
+
const id = camera.selectedId;
|
|
1264
|
+
return id && agentsById[id] ? id : null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// One lit glyph, and it is the followed agent's own facing — the reading
|
|
1268
|
+
// that makes sense on an open grid, where nearly every step is available and
|
|
1269
|
+
// lighting what is available would say nothing. With nobody followed there
|
|
1270
|
+
// is no facing to show and nothing to walk, so the ring goes away.
|
|
1271
|
+
function renderDriveRing() {
|
|
1272
|
+
const followed = followedAgentId();
|
|
1273
|
+
const ring = el("driveRing");
|
|
1274
|
+
ring.hidden = !followed;
|
|
1275
|
+
if (!followed) return;
|
|
1276
|
+
const facing = agentsById[followed].facing || DATA.defaultFacing;
|
|
1277
|
+
const buttons = ring.querySelectorAll("[data-drive]");
|
|
1278
|
+
for (let i = 0; i < buttons.length; i += 1) {
|
|
1279
|
+
buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute("data-drive") === facing ? "true" : "false");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function drivePress(direction) {
|
|
1284
|
+
const followed = followedAgentId();
|
|
1285
|
+
if (!session || !followed) { setSceneStatus("pick an agent to follow \\u2014 the ring walks whoever the camera is on."); return; }
|
|
1286
|
+
// A hand-driven turn is a deliberate one, so autoplay stands down rather
|
|
1287
|
+
// than racing the press.
|
|
1288
|
+
autoOn = false;
|
|
1289
|
+
if (ticker) ticker.pause();
|
|
1290
|
+
return serializeTick(async function () {
|
|
1291
|
+
const result = await session.driveAgent(followed, direction);
|
|
1292
|
+
applyTickResult(result);
|
|
1293
|
+
renderAll();
|
|
1294
|
+
const driven = result.driven || {};
|
|
1295
|
+
setSceneStatus(driven.accepted
|
|
1296
|
+
? followed + " went " + driven.direction + " to " + driven.cell + " \\u2014 turn " + result.turn + ", and the whole square moved with it."
|
|
1297
|
+
: followed + " could not go " + direction + " \\u2014 the turn was spent anyway, and the whole square moved.");
|
|
1298
|
+
return result;
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
el("driveRing").addEventListener("click", function (e) {
|
|
1303
|
+
const btn = e.target.closest("[data-drive]");
|
|
1304
|
+
if (!btn || btn.disabled) return;
|
|
1305
|
+
drivePress(btn.getAttribute("data-drive"));
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
// A ground click with nothing armed walks the followed agent one step along
|
|
1309
|
+
// the route to the cell, and draws the whole route it is heading down. The
|
|
1310
|
+
// route comes from the world's own exit search, so a cell behind a building
|
|
1311
|
+
// is declined rather than drawn as a line through the wall.
|
|
1312
|
+
async function walkFollowedTo(target) {
|
|
1313
|
+
const followed = followedAgentId();
|
|
1314
|
+
if (!followed) { setSceneStatus("pick an agent to follow \\u2014 a click on the ground walks whoever the camera is on."); return; }
|
|
1315
|
+
const from = agentsById[followed].cell;
|
|
1316
|
+
if (target === from) { setSceneStatus(followed + " is already at " + target + "."); return; }
|
|
1317
|
+
callScene("flashCell", target);
|
|
1318
|
+
const snap = await session.snapshot();
|
|
1319
|
+
const route = window.tmct.page.routeBetweenCells(snap.rows, from, target);
|
|
1320
|
+
if (!route || !route.directions.length) {
|
|
1321
|
+
callScene("clearRoute");
|
|
1322
|
+
setSceneStatus("no way through to " + target + " from " + from + ".");
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
callScene("showRoute", route.cells);
|
|
1326
|
+
await drivePress(route.directions[0]);
|
|
1327
|
+
const left = route.directions.length - 1;
|
|
1328
|
+
setSceneStatus(left
|
|
1329
|
+
? followed + " is heading for " + target + " \\u2014 " + left + " more step" + (left === 1 ? "" : "s") + ", one turn each."
|
|
1330
|
+
: followed + " reached " + target + ".");
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1012
1333
|
// ---- the HUD row --------------------------------------------------------
|
|
1013
1334
|
function renderHudRow() {
|
|
1014
1335
|
const ids = Object.keys(agentsById).sort();
|
|
@@ -1028,19 +1349,69 @@ function pageScript() {
|
|
|
1028
1349
|
card.querySelector(".hud-meter-fill").style.width = (fields.massPct === null ? 0 : fields.massPct) + "%";
|
|
1029
1350
|
card.querySelector(".hud-goal").textContent = fields.goal;
|
|
1030
1351
|
card.querySelector(".hud-plan").textContent = "plan: " + fields.planText;
|
|
1031
|
-
card
|
|
1032
|
-
? "believes: " + fields.beliefEntries.map(function (entry) {
|
|
1033
|
-
return entry[0] + (entry[1] ? " @ " + entry[1] : " unseen");
|
|
1034
|
-
}).join(" \\u00b7 ")
|
|
1035
|
-
: "";
|
|
1352
|
+
renderBelief(card, id, fields.beliefEntries);
|
|
1036
1353
|
}
|
|
1037
1354
|
}
|
|
1038
1355
|
|
|
1356
|
+
// A belief map grows with the cast, so the card shows the first three and a
|
|
1357
|
+
// count and keeps the rest behind a toggle. Which cards are open is held
|
|
1358
|
+
// against the AGENT ID, never the DOM: renderHudRow only rebuilds its
|
|
1359
|
+
// markup when the card count changes, and card slots are positional while
|
|
1360
|
+
// agents re-bind by sorted id, so state left in a card would follow the
|
|
1361
|
+
// slot rather than the animal.
|
|
1362
|
+
const BELIEF_SUMMARY_LIMIT = 3;
|
|
1363
|
+
function renderBelief(card, id, entries) {
|
|
1364
|
+
const toggle = card.querySelector(".hud-belief-toggle");
|
|
1365
|
+
const detail = card.querySelector(".hud-detail");
|
|
1366
|
+
const expanded = expandedAgents.has(id);
|
|
1367
|
+
const shown = entries.slice(0, BELIEF_SUMMARY_LIMIT).map(function (entry) {
|
|
1368
|
+
return entry[0] + (entry[1] ? " @ " + entry[1] : " unseen");
|
|
1369
|
+
}).join(" \\u00b7 ");
|
|
1370
|
+
const rest = entries.length - BELIEF_SUMMARY_LIMIT;
|
|
1371
|
+
card.querySelector(".hud-belief").textContent = entries.length
|
|
1372
|
+
? "believes: " + shown + (rest > 0 ? " +" + rest + " more" : "")
|
|
1373
|
+
: "";
|
|
1374
|
+
toggle.hidden = entries.length === 0;
|
|
1375
|
+
toggle.setAttribute("aria-expanded", expanded && entries.length ? "true" : "false");
|
|
1376
|
+
detail.hidden = !expanded || entries.length === 0;
|
|
1377
|
+
detail.innerHTML = entries.map(function (entry) {
|
|
1378
|
+
return '<div class="hud-detail-line">' + esc(believedFactSentence(entry[0], entry[1])) + "</div>";
|
|
1379
|
+
}).join("");
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
el("hudRow").addEventListener("click", function (e) {
|
|
1383
|
+
const toggle = e.target.closest(".hud-belief-toggle");
|
|
1384
|
+
if (!toggle) return;
|
|
1385
|
+
const card = toggle.closest(".hud-card");
|
|
1386
|
+
const id = card && card.getAttribute("data-agent");
|
|
1387
|
+
if (!id) return;
|
|
1388
|
+
if (expandedAgents.has(id)) expandedAgents.delete(id); else expandedAgents.add(id);
|
|
1389
|
+
renderHudRow();
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1039
1392
|
// ---- the top-down map panel ---------------------------------------------
|
|
1040
1393
|
function renderMapPanel() {
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1394
|
+
const board = el("mapPanelBoard");
|
|
1395
|
+
const size = gridSizeOf();
|
|
1396
|
+
// The cell divisions are two gradients stepped by this, so the drawn grid
|
|
1397
|
+
// and the dots' own percentages read off the same board size.
|
|
1398
|
+
board.style.setProperty("--map-cell-pct", (100 / size) + "%");
|
|
1399
|
+
const blocks = mapBlocksFor(props, size);
|
|
1400
|
+
const dots = mapDotsFor(agentsList(), itemsList(), size);
|
|
1401
|
+
// Blocks first, dots second: a live agent standing beside a building has
|
|
1402
|
+
// to sit on top of it, not under it.
|
|
1403
|
+
board.innerHTML = blocks.map(function (b) {
|
|
1404
|
+
return '<span class="map-block" style="left:' + b.xPct + '%;top:' + b.yPct + '%;width:' + b.sizePct
|
|
1405
|
+
+ '%;height:' + b.sizePct + '%" title="' + esc(b.id) + '"></span>';
|
|
1406
|
+
}).join("") + dots.map(function (d) {
|
|
1407
|
+
const dot = '<span class="map-dot map-dot-' + esc(d.kind) + '" style="left:' + d.xPct + '%;top:' + d.yPct + '%" title="' + esc(d.id) + '"></span>';
|
|
1408
|
+
// Items are named by their colour in the key; only the cast, which the
|
|
1409
|
+
// HUD and the follow control both name, carries its id on the board.
|
|
1410
|
+
if (d.kind !== "predator" && d.kind !== "prey") return dot;
|
|
1411
|
+
// A label on a dot near the right edge would run off the board, so
|
|
1412
|
+
// those hang to the left of their dot instead.
|
|
1413
|
+
const side = d.xPct > 70 ? " map-label-left" : "";
|
|
1414
|
+
return dot + '<span class="map-label mono' + side + '" style="left:' + d.xPct + '%;top:' + d.yPct + '%">' + esc(d.id) + "</span>";
|
|
1044
1415
|
}).join("");
|
|
1045
1416
|
el("mapPanelTurn").textContent = "turn " + globalTurn;
|
|
1046
1417
|
}
|
|
@@ -1056,7 +1427,11 @@ function pageScript() {
|
|
|
1056
1427
|
}
|
|
1057
1428
|
el("agentSelect").addEventListener("change", function () {
|
|
1058
1429
|
const id = el("agentSelect").value || null;
|
|
1059
|
-
|
|
1430
|
+
const mode = cameraModeBeforeFallback || camera.mode;
|
|
1431
|
+
cameraModeBeforeFallback = null;
|
|
1432
|
+
camera = { mode: mode, selectedId: id, status: null };
|
|
1433
|
+
renderCameraButtons();
|
|
1434
|
+
renderDriveRing();
|
|
1060
1435
|
callScene("setCamera", camera);
|
|
1061
1436
|
});
|
|
1062
1437
|
|
|
@@ -1069,6 +1444,7 @@ function pageScript() {
|
|
|
1069
1444
|
el("cameraMode").addEventListener("click", function (e) {
|
|
1070
1445
|
const btn = e.target.closest("button[data-mode]");
|
|
1071
1446
|
if (!btn) return;
|
|
1447
|
+
cameraModeBeforeFallback = null;
|
|
1072
1448
|
camera = { mode: btn.getAttribute("data-mode"), selectedId: camera.selectedId, status: null };
|
|
1073
1449
|
renderCameraButtons();
|
|
1074
1450
|
callScene("setCamera", camera);
|
|
@@ -1104,7 +1480,7 @@ function pageScript() {
|
|
|
1104
1480
|
el("playerCountSlider").addEventListener("change", function () { boot(); });
|
|
1105
1481
|
el("npcCountSlider").addEventListener("input", function () { showGoblinCount(chosenGoblinCount()); });
|
|
1106
1482
|
el("npcCountSlider").addEventListener("change", function () { boot(); });
|
|
1107
|
-
el("resetBtn").addEventListener("click", function () {
|
|
1483
|
+
el("resetBtn").addEventListener("click", function () { resetBoard(); });
|
|
1108
1484
|
const scenarioSelect = el("scenarioSelect");
|
|
1109
1485
|
if (scenarioSelect) {
|
|
1110
1486
|
scenarioSelect.addEventListener("change", function () {
|
|
@@ -1139,6 +1515,9 @@ function pageScript() {
|
|
|
1139
1515
|
// graph.
|
|
1140
1516
|
function worldOnlyRows(rows) { return rowsForWorld(rows, scenario().worldPayload.name); }
|
|
1141
1517
|
let editRows = [];
|
|
1518
|
+
// The FULL store, not the world's own rows: a term's synonyms and its is-a
|
|
1519
|
+
// chain mostly live in the background corpus, not in the square's vocabulary.
|
|
1520
|
+
let allStoreRows = [];
|
|
1142
1521
|
|
|
1143
1522
|
function renderEditPlacements() {
|
|
1144
1523
|
const placements = {};
|
|
@@ -1163,11 +1542,12 @@ function pageScript() {
|
|
|
1163
1542
|
el("editModeBtn").textContent = "back to playing";
|
|
1164
1543
|
el("editModeBtn").setAttribute("aria-pressed", "true");
|
|
1165
1544
|
const snap = await session.snapshot();
|
|
1545
|
+
allStoreRows = snap.rows;
|
|
1166
1546
|
editRows = worldOnlyRows(snap.rows);
|
|
1167
1547
|
el("editorText").value = renderMudEditorText(editRows, gridWorldEditorState(snap.state));
|
|
1168
1548
|
el("editorStatus").className = "edit-status";
|
|
1169
1549
|
el("editorStatus").textContent = "";
|
|
1170
|
-
|
|
1550
|
+
renderSuggestionPills();
|
|
1171
1551
|
renderEditPlacements();
|
|
1172
1552
|
}
|
|
1173
1553
|
|
|
@@ -1178,9 +1558,74 @@ function pageScript() {
|
|
|
1178
1558
|
el("editModeBtn").setAttribute("aria-pressed", "false");
|
|
1179
1559
|
}
|
|
1180
1560
|
|
|
1561
|
+
// The lateral SKOS neighbourhood plus the vertical is-a chain for whatever
|
|
1562
|
+
// word the cursor sits behind. Nothing found is nothing shown — an honest
|
|
1563
|
+
// miss, never a guessed suggestion.
|
|
1564
|
+
function renderSuggestionPills() {
|
|
1565
|
+
const box = el("editorPills");
|
|
1566
|
+
const term = wordBeforeCursor(el("editorText").value, el("editorText").selectionStart);
|
|
1567
|
+
if (!term || !window.tmct) { box.innerHTML = ""; return; }
|
|
1568
|
+
const related = window.tmct.page.relatedForTerm ? window.tmct.page.relatedForTerm(allStoreRows, term) : null;
|
|
1569
|
+
const chain = window.tmct.page.classAncestorChain ? window.tmct.page.classAncestorChain(term, allStoreRows) : [];
|
|
1570
|
+
const seen = {};
|
|
1571
|
+
seen[term] = true;
|
|
1572
|
+
const out = [];
|
|
1573
|
+
const push = function (label) { if (label && !seen[label]) { seen[label] = true; out.push(label); } };
|
|
1574
|
+
if (related) {
|
|
1575
|
+
related.synonyms.forEach(push);
|
|
1576
|
+
related.related.forEach(function (r) { push(r.prefLabel); });
|
|
1577
|
+
}
|
|
1578
|
+
chain.slice(1).forEach(push);
|
|
1579
|
+
box.innerHTML = out.slice(0, 8).map(function (s) {
|
|
1580
|
+
return '<button type="button" class="pill" data-insert="' + esc(s) + '">' + esc(s) + "</button>";
|
|
1581
|
+
}).join("");
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
el("editorPills").addEventListener("click", function (e) {
|
|
1585
|
+
const btn = e.target.closest(".pill");
|
|
1586
|
+
if (!btn) return;
|
|
1587
|
+
const area = el("editorText");
|
|
1588
|
+
const pos = area.selectionStart;
|
|
1589
|
+
const word = wordBeforeCursor(area.value, pos);
|
|
1590
|
+
const insert = btn.getAttribute("data-insert");
|
|
1591
|
+
area.value = area.value.slice(0, pos - word.length) + insert + area.value.slice(pos);
|
|
1592
|
+
const next = pos - word.length + insert.length;
|
|
1593
|
+
area.setSelectionRange(next, next);
|
|
1594
|
+
area.focus();
|
|
1595
|
+
onEditorChanged();
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
let suggestTimer = null;
|
|
1181
1599
|
let syncTimer = null;
|
|
1600
|
+
function scheduleSuggestions() { clearTimeout(suggestTimer); suggestTimer = setTimeout(renderSuggestionPills, 180); }
|
|
1182
1601
|
function scheduleSync() { clearTimeout(syncTimer); syncTimer = setTimeout(applyEditorText, 450); }
|
|
1183
|
-
|
|
1602
|
+
function onEditorChanged() { scheduleSuggestions(); scheduleSync(); }
|
|
1603
|
+
el("editorText").addEventListener("input", onEditorChanged);
|
|
1604
|
+
el("editorText").addEventListener("click", scheduleSuggestions);
|
|
1605
|
+
el("editorText").addEventListener("keyup", function (e) {
|
|
1606
|
+
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].indexOf(e.key) !== -1) scheduleSuggestions();
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// An edit changes the facts, so the meshes have to move with them —
|
|
1610
|
+
// otherwise a deleted well stands in the square until the next Reset.
|
|
1611
|
+
// boot() drops the camera back to overhead and empties the scene's own
|
|
1612
|
+
// agent groups, so the visitor's camera is put back and the live cast is
|
|
1613
|
+
// redrawn straight after: autoplay is paused in edit mode, so nothing else
|
|
1614
|
+
// would repopulate them.
|
|
1615
|
+
async function rebuildSceneFromEdit(result) {
|
|
1616
|
+
props = propPlacementsFrom(editRows, DATA.assetManifest);
|
|
1617
|
+
// A prop may now stand where an animal was, so a change re-casts onto the
|
|
1618
|
+
// edited board. An edit that wrote and retracted nothing leaves the cast
|
|
1619
|
+
// exactly where it stood.
|
|
1620
|
+
if (result && (result.added || result.removed)) {
|
|
1621
|
+
applyTickResult(await session.recast({ agents: cast }));
|
|
1622
|
+
}
|
|
1623
|
+
await callScene("boot", {
|
|
1624
|
+
propPlacements: props, assetManifest: DATA.assetManifest, gridSize: gridSizeOf(), cellSize: 1,
|
|
1625
|
+
});
|
|
1626
|
+
callScene("setCamera", camera);
|
|
1627
|
+
callScene("applyTick", { agents: agentsById, items: itemsById, ecology: [] });
|
|
1628
|
+
}
|
|
1184
1629
|
|
|
1185
1630
|
async function applyEditorText() {
|
|
1186
1631
|
if (!session) return;
|
|
@@ -1189,7 +1634,9 @@ function pageScript() {
|
|
|
1189
1634
|
status.textContent = "reading the square\\u2026";
|
|
1190
1635
|
const result = await serializeTick(function () { return session.applyEdit(el("editorText").value); });
|
|
1191
1636
|
const snap = await session.snapshot();
|
|
1637
|
+
allStoreRows = snap.rows;
|
|
1192
1638
|
editRows = worldOnlyRows(snap.rows);
|
|
1639
|
+
await rebuildSceneFromEdit(result);
|
|
1193
1640
|
if (result && result.unrecognized && result.unrecognized.length) {
|
|
1194
1641
|
status.className = "edit-status pending";
|
|
1195
1642
|
status.textContent = result.unrecognized.length + " line" + (result.unrecognized.length === 1 ? "" : "s")
|
|
@@ -1204,8 +1651,11 @@ function pageScript() {
|
|
|
1204
1651
|
}
|
|
1205
1652
|
|
|
1206
1653
|
// ---- booting ---------------------------------------------------------
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1654
|
+
// The board opens playing: a square standing still reads as broken, and the
|
|
1655
|
+
// first thing anyone does is press play anyway. A visitor who asked for
|
|
1656
|
+
// reduced motion gets the opening board drawn and left still — the play
|
|
1657
|
+
// control is right there — because an autoplaying board is exactly the
|
|
1658
|
+
// unasked-for movement that setting is about.
|
|
1209
1659
|
let bootSeq = 0;
|
|
1210
1660
|
async function boot() {
|
|
1211
1661
|
const seq = bootSeq += 1;
|
|
@@ -1214,14 +1664,20 @@ function pageScript() {
|
|
|
1214
1664
|
globalTurn = 0;
|
|
1215
1665
|
tickQueue = createSerialQueue();
|
|
1216
1666
|
camera = { mode: "follow", selectedId: null, status: null };
|
|
1667
|
+
cameraModeBeforeFallback = null;
|
|
1668
|
+
expandedAgents.clear();
|
|
1217
1669
|
const s = scenario();
|
|
1218
|
-
const foxes =
|
|
1219
|
-
const goblins =
|
|
1670
|
+
const foxes = mintRoster(rosterPrefixFor(s, "predator"), chosenFoxCount());
|
|
1671
|
+
const goblins = mintRoster(rosterPrefixFor(s, "prey"), chosenGoblinCount());
|
|
1220
1672
|
cast = foxes.concat(goblins);
|
|
1221
1673
|
showFoxCount(foxes.length);
|
|
1222
1674
|
showGoblinCount(goblins.length);
|
|
1223
1675
|
props = propPlacementsFrom((s.worldPayload && s.worldPayload.facts) || [], DATA.assetManifest);
|
|
1224
|
-
const opened = await window.tmct.open(s.worldPayload, {
|
|
1676
|
+
const opened = await window.tmct.open(s.worldPayload, {
|
|
1677
|
+
agents: cast,
|
|
1678
|
+
epoch: 0,
|
|
1679
|
+
getTeachEnabled: function () { return el("teachToggle").checked; },
|
|
1680
|
+
});
|
|
1225
1681
|
if (seq !== bootSeq) return;
|
|
1226
1682
|
session = opened;
|
|
1227
1683
|
agentsById = {};
|
|
@@ -1239,6 +1695,40 @@ function pageScript() {
|
|
|
1239
1695
|
camera.selectedId = Object.keys(opening.agents || {}).sort()[0] || null;
|
|
1240
1696
|
applyTickResult(opening);
|
|
1241
1697
|
renderAll();
|
|
1698
|
+
if (!prefersReducedMotion()) {
|
|
1699
|
+
autoOn = true;
|
|
1700
|
+
ensureTicker().play();
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Reset re-casts the store it already has rather than opening a new one:
|
|
1705
|
+
// the world's facts, everything taught into it and every editor change all
|
|
1706
|
+
// stand, and only the animals are minted again. Re-opening would throw the
|
|
1707
|
+
// taught facts away with the cast, which is not what "reset the board" says.
|
|
1708
|
+
// The engine owns the turn count, so a re-cast does not rewind it — the
|
|
1709
|
+
// status line says as much, because a counter that keeps climbing after a
|
|
1710
|
+
// Reset otherwise reads as a bug.
|
|
1711
|
+
async function resetBoard() {
|
|
1712
|
+
if (!session) return boot();
|
|
1713
|
+
autoOn = false;
|
|
1714
|
+
if (ticker) { ticker.pause(); ticker = null; }
|
|
1715
|
+
expandedAgents.clear();
|
|
1716
|
+
const s = scenario();
|
|
1717
|
+
const foxes = mintRoster(rosterPrefixFor(s, "predator"), chosenFoxCount());
|
|
1718
|
+
const goblins = mintRoster(rosterPrefixFor(s, "prey"), chosenGoblinCount());
|
|
1719
|
+
cast = foxes.concat(goblins);
|
|
1720
|
+
showFoxCount(foxes.length);
|
|
1721
|
+
showGoblinCount(goblins.length);
|
|
1722
|
+
const board = await serializeTick(function () { return session.recast({ agents: cast }); });
|
|
1723
|
+
camera = { mode: "follow", selectedId: Object.keys(board.agents || {}).sort()[0] || null, status: null };
|
|
1724
|
+
cameraModeBeforeFallback = null;
|
|
1725
|
+
applyTickResult(board);
|
|
1726
|
+
renderAll();
|
|
1727
|
+
setSceneStatus("re-cast \\u2014 the square's own facts stand, and its clock keeps running.");
|
|
1728
|
+
if (!prefersReducedMotion()) {
|
|
1729
|
+
autoOn = true;
|
|
1730
|
+
ensureTicker().play();
|
|
1731
|
+
}
|
|
1242
1732
|
}
|
|
1243
1733
|
|
|
1244
1734
|
function renderAll() {
|
|
@@ -1246,6 +1736,7 @@ function pageScript() {
|
|
|
1246
1736
|
renderMapPanel();
|
|
1247
1737
|
renderAgentSelect();
|
|
1248
1738
|
renderCameraButtons();
|
|
1739
|
+
renderDriveRing();
|
|
1249
1740
|
renderChatPills();
|
|
1250
1741
|
el("globalTurnCount").textContent = "turns: " + globalTurn;
|
|
1251
1742
|
}
|