@polycode-projects/the-mechanical-code-talker 5.0.6 → 5.0.7
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 +6 -0
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +8 -1
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +118 -23
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +228 -24
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +108 -25
- package/src/services/ledger-viz.mjs +4 -2
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +17 -0
- package/src/services/mudiii-scene.mjs +271 -24
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +302 -125
- package/src/services/plan-viz.mjs +23 -2
- package/src/services/predator-prey.mjs +82 -33
- package/src/services/research-viz.mjs +12 -19
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +10 -3
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- package/src/tools/server.mjs +31 -2
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
statsSummaryLine,
|
|
32
32
|
clearSiteAssetCaches,
|
|
33
33
|
fetchWithProgress,
|
|
34
|
+
loadSeedPayload,
|
|
34
35
|
renderStatsPanelInto,
|
|
35
36
|
loadProgressLine,
|
|
36
37
|
factTripleParts,
|
|
@@ -43,7 +44,7 @@ const DEFAULT_TITLE = "the-mechanical-code-talker — ingest";
|
|
|
43
44
|
/** The self-contained ingest page. Pure — the same output for the same
|
|
44
45
|
* `title` every time; every piece of state (the session, each grounded fact)
|
|
45
46
|
* is computed live in the browser once the sibling ingest bundle loads. */
|
|
46
|
-
export function renderIngestHtml({ title = DEFAULT_TITLE, seedStamp = "" } = {}) {
|
|
47
|
+
export function renderIngestHtml({ title = DEFAULT_TITLE, seedStamp = "", seedBytes = 0 } = {}) {
|
|
47
48
|
return `<!doctype html>
|
|
48
49
|
<html lang="en">
|
|
49
50
|
<head>
|
|
@@ -84,6 +85,14 @@ ${THEME_TOKENS_CSS}
|
|
|
84
85
|
.topbar-right { display: flex; align-items: center; gap: .7rem; }
|
|
85
86
|
.fact-pill { display: inline-flex; align-items: baseline; gap: .34rem; font-family: ${MONO_STACK}; font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); border: 1px solid var(--corpus-t1); border-radius: 99px; padding: .2rem .8rem; background: var(--corpus-soft); white-space: nowrap; }
|
|
86
87
|
.fact-pill .fact-pill-value { font-size: .94rem; letter-spacing: 0; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }
|
|
88
|
+
/* While the starter memory streams in, the pill says so and breathes — the
|
|
89
|
+
same three-state pill chat.html carries, for the same reason: what this
|
|
90
|
+
page knows grows while you watch it. */
|
|
91
|
+
.fact-pill[data-state="loading"] { animation: pill-breathe 1.6s ease-in-out infinite; }
|
|
92
|
+
.fact-pill[data-state="loading"] .fact-pill-value { font-size: .66rem; letter-spacing: .06em; text-transform: uppercase; font-weight: 600; }
|
|
93
|
+
.fact-pill[data-state="failed"] { border-color: var(--miss-t1, var(--corpus-t1)); }
|
|
94
|
+
@keyframes pill-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
|
|
95
|
+
@media (prefers-reduced-motion: reduce) { .fact-pill[data-state="loading"] { animation: none; } }
|
|
87
96
|
|
|
88
97
|
/* mode pills — Text | Document — the translate-tool idiom: two segments in
|
|
89
98
|
one rounded track, the active one filled. */
|
|
@@ -186,9 +195,9 @@ ${THEME_TOKENS_CSS}
|
|
|
186
195
|
<h1 class="subtitle">ingest — paste or drop text. It keeps the facts it can ground and skips the rest.</h1>
|
|
187
196
|
</div>
|
|
188
197
|
<div class="topbar-right">
|
|
189
|
-
<span class="fact-pill" id="factPill" aria-live="polite"
|
|
198
|
+
<span class="fact-pill" id="factPill" aria-live="polite" data-state="loading"
|
|
190
199
|
title="every fact this page's memory holds right now — the starter memory it booted with plus everything it has grounded since">
|
|
191
|
-
<span class="fact-pill-value" id="factPillValue"
|
|
200
|
+
<span class="fact-pill-value" id="factPillValue">loading</span><span id="factPillUnit"> starter memory…</span>
|
|
192
201
|
</span>
|
|
193
202
|
<div class="pills" role="group" aria-label="Input mode">
|
|
194
203
|
<button type="button" id="modeText" aria-pressed="true">Text</button>
|
|
@@ -254,6 +263,7 @@ ${THEME_TOKENS_CSS}
|
|
|
254
263
|
const statsSummaryLine = ${statsSummaryLine.toString()};
|
|
255
264
|
const clearSiteAssetCaches = ${clearSiteAssetCaches.toString()};
|
|
256
265
|
const fetchWithProgress = ${fetchWithProgress.toString()};
|
|
266
|
+
const loadSeedPayload = ${loadSeedPayload.toString()};
|
|
257
267
|
const renderStatsPanelInto = ${renderStatsPanelInto.toString()};
|
|
258
268
|
const el = (id) => document.getElementById(id);
|
|
259
269
|
|
|
@@ -275,7 +285,9 @@ ${THEME_TOKENS_CSS}
|
|
|
275
285
|
const seedToggleEl = el("seedToggle");
|
|
276
286
|
const fuzzyToggleEl = el("fuzzyToggle");
|
|
277
287
|
const statsPanelEl = el("statsPanel");
|
|
288
|
+
const factPillEl = el("factPill");
|
|
278
289
|
const factPillValueEl = el("factPillValue");
|
|
290
|
+
const factPillUnitEl = el("factPillUnit");
|
|
279
291
|
const askFormEl = el("askForm");
|
|
280
292
|
const askInputEl = el("askq");
|
|
281
293
|
const askGoBtn = el("askGo");
|
|
@@ -470,7 +482,8 @@ ${THEME_TOKENS_CSS}
|
|
|
470
482
|
try { stats = await window.tmct.page.memoryStats(session.memoryDir); }
|
|
471
483
|
catch { return; }
|
|
472
484
|
}
|
|
473
|
-
|
|
485
|
+
lastStatsTotal = Number(stats.total || 0);
|
|
486
|
+
renderFactPill();
|
|
474
487
|
renderStatsPanelInto(statsPanelEl, stats, {
|
|
475
488
|
bandLabel: bandLabelFor,
|
|
476
489
|
taughtHint: "nothing yet. Ingest some text and its grounded facts land here, with their source.",
|
|
@@ -496,6 +509,12 @@ ${THEME_TOKENS_CSS}
|
|
|
496
509
|
// ever return the copy this page asked for. Empty in a build with no seed.
|
|
497
510
|
const SEED_STAMP = ${JSON.stringify(seedStamp)};
|
|
498
511
|
const SEED_QUERY = SEED_STAMP ? "?b=" + SEED_STAMP : "";
|
|
512
|
+
// The builder's own measurement of chat-seed.json, in decompressed bytes.
|
|
513
|
+
// The response cannot supply this: over a compressed transfer Content-Length
|
|
514
|
+
// is the wire size while the stream yields decompressed bytes, so the two
|
|
515
|
+
// are not the same quantity. Both numbers in the percentage below are
|
|
516
|
+
// decompressed bytes, which is what makes it a real fraction.
|
|
517
|
+
const SEED_BYTES = ${Number(seedBytes) || 0};
|
|
499
518
|
|
|
500
519
|
let seedPayload = null;
|
|
501
520
|
let seedFacts = 0;
|
|
@@ -509,29 +528,89 @@ ${THEME_TOKENS_CSS}
|
|
|
509
528
|
// The one branch this page's seed choice makes: checked, fetch and parse
|
|
510
529
|
// the same chat-seed.json chat.html embeds; unchecked, skip the request
|
|
511
530
|
// outright and stay on the previous empty-store fast path.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
531
|
+
let seedLoadedBytes = 0;
|
|
532
|
+
let seedTotalBytes = SEED_BYTES;
|
|
533
|
+
let seedPercentShown = -1;
|
|
534
|
+
let lastStatsTotal = null;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* What the starter memory is doing right now, published as tmct.seed —
|
|
538
|
+
* loading, indexing, ready, failed, or skipped when the seed toggle is off.
|
|
539
|
+
* The same five phases chat.html publishes, for the same reasons: the pill
|
|
540
|
+
* can say the memory is still growing, and a test that asserts on seeded
|
|
541
|
+
* content can wait for "ready" instead of guessing.
|
|
542
|
+
*/
|
|
543
|
+
function setSeedPhase(phase, extra) {
|
|
544
|
+
window.tmct.seed = Object.assign({ state: phase, facts: seedFacts }, extra || {});
|
|
545
|
+
renderFactPill();
|
|
546
|
+
}
|
|
547
|
+
setSeedPhase("loading");
|
|
548
|
+
|
|
549
|
+
function noteSeedBytes(loaded, total) {
|
|
550
|
+
seedLoadedBytes = loaded;
|
|
551
|
+
seedTotalBytes = total;
|
|
552
|
+
const percent = seedProgressPercent();
|
|
553
|
+
if (percent === seedPercentShown) return;
|
|
554
|
+
seedPercentShown = percent;
|
|
555
|
+
renderFactPill();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Real progress, 0-100, or null when no true total is known — the pill then
|
|
559
|
+
* shows that it is loading and no number, never a figure inferred from how
|
|
560
|
+
* long it has been going. */
|
|
561
|
+
function seedProgressPercent() {
|
|
562
|
+
if (!(seedTotalBytes > 0)) return null;
|
|
563
|
+
return Math.min(100, Math.floor((seedLoadedBytes / seedTotalBytes) * 100));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function renderFactPill() {
|
|
567
|
+
const seedState = window.tmct.seed ? window.tmct.seed.state : "loading";
|
|
568
|
+
if (seedState === "loading") {
|
|
569
|
+
const percent = seedProgressPercent();
|
|
570
|
+
factPillEl.dataset.state = "loading";
|
|
571
|
+
factPillValueEl.textContent = percent === null ? "loading" : percent + "%";
|
|
572
|
+
factPillUnitEl.textContent = percent === null ? " starter memory\\u2026" : " of starter memory";
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// "ready" lands a moment before the first memoryStats read returns, and
|
|
576
|
+
// there is no count to show until it does. Holding the indexing words over
|
|
577
|
+
// that gap keeps the pill from flashing a zero it does not mean.
|
|
578
|
+
if (seedState === "indexing" || (seedState === "ready" && lastStatsTotal === null)) {
|
|
579
|
+
factPillEl.dataset.state = "loading";
|
|
580
|
+
factPillValueEl.textContent = "indexing";
|
|
581
|
+
factPillUnitEl.textContent = " " + seedFacts.toLocaleString() + " facts\\u2026";
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (seedState === "failed" && lastStatsTotal === null) {
|
|
585
|
+
factPillEl.dataset.state = "failed";
|
|
586
|
+
factPillValueEl.textContent = "no";
|
|
587
|
+
factPillUnitEl.textContent = " starter memory";
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
factPillEl.dataset.state = seedState === "failed" ? "failed" : "ready";
|
|
591
|
+
factPillValueEl.textContent = Number(lastStatsTotal || 0).toLocaleString();
|
|
592
|
+
factPillUnitEl.textContent = " facts in memory";
|
|
593
|
+
}
|
|
594
|
+
|
|
518
595
|
async function fetchSeedIfWanted() {
|
|
519
|
-
if (!seedToggleEl.checked) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
596
|
+
if (!seedToggleEl.checked) {
|
|
597
|
+
seedPayload = null;
|
|
598
|
+
seedFacts = 0;
|
|
599
|
+
setSeedPhase("skipped");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const outcome = await loadSeedPayload(fetchWithProgress, "./chat-seed.json", SEED_QUERY, (loaded, total) => {
|
|
603
|
+
noteProgress("seed", loaded, total || SEED_BYTES);
|
|
604
|
+
noteSeedBytes(loaded, total || SEED_BYTES);
|
|
605
|
+
});
|
|
606
|
+
seedPayload = outcome.payload;
|
|
607
|
+
seedFacts = outcome.status.facts;
|
|
608
|
+
if (outcome.status.state === "failed") {
|
|
609
|
+
console.error("tmct ingest: chat-seed.json unavailable — starting unseeded (" + outcome.status.error + ")");
|
|
610
|
+
setSeedPhase("failed", { error: outcome.status.error, attempts: outcome.status.attempts });
|
|
611
|
+
return;
|
|
534
612
|
}
|
|
613
|
+
setSeedPhase("indexing", { attempts: outcome.status.attempts });
|
|
535
614
|
}
|
|
536
615
|
const cloneMemoryPayload = ${cloneMemoryPayload.toString()};
|
|
537
616
|
async function newSession() {
|
|
@@ -610,6 +689,7 @@ ${THEME_TOKENS_CSS}
|
|
|
610
689
|
session = await newSession();
|
|
611
690
|
clearFactsPane();
|
|
612
691
|
askLogEl.textContent = "";
|
|
692
|
+
if (window.tmct.seed.state === "indexing") setSeedPhase("ready");
|
|
613
693
|
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
614
694
|
statusEl.textContent = statsSummaryLine(stats, bandLabelFor) + ". Ready.";
|
|
615
695
|
await renderStatsPanel(stats);
|
|
@@ -640,6 +720,9 @@ ${THEME_TOKENS_CSS}
|
|
|
640
720
|
setMode(false);
|
|
641
721
|
updateIngestEnabled();
|
|
642
722
|
updateAskEnabled();
|
|
723
|
+
// The session holds the seed now, so the phase settles here rather than
|
|
724
|
+
// when the bytes landed: "ready" means queryable.
|
|
725
|
+
if (window.tmct.seed.state === "indexing") setSeedPhase("ready");
|
|
643
726
|
const stats = await window.tmct.page.memoryStats(session.memoryDir);
|
|
644
727
|
const winkPart = winkStatus === "loaded"
|
|
645
728
|
? "wink-nlp: loaded"
|
|
@@ -1237,7 +1237,9 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
1237
1237
|
// hold it), and resolveAnsweredTerm has no way to tell that
|
|
1238
1238
|
// apart from the term genuinely being discussed.
|
|
1239
1239
|
const hit = resolveAnsweredTerm(result.answer, q, LEDGER.terms, tmct.page.normFactTerm);
|
|
1240
|
-
|
|
1240
|
+
// No label: the breadcrumb falls back to the term itself
|
|
1241
|
+
// (c.label || c.term) rather than showing the whole question.
|
|
1242
|
+
if (hit) refocusWithLabel(hit);
|
|
1241
1243
|
}
|
|
1242
1244
|
} catch {
|
|
1243
1245
|
pending.className = "a miss";
|
|
@@ -1440,7 +1442,7 @@ ${ledgerBundleAvailable ? `<script src="./ledger-browser.bundle.js"></script>` :
|
|
|
1440
1442
|
// full-stop-terminated, rendered only when the engine deduced one.
|
|
1441
1443
|
if (data && data.goal) addLine("a goal", "Goal (inferred): " + esc(data.goal.charAt(0).toUpperCase() + data.goal.slice(1)) + ".");
|
|
1442
1444
|
const hit = resolveAnsweredTerm(answer, q, LEDGER.terms, tmct.page.normFactTerm);
|
|
1443
|
-
if (hit) refocusWithLabel(hit
|
|
1445
|
+
if (hit) refocusWithLabel(hit);
|
|
1444
1446
|
} else {
|
|
1445
1447
|
const tips = LEDGER.terms.filter((t) => t.term.length >= 3).slice(0, 2)
|
|
1446
1448
|
.map((t) => '"what is ' + esc(t.term) + '"').join(" \\u00b7 ");
|
|
@@ -104,6 +104,50 @@ export async function fetchWithProgress(url, onProgress) {
|
|
|
104
104
|
return new Blob(chunks);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Fetch and parse the starter-memory seed, and say what actually happened.
|
|
109
|
+
*
|
|
110
|
+
* Resolves to `{ payload, status }` and never rejects. `payload` is the parsed
|
|
111
|
+
* seed, or null. `status` is the small, payload-free record a page publishes
|
|
112
|
+
* as `tmct.seed` and a test reads back:
|
|
113
|
+
*
|
|
114
|
+
* { state: "loaded", facts, attempts }
|
|
115
|
+
* { state: "failed", facts: 0, attempts, error }
|
|
116
|
+
*
|
|
117
|
+
* The record exists because a seed that failed to download and a store that is
|
|
118
|
+
* empty on purpose both leave the fact count at zero, and until now nothing
|
|
119
|
+
* downstream could tell them apart — not the page, which went on answering as
|
|
120
|
+
* a normal empty chat, and not a test, which could only report the zero. A
|
|
121
|
+
* page that cannot ground an answer must be able to say WHY it cannot, and
|
|
122
|
+
* that starts with knowing.
|
|
123
|
+
*
|
|
124
|
+
* It resolves rather than throwing because a failed seed is not a failed boot:
|
|
125
|
+
* you can still teach the page facts of your own. Boot carries on, and the
|
|
126
|
+
* caller decides what to show.
|
|
127
|
+
*
|
|
128
|
+
* Two attempts, the second under a cache-busting query param: a CDN edge can
|
|
129
|
+
* serve a corrupt, truncated or wrongly-encoded response, and a throw out of
|
|
130
|
+
* the read or the parse is the only signal of that. `seedQuery` already
|
|
131
|
+
* carries the build's own content-hash query string, if any, so the bust param
|
|
132
|
+
* joins with `&` or `?` accordingly.
|
|
133
|
+
*/
|
|
134
|
+
export async function loadSeedPayload(fetcher, url, seedQuery, onProgress) {
|
|
135
|
+
let lastError = null;
|
|
136
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
137
|
+
try {
|
|
138
|
+
const bust = attempt === 1 ? "" : (seedQuery ? "&" : "?") + "retry=1";
|
|
139
|
+
const blob = await fetcher(url + seedQuery + bust, onProgress);
|
|
140
|
+
const payload = JSON.parse(await blob.text());
|
|
141
|
+
const facts = (payload.individuals || []).filter((i) => i.class === "Fact").length;
|
|
142
|
+
return { payload, status: { state: "loaded", facts, attempts: attempt } };
|
|
143
|
+
} catch (err) {
|
|
144
|
+
lastError = err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const reason = lastError && lastError.message ? lastError.message : String(lastError);
|
|
148
|
+
return { payload: null, status: { state: "failed", facts: 0, attempts: 2, error: reason } };
|
|
149
|
+
}
|
|
150
|
+
|
|
107
151
|
/**
|
|
108
152
|
* (Re)render the docked "this session's memory" panel into `panelEl` from a
|
|
109
153
|
* memoryStats() result: a total-facts row, one row per seed band the session
|
package/src/services/mud-viz.mjs
CHANGED
|
@@ -406,6 +406,7 @@ ${MUD_SHARE_SKIN}
|
|
|
406
406
|
<section class="deck" aria-label="simulation controls">
|
|
407
407
|
<div class="deck-controls">
|
|
408
408
|
<button type="button" class="deck-play" id="autoToggle" aria-pressed="false">▶ play</button>
|
|
409
|
+
<button type="button" id="stepBtn">step</button>
|
|
409
410
|
<button type="button" id="resetBtn">reset</button>
|
|
410
411
|
${scenarioList.length > 1 ? ` <select id="scenarioSelect" class="deck-select" aria-label="which burrow to play">
|
|
411
412
|
${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " selected" : ""}>${escapeHtml(s.label || scenarioLabel(s.worldPayload?.name))}</option>`).join("\n")}
|
|
@@ -2092,6 +2093,7 @@ function pageScript() {
|
|
|
2092
2093
|
// that could start one is disabled for as long as the editor is open.
|
|
2093
2094
|
function setPlayControlsEnabled(on) {
|
|
2094
2095
|
el("autoToggle").disabled = !on;
|
|
2096
|
+
el("stepBtn").disabled = !on || autoOn;
|
|
2095
2097
|
el("playerCountSlider").disabled = !on;
|
|
2096
2098
|
el("resetBtn").disabled = !on;
|
|
2097
2099
|
for (const character of cast) {
|
|
@@ -2889,6 +2891,7 @@ function pageScript() {
|
|
|
2889
2891
|
|
|
2890
2892
|
function wireDeck() {
|
|
2891
2893
|
const playBtn = el("autoToggle");
|
|
2894
|
+
const stepBtn = el("stepBtn");
|
|
2892
2895
|
const delaySlider = el("delaySlider");
|
|
2893
2896
|
const maxTurnsSlider = el("maxTurnsSlider");
|
|
2894
2897
|
const playerCountSlider = el("playerCountSlider");
|
|
@@ -2898,6 +2901,7 @@ function pageScript() {
|
|
|
2898
2901
|
autoOn = !autoOn;
|
|
2899
2902
|
playBtn.setAttribute("aria-pressed", autoOn ? "true" : "false");
|
|
2900
2903
|
playBtn.textContent = autoOn ? "\\u23F8 pause" : "\\u25B6 play";
|
|
2904
|
+
stepBtn.disabled = autoOn;
|
|
2901
2905
|
// Every animal in the world, pane or none: the deck's play control is the
|
|
2902
2906
|
// world running, and an npc has no other control that could start it.
|
|
2903
2907
|
for (const character of everyone()) {
|
|
@@ -2906,6 +2910,19 @@ function pageScript() {
|
|
|
2906
2910
|
if (autoOn) ticker.play(); else ticker.pause();
|
|
2907
2911
|
}
|
|
2908
2912
|
});
|
|
2913
|
+
// One whole turn: every animal in the world (pane or none, same "everyone"
|
|
2914
|
+
// the play toggle above drives) takes exactly one tick, so every pane's own
|
|
2915
|
+
// "turn N" reading is one higher once every stepOnce() below has settled.
|
|
2916
|
+
// Each character keeps its own independent tick count (mud has no single
|
|
2917
|
+
// shared "round" the way adventure/spider-fly's one-ticker world does), so
|
|
2918
|
+
// stepping the whole deck is stepping every one of those counters once,
|
|
2919
|
+
// not advancing one shared counter by one.
|
|
2920
|
+
stepBtn.addEventListener("click", function () {
|
|
2921
|
+
if (!session || autoOn) return;
|
|
2922
|
+
stepBtn.disabled = true;
|
|
2923
|
+
const steps = everyone().map(function (character) { return ensureTicker(character).stepOnce(); });
|
|
2924
|
+
Promise.all(steps).finally(function () { stepBtn.disabled = autoOn; });
|
|
2925
|
+
});
|
|
2909
2926
|
delaySlider.addEventListener("input", function () {
|
|
2910
2927
|
delayMs = Number(delaySlider.value);
|
|
2911
2928
|
el("delayValue").textContent = delayMs + "ms";
|