@polycode-projects/the-mechanical-code-talker 6.0.21 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -15
- package/bin/tmct.mjs +8 -3
- package/package.json +1 -5
- package/src/adapters/memory/core.mjs +108 -96
- package/src/domain/agent-traits.mjs +1 -1
- package/src/domain/answer-variants.json +1 -1
- package/src/domain/cli-verbs.mjs +1 -0
- package/src/domain/memory/causal-stability.mjs +22 -5
- package/src/domain/memory/fact-order.mjs +3 -3
- package/src/domain/memory/provenance-time.mjs +35 -0
- package/src/domain/memory/retraction.mjs +3 -5
- package/src/domain/memory/trust.mjs +11 -11
- package/src/domain/news-feed.mjs +1 -1
- package/src/domain/seeded-random.mjs +6 -6
- package/src/services/adventure.mjs +10 -41
- package/src/services/chat-page-viz.mjs +12 -1111
- package/src/services/chat-session.mjs +20 -6
- package/src/services/chat.mjs +269 -197
- package/src/services/extract-facts.mjs +1 -1
- package/src/services/import-file.mjs +1 -1
- package/src/services/mud-viz.mjs +21 -1082
- package/src/services/mudiii-viz.mjs +0 -4
- package/src/services/pill-complete.mjs +5 -8
- package/src/services/predator-prey.mjs +3 -5
- package/src/surfaces/web/memory-ask-browser.bundle.js +107 -107
- package/src/surfaces/web/mud-browser-entry.mjs +8 -48
- package/test-benchmarks/agentbench/README.md +7 -10
- package/src/adapters/p2p/webrtc-transport.mjs +0 -169
- package/src/domain/p2p/facts.mjs +0 -102
- package/src/domain/p2p/peer-id.mjs +0 -47
- package/src/domain/p2p/provenance-relabel.mjs +0 -37
- package/src/domain/p2p/sync-filter.mjs +0 -43
- package/src/domain/p2p/wire.mjs +0 -126
- package/src/services/p2p-room.mjs +0 -848
- package/src/services/share-overlay-viz.mjs +0 -623
- package/src/surfaces/web/p2p-browser-entry.mjs +0 -39
package/src/services/mud-viz.mjs
CHANGED
|
@@ -85,14 +85,6 @@ import {
|
|
|
85
85
|
visibleRoomOf, roomKindForRoom, allRoomIds,
|
|
86
86
|
} from "./adventure-viz.mjs";
|
|
87
87
|
import { renderMudEditorText } from "./mud-editor.mjs";
|
|
88
|
-
import {
|
|
89
|
-
wireStateLabel, nodeRowsFor, nodeInitials, inviteLinkFor, inviteParamsFrom, tapeRowFor, tapeClock,
|
|
90
|
-
} from "./chat-page-viz.mjs";
|
|
91
|
-
import {
|
|
92
|
-
SHARE_OVERLAY_CSS, shareOverlayHtml, shareStepStates, activeWaves, offerBlobIn, peerTerm,
|
|
93
|
-
shareMessageFor, replyMessageFor, whatsAppShareUrl, isProbablyMobile, copyTextToClipboard, flashCopyTip,
|
|
94
|
-
} from "./share-overlay-viz.mjs";
|
|
95
|
-
export { offerBlobIn, peerTerm };
|
|
96
88
|
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
97
89
|
import { fnv1a32 } from "../domain/hash.mjs";
|
|
98
90
|
import { predatorSubjects } from "../domain/mud-facts.mjs";
|
|
@@ -125,25 +117,8 @@ const DEFAULT_MAX_TURNS = 400;
|
|
|
125
117
|
const MUD_NOTE_LINES = [
|
|
126
118
|
"Burrowing animals share one world here. The players slider picks how many get a window of their own; the npcs slider adds more animals that dig and forage without one. Each one only knows what it has dug up, asked about, or been told. Nobody sees the whole burrow except you, watching from the survey above.",
|
|
127
119
|
`This is a MUD, short for Multi Underground creature Dig. The name nods to MUD, or in its current form MUDII (mudii.co.uk), one of the first multiplayer text games. The dig-your-own-rooms idea came from a skim of Wikipedia's Colossal Cave Adventure article, the game that started the genre.`,
|
|
128
|
-
"Share the burrow and a second digger joins from another machine, browser to browser, with no server in the middle. Each animal is claimed by one node, one hand on one lemming, and everything either of you digs, eats or learns lands in the same graph.",
|
|
129
120
|
];
|
|
130
121
|
|
|
131
|
-
// The shared sharing overlay, in this page's own words — same markup, same
|
|
132
|
-
// ids, same ladder as chat.html's; only the vocabulary is a burrow's.
|
|
133
|
-
const MUD_SHARE_COPY = {
|
|
134
|
-
thing: "burrow",
|
|
135
|
-
lede: "This burrow lives in your browser and nowhere else. Sharing it opens a direct line (WebRTC) to one other browser: you send an invite, they send a reply, you paste it in. No account, no server, no copy anywhere in between.",
|
|
136
|
-
worldLabel: "the burrow you are sharing",
|
|
137
|
-
nodeLabel: "your node name",
|
|
138
|
-
namesNote: "both names are facts in the burrow, not settings.",
|
|
139
|
-
invitedEyebrow: "you have been invited to dig in",
|
|
140
|
-
joinBody: "Nothing runs until you press the button. It makes one reply for you to send back the way the invite reached you.",
|
|
141
|
-
dismiss: "dig on your own instead",
|
|
142
|
-
rosterTitle: "who digs this burrow",
|
|
143
|
-
nodeEmpty: "nobody else is digging here yet.",
|
|
144
|
-
idleNote: "this browser holds the only copy of this burrow.",
|
|
145
|
-
};
|
|
146
|
-
|
|
147
122
|
/** A character id's species — "mole-1" -> "mole", "groundhog-1" ->
|
|
148
123
|
* "groundhog" — mirroring spider-fly-viz.mjs's own classOfAgentId. Self-
|
|
149
124
|
* contained, `.toString()`-splice safe. */
|
|
@@ -243,84 +218,6 @@ export function isCreature(state, subject) {
|
|
|
243
218
|
return state.placements.get(subject)?.predicate === "mgx:currently-in";
|
|
244
219
|
}
|
|
245
220
|
|
|
246
|
-
// ---- the shared burrow: reading the P2P layer's own facts back ---------------
|
|
247
|
-
// All four readers below work on plain fact rows and nothing else, so the page
|
|
248
|
-
// draws a claim, a wave and a node name identically whether the world is shared
|
|
249
|
-
// with three machines or with none. Pure and `.toString()`-splice safe.
|
|
250
|
-
|
|
251
|
-
/** Every assertion time a provenance string carries, in milliseconds. A tag is
|
|
252
|
-
* `<kind>:<id>@<ISO>`, and a fact asserted more than once holds several joined
|
|
253
|
-
* by " | " — a peer's relabelled copy of a tag keeps the time the fact was
|
|
254
|
-
* actually asserted, so reading them all back is what lets a claim be settled
|
|
255
|
-
* by its oldest and a wave by its newest. Pure, self-contained. */
|
|
256
|
-
export function provenanceStamps(provenance) {
|
|
257
|
-
const stamps = [];
|
|
258
|
-
const text = String(provenance || "");
|
|
259
|
-
const pattern = /@(\d{4}-\d{2}-\d{2}T[0-9:.]+Z)/g;
|
|
260
|
-
let found = pattern.exec(text);
|
|
261
|
-
while (found !== null) {
|
|
262
|
-
const at = Date.parse(found[1]);
|
|
263
|
-
if (!Number.isNaN(at)) stamps.push(at);
|
|
264
|
-
found = pattern.exec(text);
|
|
265
|
-
}
|
|
266
|
-
return stamps;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Which peer plays `character`, or null when nobody has claimed it. Claims are
|
|
270
|
-
* add-only and never retracted, so two peers claiming the same animal both
|
|
271
|
-
* write and every page settles it the same way: the OLDEST claim wins, with
|
|
272
|
-
* the lower peer term breaking a dead heat so two machines reading the same
|
|
273
|
-
* rows can never disagree. Pure, self-contained (provenanceStamps is spliced
|
|
274
|
-
* alongside it). */
|
|
275
|
-
export function claimantOf(rows, character) {
|
|
276
|
-
let owner = null;
|
|
277
|
-
let ownerAt = Infinity;
|
|
278
|
-
for (const row of rows || []) {
|
|
279
|
-
if (row.subject !== character || row.predicate !== "mgx:playedBy") continue;
|
|
280
|
-
let at = Infinity;
|
|
281
|
-
for (const stamp of provenanceStamps(row.provenance)) if (stamp < at) at = stamp;
|
|
282
|
-
if (at < ownerAt || (at === ownerAt && owner !== null && String(row.object) < owner)) {
|
|
283
|
-
ownerAt = at;
|
|
284
|
-
owner = String(row.object);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return owner;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Which characters are waving right now — a read-time recency question over
|
|
291
|
-
* the newest tag on each `mgx:waved` fact, never stored state and never a
|
|
292
|
-
* retraction. A wave older than the window simply stops being read as one.
|
|
293
|
-
* Pure, self-contained. */
|
|
294
|
-
export function wavingCharacters(rows, nowMs, windowMs = 8000) {
|
|
295
|
-
const waving = [];
|
|
296
|
-
for (const row of rows || []) {
|
|
297
|
-
if (row.predicate !== "mgx:waved") continue;
|
|
298
|
-
let newest = null;
|
|
299
|
-
for (const stamp of provenanceStamps(row.provenance)) if (newest === null || stamp > newest) newest = stamp;
|
|
300
|
-
if (newest === null) continue;
|
|
301
|
-
const age = nowMs - newest;
|
|
302
|
-
if (age < 0 || age > windowMs) continue;
|
|
303
|
-
if (waving.indexOf(row.subject) === -1) waving.push(row.subject);
|
|
304
|
-
}
|
|
305
|
-
return waving;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** The name a peer chose for its node, from its latest `mgx:nodeName` fact,
|
|
309
|
-
* falling back to a shortened peer id. A label never waits for a name to
|
|
310
|
-
* arrive. Pure, self-contained. */
|
|
311
|
-
export function nodeNameFor(rows, peerId) {
|
|
312
|
-
const want = peerTerm(peerId);
|
|
313
|
-
let name = null;
|
|
314
|
-
let nameAt = -Infinity;
|
|
315
|
-
for (const row of rows || []) {
|
|
316
|
-
if (row.predicate !== "mgx:nodeName" || peerTerm(row.subject) !== want) continue;
|
|
317
|
-
let at = -Infinity;
|
|
318
|
-
for (const stamp of provenanceStamps(row.provenance)) if (stamp > at) at = stamp;
|
|
319
|
-
if (at >= nameAt) { nameAt = at; name = String(row.object); }
|
|
320
|
-
}
|
|
321
|
-
return name || want.slice(0, 8);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
221
|
/** The self-contained mud.html page. Pure — identical output for identical
|
|
325
222
|
* input; every other piece of state (the live world, chat, ticks) is
|
|
326
223
|
* computed in the browser once the sibling bundle loads.
|
|
@@ -388,22 +285,14 @@ ${engineBundleJs ? "" : `<link rel="icon" href="./favicon.svg" type="image/svg+x
|
|
|
388
285
|
<style>
|
|
389
286
|
${THEME_TOKENS_CSS}
|
|
390
287
|
${MUD_STYLE}
|
|
391
|
-
${SHARE_OVERLAY_CSS}
|
|
392
|
-
${MUD_SHARE_SKIN}
|
|
393
288
|
</style>
|
|
394
289
|
</head>
|
|
395
290
|
<body>
|
|
396
291
|
<main>
|
|
397
292
|
<header class="mud-topbar">
|
|
398
293
|
<h1 class="eyebrow">${demoEyebrowHtml("mud", "mud")}</h1>
|
|
399
|
-
<
|
|
400
|
-
|
|
401
|
-
title="whether this burrow is shared with anyone"><i class="state-dot"></i><span id="statePillWord">not shared</span></button>
|
|
402
|
-
<button type="button" id="shareBtn">share</button>
|
|
403
|
-
<button type="button" id="joinOpenBtn">join</button>
|
|
404
|
-
<a class="mud-topbar-help" href="./help.html#sharing" target="_blank" rel="noopener"
|
|
405
|
-
title="how sharing works, in a new tab" aria-label="how sharing works, opens in a new tab">?</a>
|
|
406
|
-
</div>
|
|
294
|
+
<a class="mud-topbar-help" href="./help.html" target="_blank" rel="noopener"
|
|
295
|
+
title="how this page works, in a new tab" aria-label="how this page works, opens in a new tab">?</a>
|
|
407
296
|
</header>
|
|
408
297
|
<div class="deck-row">
|
|
409
298
|
<section class="deck" aria-label="simulation controls">
|
|
@@ -463,7 +352,6 @@ ${scenarioList.map((s, i) => ` <option value="${i}"${i === 0 ? " select
|
|
|
463
352
|
<div class="world-map-key" id="worldMapKey"></div>
|
|
464
353
|
</section>
|
|
465
354
|
</div>
|
|
466
|
-
${shareOverlayHtml({ copy: MUD_SHARE_COPY, withTape: true })}
|
|
467
355
|
<div class="mud-stage" id="mudStage" data-panes="${openingCount}">
|
|
468
356
|
${paneHtml}
|
|
469
357
|
</div>
|
|
@@ -515,7 +403,6 @@ export function paneMarkup(slot) {
|
|
|
515
403
|
return ` <section class="mud-window pane-${escapeHtml(slot)}" id="${w}" data-slot="${escapeHtml(slot)}" data-character="">
|
|
516
404
|
<div class="pane-head">
|
|
517
405
|
<h2 id="${w}-name">waiting</h2>
|
|
518
|
-
<span class="pane-node" id="${w}-node" hidden></span>
|
|
519
406
|
<span class="mono pane-turn" id="${w}-turn">turn 0</span>
|
|
520
407
|
</div>
|
|
521
408
|
<div class="room-stage">
|
|
@@ -559,11 +446,7 @@ export function paneMarkup(slot) {
|
|
|
559
446
|
</div>
|
|
560
447
|
</div>
|
|
561
448
|
<div class="chat-console">
|
|
562
|
-
<div class="
|
|
563
|
-
<button type="button" class="pill wave-btn" id="${w}-wave" title="wave, so everyone in this room sees it"
|
|
564
|
-
aria-label="wave">👋 wave</button>
|
|
565
|
-
<div class="chatpills" id="${w}-chatpills" role="group" aria-label="quick commands"></div>
|
|
566
|
-
</div>
|
|
449
|
+
<div class="chatpills" id="${w}-chatpills" role="group" aria-label="quick commands"></div>
|
|
567
450
|
<form class="chatask" id="${w}-chatform">
|
|
568
451
|
<span class="prompt mono">tmct></span>
|
|
569
452
|
<input id="${w}-chatq" type="text" placeholder="look" aria-label="type a command" disabled>
|
|
@@ -596,21 +479,14 @@ const MUD_STYLE = `
|
|
|
596
479
|
.eyebrow { font-family: ${MONO_STACK}; font-weight: 500; font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: var(--parchment); opacity: .9; margin: 0 0 .8rem; }
|
|
597
480
|
${EYEBROW_LINKS_CSS}
|
|
598
481
|
|
|
599
|
-
/* ---- the header: brand on the left, the
|
|
600
|
-
The same arrangement chat.html's topbar holds — sharing is page chrome,
|
|
601
|
-
not a simulation control, so it lives above the deck rather than in it. */
|
|
482
|
+
/* ---- the header: brand on the left, the way into the help page on the right ---- */
|
|
602
483
|
.mud-topbar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: 0 0 .8rem; }
|
|
603
484
|
.mud-topbar .eyebrow { margin: 0; }
|
|
604
|
-
.mud-topbar-
|
|
605
|
-
.mud-topbar-actions button:not(.state-pill), .mud-topbar-help {
|
|
485
|
+
.mud-topbar-help {
|
|
606
486
|
background: var(--lem-face); color: var(--mud-ink); border: 1px solid var(--soil-mid); border-radius: 3px;
|
|
607
|
-
padding: .32rem .
|
|
487
|
+
padding: .32rem .55rem; box-shadow: 0 1px 0 rgba(0, 0, 0, .12); text-decoration: none; font-size: .82rem; line-height: 1.2;
|
|
608
488
|
}
|
|
609
|
-
.mud-topbar-
|
|
610
|
-
.mud-topbar-help { padding: .32rem .55rem; line-height: 1.2; }
|
|
611
|
-
/* the pill wears the deck's lit-readout idiom: this is the one word that
|
|
612
|
-
says whether the burrow is shared, and it reads like an instrument. */
|
|
613
|
-
.mud-topbar-actions .state-pill { background: var(--lem-readout-bg); color: var(--lem-readout); border: 1px solid #000; border-radius: 2px; padding: .3rem .6rem; cursor: pointer; }
|
|
489
|
+
.mud-topbar-help:hover { background: var(--lem-face-hi); border-color: var(--burrow-glow); }
|
|
614
490
|
h2 { font-family: ${DISPLAY_STACK}; font-size: 1rem; margin: 0; text-transform: capitalize; }
|
|
615
491
|
h3 { font-family: ${MONO_STACK}; font-size: .58rem; margin: 0 0 .3rem; text-transform: uppercase; letter-spacing: .12em; color: var(--soil-mid); }
|
|
616
492
|
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
@@ -1031,8 +907,7 @@ const MUD_STYLE = `
|
|
|
1031
907
|
.deck button:hover:not(:disabled), .pane-controls button:hover:not(:disabled) {
|
|
1032
908
|
background: var(--lem-face-hi); border-color: var(--burrow-glow);
|
|
1033
909
|
}
|
|
1034
|
-
.deck button:active:not(:disabled), .pane-controls button:active:not(:disabled)
|
|
1035
|
-
.mud-topbar-actions button:active:not(:disabled) {
|
|
910
|
+
.deck button:active:not(:disabled), .pane-controls button:active:not(:disabled) {
|
|
1036
911
|
background: var(--lem-face-lo); box-shadow: inset 0 1px 2px rgba(0,0,0,.25);
|
|
1037
912
|
}
|
|
1038
913
|
.deck-play { background: var(--lem-go) !important; color: var(--parchment) !important; border-color: var(--lem-go) !important; }
|
|
@@ -1043,60 +918,6 @@ const MUD_STYLE = `
|
|
|
1043
918
|
.deck-info-btn { background: var(--lem-face); color: var(--mud-ink); border: 1px solid var(--soil-mid); }
|
|
1044
919
|
.deck-info-btn:hover, .deck-info-btn[aria-expanded="true"] { background: var(--lem-face-hi); border-color: var(--burrow-glow); color: var(--mud-ink); }
|
|
1045
920
|
#editModeBtn[aria-pressed="true"] { background: var(--burrow-glow); border-color: var(--burrow-glow); color: var(--mud-ink); }
|
|
1046
|
-
|
|
1047
|
-
/* ---- whether the burrow is shared, said in one word on the panel ---- */
|
|
1048
|
-
.state-pill { display: inline-flex; align-items: center; gap: .34rem;
|
|
1049
|
-
font-family: ${MONO_STACK}; font-size: .68rem; text-transform: uppercase; letter-spacing: .08em; }
|
|
1050
|
-
.state-dot { width: .46rem; height: .46rem; border-radius: 50%; background: var(--lem-face-lo); flex: 0 0 auto; }
|
|
1051
|
-
.state-pill[data-tone="waiting"] .state-dot { background: #E0912A; }
|
|
1052
|
-
.state-pill[data-tone="working"] .state-dot { background: #E0912A; }
|
|
1053
|
-
.state-pill[data-tone="live"] .state-dot { background: var(--lem-go); }
|
|
1054
|
-
.state-pill[data-tone="failed"] .state-dot { background: var(--lem-alert); }
|
|
1055
|
-
|
|
1056
|
-
/* ---- a wave, and the node a character is played from ---- */
|
|
1057
|
-
.pill-strip { display: flex; align-items: flex-start; gap: .22rem; min-width: 0; }
|
|
1058
|
-
.pill-strip .chatpills { flex: 1 1 auto; min-width: 0; }
|
|
1059
|
-
.wave-btn { flex: 0 0 auto; }
|
|
1060
|
-
.sprite-card { position: relative; }
|
|
1061
|
-
.wave-hand { position: absolute; top: -0.55rem; right: -0.35rem; font-size: 1.1rem; line-height: 1;
|
|
1062
|
-
transform-origin: 70% 80%; animation: hand-wave .8s ease-in-out infinite; pointer-events: none; }
|
|
1063
|
-
@keyframes hand-wave { 0%, 100% { transform: rotate(-14deg); } 50% { transform: rotate(20deg); } }
|
|
1064
|
-
.sprite-node {
|
|
1065
|
-
margin-top: 1px; max-width: 100%; box-sizing: border-box;
|
|
1066
|
-
font-family: ${MONO_STACK}; font-size: .34rem; line-height: 1.3; letter-spacing: .01em; text-align: center;
|
|
1067
|
-
color: var(--soil-mid); background: rgba(239,230,216,.7); border-radius: 2px; padding: 0 .2rem;
|
|
1068
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1069
|
-
}
|
|
1070
|
-
.pane-node { font-family: ${MONO_STACK}; font-size: .56rem; color: var(--soil-mid); opacity: .85; }
|
|
1071
|
-
.mud-window.remote .pane-controls button { display: none; }
|
|
1072
|
-
.mud-window.remote .room-view { box-shadow: inset 0 0 0 2px rgba(60,60,180,.35); }
|
|
1073
|
-
|
|
1074
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1075
|
-
.wave-hand { animation: none; }
|
|
1076
|
-
}
|
|
1077
|
-
`;
|
|
1078
|
-
|
|
1079
|
-
// The shared overlay, re-pointed at the burrow's own palette: soil-deep card,
|
|
1080
|
-
// parchment ink, the burrow-glow as the working accent, moss as the live one.
|
|
1081
|
-
// Only variables change — every rule stays the component's.
|
|
1082
|
-
const MUD_SHARE_SKIN = `
|
|
1083
|
-
.shareOverlay {
|
|
1084
|
-
--so-scrim: rgba(10, 6, 2, .74);
|
|
1085
|
-
--so-card: var(--soil-deep);
|
|
1086
|
-
--so-ink: var(--parchment);
|
|
1087
|
-
--so-muted: var(--chalk);
|
|
1088
|
-
--so-line: var(--soil-light);
|
|
1089
|
-
--so-accent: var(--burrow-glow);
|
|
1090
|
-
--so-good: #9DB36A;
|
|
1091
|
-
--so-warn: var(--burrow-glow);
|
|
1092
|
-
--so-alert: #FF9C82;
|
|
1093
|
-
--so-good-soft: rgba(157, 179, 106, .18);
|
|
1094
|
-
--so-accent-soft: rgba(232, 163, 61, .14);
|
|
1095
|
-
--so-alert-soft: rgba(255, 156, 130, .14);
|
|
1096
|
-
--so-display: ${DISPLAY_STACK};
|
|
1097
|
-
--so-body: ${SANS_STACK};
|
|
1098
|
-
--so-mono: ${MONO_STACK};
|
|
1099
|
-
}
|
|
1100
921
|
`;
|
|
1101
922
|
|
|
1102
923
|
/** The inlined page script, as a plain function body handed to
|
|
@@ -1142,27 +963,6 @@ function pageScript() {
|
|
|
1142
963
|
const allRoomIds = ${allRoomIds.toString()};
|
|
1143
964
|
const renderMudEditorText = ${renderMudEditorText.toString()};
|
|
1144
965
|
const wordBeforeCursor = ${wordBeforeCursor.toString()};
|
|
1145
|
-
const provenanceStamps = ${provenanceStamps.toString()};
|
|
1146
|
-
const peerTerm = ${peerTerm.toString()};
|
|
1147
|
-
const claimantOf = ${claimantOf.toString()};
|
|
1148
|
-
const wavingCharacters = ${wavingCharacters.toString()};
|
|
1149
|
-
const nodeNameFor = ${nodeNameFor.toString()};
|
|
1150
|
-
const offerBlobIn = ${offerBlobIn.toString()};
|
|
1151
|
-
const wireStateLabel = ${wireStateLabel.toString()};
|
|
1152
|
-
const tapeRowFor = ${tapeRowFor.toString()};
|
|
1153
|
-
const tapeClock = ${tapeClock.toString()};
|
|
1154
|
-
const nodeRowsFor = ${nodeRowsFor.toString()};
|
|
1155
|
-
const nodeInitials = ${nodeInitials.toString()};
|
|
1156
|
-
const inviteLinkFor = ${inviteLinkFor.toString()};
|
|
1157
|
-
const inviteParamsFrom = ${inviteParamsFrom.toString()};
|
|
1158
|
-
const shareStepStates = ${shareStepStates.toString()};
|
|
1159
|
-
const activeWaves = ${activeWaves.toString()};
|
|
1160
|
-
const shareMessageFor = ${shareMessageFor.toString()};
|
|
1161
|
-
const replyMessageFor = ${replyMessageFor.toString()};
|
|
1162
|
-
const whatsAppShareUrl = ${whatsAppShareUrl.toString()};
|
|
1163
|
-
const isProbablyMobile = ${isProbablyMobile.toString()};
|
|
1164
|
-
const copyText = ${copyTextToClipboard.toString()};
|
|
1165
|
-
const flashTip = ${flashCopyTip.toString()};
|
|
1166
966
|
|
|
1167
967
|
const el = (id) => document.getElementById(id);
|
|
1168
968
|
// Which of the shipped burrows is loaded. Every read of the world — the
|
|
@@ -1214,15 +1014,6 @@ function pageScript() {
|
|
|
1214
1014
|
const liveWait = function () { return wait(delayMs); };
|
|
1215
1015
|
|
|
1216
1016
|
let freshlyDugRoom = null;
|
|
1217
|
-
// Which animals another node has claimed, character -> that node's name.
|
|
1218
|
-
// Refreshed from the store on every redraw, which is also every turn, so a
|
|
1219
|
-
// claim that arrives from a peer takes effect on the next tick rather than
|
|
1220
|
-
// being cached until something else happens to look.
|
|
1221
|
-
let claimedElsewhere = {};
|
|
1222
|
-
// Whoever is waving right now, and the timer that lets a wave stop being
|
|
1223
|
-
// current. A wave is never retracted — it just stops being recent.
|
|
1224
|
-
let wavingNow = [];
|
|
1225
|
-
let waveRedrawTimer = null;
|
|
1226
1017
|
// The last omniscient read, kept so the pounce can draw the predator's own
|
|
1227
1018
|
// den after the engine has already moved the character out of every room.
|
|
1228
1019
|
let lastSnapshot = null;
|
|
@@ -1258,15 +1049,10 @@ function pageScript() {
|
|
|
1258
1049
|
const bootAt = bootSeq;
|
|
1259
1050
|
return serializeTick(async function () {
|
|
1260
1051
|
if (bootAt !== bootSeq || finished[character]) return { outOfPlay: true };
|
|
1261
|
-
// An animal another node claimed is played there, not here. Its moves
|
|
1262
|
-
// still arrive as facts and it still draws, talks and can be waved at —
|
|
1263
|
-
// this page just never takes its turn for it.
|
|
1264
|
-
if (claimedElsewhere[character]) return { playedElsewhere: true };
|
|
1265
1052
|
globalTurn += 1;
|
|
1266
1053
|
const result = await session.windows[character].autoplayTick(globalTurn);
|
|
1267
1054
|
if (!result.outOfPlay) turnsTaken[character] = (turnsTaken[character] || 0) + 1;
|
|
1268
1055
|
afterEngineTurn(character, result);
|
|
1269
|
-
await broadcastLocalChange();
|
|
1270
1056
|
return result;
|
|
1271
1057
|
});
|
|
1272
1058
|
}
|
|
@@ -1400,42 +1186,13 @@ function pageScript() {
|
|
|
1400
1186
|
|
|
1401
1187
|
function sendCommand(character, line) {
|
|
1402
1188
|
appendChat(character, "u", line);
|
|
1403
|
-
|
|
1404
|
-
if (owner) {
|
|
1405
|
-
const refusal = character + " is played from " + owner + ". You can watch it and wave at it, but its turns are theirs.";
|
|
1406
|
-
appendChat(character, "a", refusal);
|
|
1407
|
-
return Promise.resolve({ answer: refusal, end: false });
|
|
1408
|
-
}
|
|
1409
|
-
// A wave is a gesture, not a world command — the engine has no verb for it
|
|
1410
|
-
// and would answer with an honest miss. Both ways in (this typed line and
|
|
1411
|
-
// the pane's own hand button) write the same fact.
|
|
1412
|
-
if (/^\\/?wave$/i.test(line.trim())) {
|
|
1413
|
-
return waveAs(character).then(function (here) {
|
|
1414
|
-
appendChat(character, "a", here
|
|
1415
|
-
? "you wave. Everyone in the " + here + " sees it."
|
|
1416
|
-
: character + " is standing nowhere, so there is nobody to wave at.");
|
|
1417
|
-
return { answer: "", end: false };
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
return serializeTick(function () { return tmct.turn(line, { as: character }); }).then(async function (res) {
|
|
1189
|
+
return serializeTick(function () { return tmct.turn(line, { as: character }); }).then(function (res) {
|
|
1421
1190
|
appendChat(character, "a", res.answer);
|
|
1422
|
-
await broadcastLocalChange();
|
|
1423
1191
|
renderSoon();
|
|
1424
1192
|
return res;
|
|
1425
1193
|
});
|
|
1426
1194
|
}
|
|
1427
1195
|
|
|
1428
|
-
// A wave is an ordinary add-only fact in the shared world, so it needs no
|
|
1429
|
-
// network to work: one browser renders it exactly the way four do, and the
|
|
1430
|
-
// room broadcast below is the only part sharing adds.
|
|
1431
|
-
async function waveAs(character) {
|
|
1432
|
-
if (!session || finished[character]) return null;
|
|
1433
|
-
const here = await serializeTick(function () { return session.wave(character); });
|
|
1434
|
-
if (here) await broadcastLocalChange();
|
|
1435
|
-
renderSoon();
|
|
1436
|
-
return here;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
1196
|
// Wired as the pane is built. The stage is rebuilt on every boot, so a
|
|
1440
1197
|
// pane's listeners live and die with it; each one still reads its slot's
|
|
1441
1198
|
// character at click time, because the cast is bound after the markup.
|
|
@@ -1460,10 +1217,6 @@ function pageScript() {
|
|
|
1460
1217
|
const character = characterInSlot(slot);
|
|
1461
1218
|
if (character) ensureTicker(character).stepOnce();
|
|
1462
1219
|
});
|
|
1463
|
-
el(w + "-wave").addEventListener("click", function () {
|
|
1464
|
-
const character = characterInSlot(slot);
|
|
1465
|
-
if (character) sendCommand(character, "wave");
|
|
1466
|
-
});
|
|
1467
1220
|
|
|
1468
1221
|
const log = el(w + "-chatlog");
|
|
1469
1222
|
log.addEventListener("click", function (e) {
|
|
@@ -1602,33 +1355,18 @@ function pageScript() {
|
|
|
1602
1355
|
|
|
1603
1356
|
// A sprite card: adventure.html's own frame-plus-nameplate, carrying the one
|
|
1604
1357
|
// command this room currently grants on its subject (none, for a thing the
|
|
1605
|
-
// room offers nothing on) and the subject's own mass under the name.
|
|
1606
|
-
|
|
1607
|
-
// character waving right now, and the node another peer plays it from — one
|
|
1608
|
-
// more line under the name, in the same font, never a second label system.
|
|
1609
|
-
function spriteCardHtml(subject, label, inner, command, extraClass, mass, extra) {
|
|
1610
|
-
const opts = extra || {};
|
|
1358
|
+
// room offers nothing on) and the subject's own mass under the name.
|
|
1359
|
+
function spriteCardHtml(subject, label, inner, command, extraClass, mass) {
|
|
1611
1360
|
const action = command
|
|
1612
1361
|
? ' actionable" data-command="' + esc(command) + '" role="button" tabindex="0" title="' + esc(command) + '"'
|
|
1613
1362
|
: '" title="' + esc(label) + '"';
|
|
1614
|
-
return '<div class="sprite-card ' + (extraClass || "") +
|
|
1363
|
+
return '<div class="sprite-card ' + (extraClass || "") + action
|
|
1615
1364
|
+ ' data-subject="' + esc(subject) + '">'
|
|
1616
|
-
+ (opts.waving ? '<div class="wave-hand" aria-hidden="true">\\u{1F44B}</div>' : "")
|
|
1617
1365
|
+ inner + '<div class="sprite-label">' + esc(label) + "</div>"
|
|
1618
|
-
+ (opts.node ? '<div class="sprite-node">via ' + esc(opts.node) + "</div>" : "")
|
|
1619
1366
|
+ (mass === null || mass === undefined ? "" : '<div class="sprite-mass">mass ' + esc(mass) + "</div>")
|
|
1620
1367
|
+ "</div>";
|
|
1621
1368
|
}
|
|
1622
1369
|
|
|
1623
|
-
// A character another node plays carries that node's name; one this node
|
|
1624
|
-
// plays, or one nobody has claimed, carries nothing extra.
|
|
1625
|
-
function nodeLabelFor(character) {
|
|
1626
|
-
return claimedElsewhere[character] || null;
|
|
1627
|
-
}
|
|
1628
|
-
function isWavingNow(character) {
|
|
1629
|
-
return wavingNow.indexOf(character) !== -1;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
1370
|
function renderRoomView(character, rows, state, here, roomIds, roomMates) {
|
|
1633
1371
|
const w = paneIdFor(character);
|
|
1634
1372
|
drawRoomBackdrop(el(w + "-canvas"), roomKindForRoom(rows, here), here);
|
|
@@ -1638,14 +1376,12 @@ function pageScript() {
|
|
|
1638
1376
|
character, character,
|
|
1639
1377
|
'<div class="sprite">' + spriteSvgFor(speciesOfCharacter(character), rows, character) + "</div>",
|
|
1640
1378
|
null, "self", massOf(state, character),
|
|
1641
|
-
{ waving: isWavingNow(character), node: nodeLabelFor(character) },
|
|
1642
1379
|
);
|
|
1643
1380
|
|
|
1644
1381
|
const others = roomMates.map(function (mate) {
|
|
1645
1382
|
return spriteCardHtml(mate, mate,
|
|
1646
1383
|
'<div class="sprite">' + spriteSvgFor(speciesOfCharacter(mate), rows, mate) + "</div>",
|
|
1647
|
-
commands[mate], "", massOf(state, mate)
|
|
1648
|
-
{ waving: isWavingNow(mate), node: nodeLabelFor(mate) });
|
|
1384
|
+
commands[mate], "", massOf(state, mate));
|
|
1649
1385
|
});
|
|
1650
1386
|
const wall = [];
|
|
1651
1387
|
for (const obj of mudRoomSceneObjects(rows, state, here, character)) {
|
|
@@ -1876,23 +1612,6 @@ function pageScript() {
|
|
|
1876
1612
|
const snap = await session.snapshot();
|
|
1877
1613
|
lastSnapshot = snap;
|
|
1878
1614
|
const roomIds = allRoomIds(snap.rows);
|
|
1879
|
-
const nowMs = Date.now();
|
|
1880
|
-
wavingNow = wavingCharacters(snap.rows, nowMs);
|
|
1881
|
-
// Every claimed animal, not just the ones this page cast: a character
|
|
1882
|
-
// another node plays walks into this room as an ordinary room-mate, and
|
|
1883
|
-
// its label has to name where it is played from just the same.
|
|
1884
|
-
claimedElsewhere = {};
|
|
1885
|
-
if (myPeerId) {
|
|
1886
|
-
const claimed = {};
|
|
1887
|
-
for (const row of snap.rows) if (row.predicate === "mgx:playedBy") claimed[row.subject] = true;
|
|
1888
|
-
for (const character of Object.keys(claimed)) {
|
|
1889
|
-
const owner = claimantOf(snap.rows, character);
|
|
1890
|
-
if (owner === null || peerTerm(owner) === peerTerm(myPeerId)) continue;
|
|
1891
|
-
claimedElsewhere[character] = nodeNameFor(snap.rows, owner);
|
|
1892
|
-
const ticker = tickers[character];
|
|
1893
|
-
if (ticker) ticker.pause();
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
1615
|
for (const character of everyone()) {
|
|
1897
1616
|
const reason = window.tmct.page.outOfPlayReasonOf(snap.state, character);
|
|
1898
1617
|
if (reason) markOutOfPlay(character, reason, null);
|
|
@@ -1901,10 +1620,6 @@ function pageScript() {
|
|
|
1901
1620
|
for (const character of cast) {
|
|
1902
1621
|
const pane = paneIdFor(character);
|
|
1903
1622
|
el(pane + "-turn").textContent = "turn " + turnsFor(character);
|
|
1904
|
-
const owner = claimedElsewhere[character];
|
|
1905
|
-
el(pane).classList.toggle("remote", Boolean(owner));
|
|
1906
|
-
el(pane + "-node").hidden = !owner;
|
|
1907
|
-
el(pane + "-node").textContent = owner ? "played from " + owner : "";
|
|
1908
1623
|
if (finished[character]) continue;
|
|
1909
1624
|
const place = snap.state.placements.get(character);
|
|
1910
1625
|
const here = place ? place.object : null;
|
|
@@ -1917,12 +1632,6 @@ function pageScript() {
|
|
|
1917
1632
|
renderMinimap(character, snap.rows, snap.state, here);
|
|
1918
1633
|
}
|
|
1919
1634
|
freshlyDugRoom = null;
|
|
1920
|
-
// A wave carries no retraction, so nothing tells the page when one stops
|
|
1921
|
-
// being current — the recency window does, and only a redraw can notice it
|
|
1922
|
-
// has passed. One follow-up per second while any hand is up, and none at
|
|
1923
|
-
// all once they are all down.
|
|
1924
|
-
clearTimeout(waveRedrawTimer);
|
|
1925
|
-
if (wavingNow.length) waveRedrawTimer = setTimeout(renderSoon, 1000);
|
|
1926
1635
|
}
|
|
1927
1636
|
|
|
1928
1637
|
// ---- edit mode -----------------------------------------------------------
|
|
@@ -2067,7 +1776,6 @@ function pageScript() {
|
|
|
2067
1776
|
status.className = "edit-status pending";
|
|
2068
1777
|
status.textContent = "reading the burrow\\u2026";
|
|
2069
1778
|
const result = await serializeTick(function () { return session.applyEdit(el("editorText").value); });
|
|
2070
|
-
await broadcastLocalChange();
|
|
2071
1779
|
const snap = await session.snapshot();
|
|
2072
1780
|
allStoreRows = snap.rows;
|
|
2073
1781
|
editRows = worldOnlyRows(snap.rows);
|
|
@@ -2153,729 +1861,6 @@ function pageScript() {
|
|
|
2153
1861
|
await renderAll();
|
|
2154
1862
|
}
|
|
2155
1863
|
|
|
2156
|
-
// ---- the shared burrow: nodes, the wire, and the two-paste handshake -----
|
|
2157
|
-
// Every piece of networking arrives from ./vendor/p2p.js, the site's own
|
|
2158
|
-
// shared P2P asset, imported the first time somebody actually asks to share
|
|
2159
|
-
// rather than at boot. The room owns signaling, the mesh and the merge; this
|
|
2160
|
-
// block owns what a person sees and clicks, and nothing else.
|
|
2161
|
-
const P2P_ASSET = "./vendor/p2p.js";
|
|
2162
|
-
const NODE_NAME_KEY = "tmct.mud.nodeName";
|
|
2163
|
-
// Not the IndexedDB store beside it: that record is stamped with the site
|
|
2164
|
-
// version and the seed, and is dropped whenever either moves. A node id has
|
|
2165
|
-
// to outlive a deploy, because peers have already keyed this node's facts
|
|
2166
|
-
// on it.
|
|
2167
|
-
const NODE_ID_KEY = "tmct.mud.nodeId";
|
|
2168
|
-
const invite = inviteParamsFrom(window.location.search);
|
|
2169
|
-
|
|
2170
|
-
let p2p = null;
|
|
2171
|
-
let p2pLoad = null;
|
|
2172
|
-
let room = null;
|
|
2173
|
-
let myPeerId = null;
|
|
2174
|
-
let myNodeId = "";
|
|
2175
|
-
let myDisplayName = "";
|
|
2176
|
-
let worldId = "";
|
|
2177
|
-
let worldName = "";
|
|
2178
|
-
let nodeClockTimer = null;
|
|
2179
|
-
let mintedBlob = "";
|
|
2180
|
-
let nodeWaveTimer = null;
|
|
2181
|
-
let channelCount = 0;
|
|
2182
|
-
|
|
2183
|
-
// Which seat this page occupies in the handshake — "idle" until something
|
|
2184
|
-
// happens, "sponsor" once an invite is minted here, "joiner" once an invite
|
|
2185
|
-
// is opened here. The overlay's CSS reads it to decide which calls to
|
|
2186
|
-
// action stand at each step; entry distinguishes an invite that arrived as
|
|
2187
|
-
// a link (the hero card owns the reply) from one pasted in as text.
|
|
2188
|
-
function setOverlayRole(role, entry) {
|
|
2189
|
-
const overlay = el("netPanel");
|
|
2190
|
-
overlay.dataset.role = role;
|
|
2191
|
-
overlay.dataset.entry = entry || "";
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
function loadP2p() {
|
|
2195
|
-
if (!p2pLoad) {
|
|
2196
|
-
p2pLoad = import(P2P_ASSET).then(function (mod) { p2p = mod; return mod; });
|
|
2197
|
-
p2pLoad.catch(function (err) {
|
|
2198
|
-
noteTape("note", "fault", "networking unavailable", err && err.message ? err.message : String(err));
|
|
2199
|
-
});
|
|
2200
|
-
}
|
|
2201
|
-
return p2pLoad;
|
|
2202
|
-
}
|
|
2203
|
-
window.tmctP2pLoad = loadP2p;
|
|
2204
|
-
|
|
2205
|
-
function readStoredNodeName() {
|
|
2206
|
-
try { return localStorage.getItem(NODE_NAME_KEY) || ""; } catch { return ""; }
|
|
2207
|
-
}
|
|
2208
|
-
function writeStoredNodeName(name) {
|
|
2209
|
-
try { localStorage.setItem(NODE_NAME_KEY, name); } catch { /* private mode — the name still holds for this visit */ }
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
async function ensureIdentity() {
|
|
2213
|
-
const mod = await loadP2p();
|
|
2214
|
-
if (!myPeerId) myPeerId = mod.generatePeerId();
|
|
2215
|
-
if (!myNodeId) {
|
|
2216
|
-
try { myNodeId = localStorage.getItem(NODE_ID_KEY) || ""; } catch { myNodeId = ""; }
|
|
2217
|
-
if (!myNodeId) {
|
|
2218
|
-
myNodeId = mod.generateNodeId();
|
|
2219
|
-
try { localStorage.setItem(NODE_ID_KEY, myNodeId); } catch { /* private mode — this visit still has an id, it just won't outlive the tab */ }
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
if (!myDisplayName) {
|
|
2223
|
-
myDisplayName = readStoredNodeName() || mod.generateDisplayName();
|
|
2224
|
-
el("nodeNameInput").value = myDisplayName;
|
|
2225
|
-
}
|
|
2226
|
-
if (!worldId) worldId = invite && invite.world ? invite.world : mod.generateWorldId();
|
|
2227
|
-
if (!worldName) {
|
|
2228
|
-
worldName = invite && invite.worldName ? invite.worldName : mod.generateDisplayName();
|
|
2229
|
-
el("worldNameInput").value = worldName;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
// The one place a transport gets made, which makes it the one place every
|
|
2234
|
-
// message crossing one can be seen. The room asks for transports through
|
|
2235
|
-
// this factory and never learns it is being watched.
|
|
2236
|
-
function instrumentedTransport() {
|
|
2237
|
-
const transport = p2p.createTransport();
|
|
2238
|
-
const channel = "ch" + (++channelCount);
|
|
2239
|
-
transport.onMessage(function (message) { noteWire("in", message, channel); });
|
|
2240
|
-
transport.onOpen(function () { noteTape("note", "link", "channel open", channel); });
|
|
2241
|
-
transport.onClose(function () { noteTape("note", "link", "channel closed", channel); });
|
|
2242
|
-
return {
|
|
2243
|
-
createOffer: function () {
|
|
2244
|
-
return transport.createOffer().then(function (sdp) { noteTape("note", "signal", "offer minted", channel); return sdp; });
|
|
2245
|
-
},
|
|
2246
|
-
createAnswerFor: function (offerSdp) {
|
|
2247
|
-
return transport.createAnswerFor(offerSdp).then(function (sdp) { noteTape("note", "signal", "answer minted", channel); return sdp; });
|
|
2248
|
-
},
|
|
2249
|
-
completeWithAnswer: function (answerSdp) {
|
|
2250
|
-
noteTape("note", "signal", "answer accepted", channel);
|
|
2251
|
-
return transport.completeWithAnswer(answerSdp);
|
|
2252
|
-
},
|
|
2253
|
-
send: function (message) { transport.send(message); noteWire("out", message, channel); },
|
|
2254
|
-
onMessage: function (fn) { transport.onMessage(fn); },
|
|
2255
|
-
onOpen: function (fn) { transport.onOpen(fn); },
|
|
2256
|
-
onClose: function (fn) { transport.onClose(fn); },
|
|
2257
|
-
close: function () { transport.close(); },
|
|
2258
|
-
get connectionState() { return transport.connectionState; },
|
|
2259
|
-
};
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
async function ensureRoom() {
|
|
2263
|
-
if (room) return room;
|
|
2264
|
-
const mod = await loadP2p();
|
|
2265
|
-
await ensureIdentity();
|
|
2266
|
-
if (!session && bootRun) await bootRun;
|
|
2267
|
-
if (!session) throw new Error("the burrow hasn't finished opening");
|
|
2268
|
-
// The one place this page decides what is worth replicating: whatever the
|
|
2269
|
-
// engine calls live world state, plus the P2P layer's own four predicates.
|
|
2270
|
-
// The check is handed over rather than imported, so the sync filter never
|
|
2271
|
-
// learns the world engine's private predicate names.
|
|
2272
|
-
const extraPredicates = (window.tmct.page.P2P_PREDICATES || []).slice();
|
|
2273
|
-
const isState = window.tmct.page.isMudStatePredicate;
|
|
2274
|
-
room = mod.createP2pRoom({
|
|
2275
|
-
memoryDir: session.memoryDir,
|
|
2276
|
-
myPeerId: myPeerId,
|
|
2277
|
-
myDisplayName: myDisplayName,
|
|
2278
|
-
myNodeId: myNodeId,
|
|
2279
|
-
worldId: worldId,
|
|
2280
|
-
worldName: worldName,
|
|
2281
|
-
transportFactory: instrumentedTransport,
|
|
2282
|
-
syncableFacts: function (rows) { return mod.mudSyncableFacts(rows, isState, extraPredicates); },
|
|
2283
|
-
});
|
|
2284
|
-
room.onStateChanged(function (state) {
|
|
2285
|
-
noteTape("note", state === "failed" ? "fault" : "state", "state " + state, "");
|
|
2286
|
-
// The overlay exists to make one connection. The moment the channel is
|
|
2287
|
-
// open its work is done — the lights come back up, and the pill in the
|
|
2288
|
-
// header is the way back in.
|
|
2289
|
-
if (state === "connected") {
|
|
2290
|
-
el("joinCard").hidden = true;
|
|
2291
|
-
closeNetPanel();
|
|
2292
|
-
flashTip(el("statePill"), "connected \\u2014 you're digging together");
|
|
2293
|
-
}
|
|
2294
|
-
renderWire();
|
|
2295
|
-
});
|
|
2296
|
-
room.onPeersChanged(function () { renderNodes(); renderWire(); });
|
|
2297
|
-
room.onFactsChanged(function (payload) {
|
|
2298
|
-
noteTape("note", "facts", "merged", payload.merged + (payload.merged === 1 ? " fact" : " facts"));
|
|
2299
|
-
renderNodes();
|
|
2300
|
-
renderSoon();
|
|
2301
|
-
});
|
|
2302
|
-
await room.start();
|
|
2303
|
-
// The burrow's name is written into the graph the moment the room starts,
|
|
2304
|
-
// and this version retracts nothing — so the field stops taking edits.
|
|
2305
|
-
el("worldNameInput").readOnly = true;
|
|
2306
|
-
window.tmctP2pRoom = room;
|
|
2307
|
-
noteTape("note", "link", "world " + worldName, myDisplayName);
|
|
2308
|
-
await claimMyAnimals();
|
|
2309
|
-
renderWire();
|
|
2310
|
-
renderNodes();
|
|
2311
|
-
if (!nodeClockTimer) nodeClockTimer = setInterval(renderNodes, 10000);
|
|
2312
|
-
return room;
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
// Every animal this page drives is claimed the moment the burrow is shared,
|
|
2316
|
-
// panes and npcs alike: two peers each running scripted turns for one mole
|
|
2317
|
-
// would be two hands on one lemming. The claim is add-only and the oldest
|
|
2318
|
-
// wins, so a page that loses one simply stops driving that animal and
|
|
2319
|
-
// watches somebody else play it instead.
|
|
2320
|
-
async function claimMyAnimals() {
|
|
2321
|
-
if (!session || !room) return;
|
|
2322
|
-
await serializeTick(function () { return session.claimCharacters(everyone(), myPeerId); });
|
|
2323
|
-
await broadcastLocalChange();
|
|
2324
|
-
renderSoon();
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
// The room diffs the store against what it last saw and broadcasts whatever
|
|
2328
|
-
// changed. The page calls it after every local turn, wave, claim and edit —
|
|
2329
|
-
// a broadcast that fails is a dead channel, reported by its own close
|
|
2330
|
-
// handler, and must never fail the turn that happened to carry it.
|
|
2331
|
-
function broadcastLocalChange() {
|
|
2332
|
-
if (!room) return Promise.resolve({ broadcast: 0 });
|
|
2333
|
-
return room.afterLocalChange().catch(function () { return { broadcast: 0 }; });
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
function dropRoom(note) {
|
|
2337
|
-
if (!room) return;
|
|
2338
|
-
room.close();
|
|
2339
|
-
room = null;
|
|
2340
|
-
window.tmctP2pRoom = null;
|
|
2341
|
-
clearInterval(nodeClockTimer);
|
|
2342
|
-
nodeClockTimer = null;
|
|
2343
|
-
el("shareLink").value = "";
|
|
2344
|
-
el("replyOut").value = "";
|
|
2345
|
-
el("worldNameInput").readOnly = false;
|
|
2346
|
-
mintedBlob = "";
|
|
2347
|
-
setOverlayRole("idle", "");
|
|
2348
|
-
noteTape("note", "link", "world closed", worldName);
|
|
2349
|
-
renderWire();
|
|
2350
|
-
renderNodes();
|
|
2351
|
-
// Last, not first: renderWire restates the note for the state it has just
|
|
2352
|
-
// read, which for a room that is gone is the idle one ("this browser holds
|
|
2353
|
-
// the only copy"). WHY the room went is the thing worth saying, so it is
|
|
2354
|
-
// written over the top rather than under it.
|
|
2355
|
-
if (note) el("wireStateNote").textContent = note;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
// ---- the wire tape: every message, as it happens ------------------------
|
|
2359
|
-
const TAPE_CAP = 240;
|
|
2360
|
-
const TAPE_FAMILY_COLOR = {
|
|
2361
|
-
facts: "var(--so-good)",
|
|
2362
|
-
state: "var(--so-accent)",
|
|
2363
|
-
greeting: "var(--so-warn)",
|
|
2364
|
-
signal: "var(--so-warn)",
|
|
2365
|
-
fault: "var(--so-alert)",
|
|
2366
|
-
link: "var(--so-muted)",
|
|
2367
|
-
};
|
|
2368
|
-
const tapeCounts = new Map();
|
|
2369
|
-
let wireMessageCount = 0;
|
|
2370
|
-
|
|
2371
|
-
function pushTape(entry) {
|
|
2372
|
-
// The meter counts real wire messages only; the tape below shows those
|
|
2373
|
-
// plus the local notes (state changes, a channel opening) that explain
|
|
2374
|
-
// them. Folding notes into the counts would make "12 op" mean two things.
|
|
2375
|
-
if (entry.dir !== "note") {
|
|
2376
|
-
wireMessageCount += 1;
|
|
2377
|
-
tapeCounts.set(entry.type, (tapeCounts.get(entry.type) || 0) + 1);
|
|
2378
|
-
}
|
|
2379
|
-
const tapeEl = el("tape");
|
|
2380
|
-
el("tapeEmpty").hidden = true;
|
|
2381
|
-
const row = document.createElement("li");
|
|
2382
|
-
row.className = "tape-row";
|
|
2383
|
-
row.dataset.dir = entry.dir;
|
|
2384
|
-
row.dataset.family = entry.family;
|
|
2385
|
-
row.dataset.type = entry.type;
|
|
2386
|
-
const bar = document.createElement("i");
|
|
2387
|
-
bar.className = "tape-bar";
|
|
2388
|
-
const clock = document.createElement("span");
|
|
2389
|
-
clock.className = "tape-clock";
|
|
2390
|
-
clock.textContent = tapeClock();
|
|
2391
|
-
const type = document.createElement("span");
|
|
2392
|
-
type.className = "tape-type";
|
|
2393
|
-
type.textContent = entry.type;
|
|
2394
|
-
const detail = document.createElement("span");
|
|
2395
|
-
detail.className = "tape-detail";
|
|
2396
|
-
detail.textContent = entry.detail || "";
|
|
2397
|
-
row.appendChild(bar);
|
|
2398
|
-
row.appendChild(clock);
|
|
2399
|
-
row.appendChild(type);
|
|
2400
|
-
row.appendChild(detail);
|
|
2401
|
-
tapeEl.insertBefore(row, tapeEl.firstChild);
|
|
2402
|
-
while (tapeEl.childElementCount > TAPE_CAP) tapeEl.removeChild(tapeEl.lastElementChild);
|
|
2403
|
-
renderTapeMeter();
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
function noteWire(direction, message, channel) {
|
|
2407
|
-
const row = tapeRowFor(direction, message);
|
|
2408
|
-
pushTape({ dir: direction, family: row.family, type: row.type, detail: row.detail || channel });
|
|
2409
|
-
}
|
|
2410
|
-
function noteTape(dir, family, type, detail) {
|
|
2411
|
-
pushTape({ dir: dir, family: family, type: type, detail: detail });
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
function renderTapeMeter() {
|
|
2415
|
-
const tapeMeterEl = el("tapeMeter");
|
|
2416
|
-
el("tapeTotal").textContent = wireMessageCount + (wireMessageCount === 1 ? " message" : " messages");
|
|
2417
|
-
tapeMeterEl.textContent = "";
|
|
2418
|
-
const counted = [...tapeCounts.entries()].sort(function (a, b) { return b[1] - a[1]; });
|
|
2419
|
-
for (const pair of counted) {
|
|
2420
|
-
const item = document.createElement("span");
|
|
2421
|
-
item.className = "meter-item";
|
|
2422
|
-
item.dataset.type = pair[0];
|
|
2423
|
-
const bar = document.createElement("i");
|
|
2424
|
-
bar.className = "meter-bar";
|
|
2425
|
-
bar.style.background = TAPE_FAMILY_COLOR[tapeRowFor("in", { type: pair[0] }).family] || "var(--so-muted)";
|
|
2426
|
-
const label = document.createElement("span");
|
|
2427
|
-
label.textContent = pair[0];
|
|
2428
|
-
const count = document.createElement("span");
|
|
2429
|
-
count.className = "meter-n";
|
|
2430
|
-
count.textContent = String(pair[1]);
|
|
2431
|
-
item.appendChild(bar);
|
|
2432
|
-
item.appendChild(label);
|
|
2433
|
-
item.appendChild(count);
|
|
2434
|
-
tapeMeterEl.appendChild(item);
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
// The state machine is shared with chat.html and the words mostly are too.
|
|
2439
|
-
// Two of them are not: nothing is taught here and nothing is a node below,
|
|
2440
|
-
// so those two notes are said in this page's own terms rather than forking
|
|
2441
|
-
// the whole table for a burrow.
|
|
2442
|
-
const MUD_WIRE_NOTES = {
|
|
2443
|
-
idle: "this browser holds the only copy of this burrow.",
|
|
2444
|
-
connected: "what you dig from here reaches every node in the burrow, and theirs reaches you.",
|
|
2445
|
-
};
|
|
2446
|
-
|
|
2447
|
-
function renderWire() {
|
|
2448
|
-
const state = room ? room.state : "idle";
|
|
2449
|
-
const label = wireStateLabel(state);
|
|
2450
|
-
const note = MUD_WIRE_NOTES[state] || label.note;
|
|
2451
|
-
const wire = el("wireState");
|
|
2452
|
-
wire.dataset.tone = label.tone;
|
|
2453
|
-
el("wireStateWord").textContent = label.word;
|
|
2454
|
-
el("wireStateNote").textContent = note;
|
|
2455
|
-
const pill = el("statePill");
|
|
2456
|
-
pill.dataset.tone = label.tone;
|
|
2457
|
-
el("statePillWord").textContent = label.pill;
|
|
2458
|
-
pill.title = note;
|
|
2459
|
-
el("netPanel").dataset.tone = label.tone;
|
|
2460
|
-
renderSteps();
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
function renderSteps() {
|
|
2464
|
-
const states = shareStepStates({
|
|
2465
|
-
role: el("netPanel").dataset.role,
|
|
2466
|
-
state: room ? room.state : "idle",
|
|
2467
|
-
hasReply: Boolean(el("replyOut").value),
|
|
2468
|
-
});
|
|
2469
|
-
for (const step of states) {
|
|
2470
|
-
const item = el("step-" + step.key);
|
|
2471
|
-
if (item) item.dataset.status = step.status;
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
// The scene's two browser cards: this one wears the node's own name, the
|
|
2476
|
-
// far one wears the first peer's the moment there is one to name.
|
|
2477
|
-
function renderScene() {
|
|
2478
|
-
el("sceneYouName").textContent = myDisplayName || "you";
|
|
2479
|
-
const peers = room ? room.peers() : [];
|
|
2480
|
-
const first = peers.find(function (p) { return p.connected; }) || peers[0];
|
|
2481
|
-
el("sceneThemName").textContent = first ? room.displayNameFor(first.peerId) : "your friend";
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
function relativeWhen(at, nowMs) {
|
|
2485
|
-
if (at === null || at === undefined) return "\\u2014";
|
|
2486
|
-
const seconds = Math.max(0, Math.round((nowMs - at) / 1000));
|
|
2487
|
-
if (seconds < 5) return "now";
|
|
2488
|
-
if (seconds < 60) return seconds + "s";
|
|
2489
|
-
if (seconds < 3600) return Math.round(seconds / 60) + "m";
|
|
2490
|
-
if (seconds < 86400) return Math.round(seconds / 3600) + "h";
|
|
2491
|
-
return Math.round(seconds / 86400) + "d";
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
// A node is marked waving whether the wave came from one of its claimed
|
|
2495
|
-
// animals (a pane's own wave button) or from the node itself (the roster's
|
|
2496
|
-
// peer-scoped wave) — one hand, two ways to raise it.
|
|
2497
|
-
function wavingPeerTerms(nowMs) {
|
|
2498
|
-
const rows = room.factRows();
|
|
2499
|
-
const terms = new Set();
|
|
2500
|
-
for (const wave of activeWaves(rows, nowMs)) terms.add(wave.waver);
|
|
2501
|
-
for (const character of wavingCharacters(rows, nowMs)) {
|
|
2502
|
-
const owner = claimantOf(rows, character);
|
|
2503
|
-
if (owner !== null) terms.add(peerTerm(owner));
|
|
2504
|
-
}
|
|
2505
|
-
return terms;
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
function renderNodes() {
|
|
2509
|
-
const listEl = el("nodeList");
|
|
2510
|
-
if (!room || !p2p) {
|
|
2511
|
-
el("nodeCount").textContent = "";
|
|
2512
|
-
listEl.textContent = "";
|
|
2513
|
-
el("nodeEmpty").hidden = false;
|
|
2514
|
-
renderScene();
|
|
2515
|
-
return;
|
|
2516
|
-
}
|
|
2517
|
-
const rows = nodeRowsFor({
|
|
2518
|
-
peers: room.peers(),
|
|
2519
|
-
factRows: room.factRows(),
|
|
2520
|
-
myPeerId: myPeerId,
|
|
2521
|
-
myDisplayName: myDisplayName,
|
|
2522
|
-
nameFor: room.displayNameFor,
|
|
2523
|
-
latestTimestampOf: p2p.latestProvenanceTimestamp,
|
|
2524
|
-
});
|
|
2525
|
-
const nowMs = Date.now();
|
|
2526
|
-
const waving = wavingPeerTerms(nowMs);
|
|
2527
|
-
el("nodeCount").textContent = rows.length + (rows.length === 1 ? " node" : " nodes");
|
|
2528
|
-
listEl.textContent = "";
|
|
2529
|
-
for (const entry of rows) {
|
|
2530
|
-
const item = document.createElement("li");
|
|
2531
|
-
item.className = "node-row";
|
|
2532
|
-
item.dataset.self = String(entry.isSelf);
|
|
2533
|
-
item.dataset.away = String(!entry.connected);
|
|
2534
|
-
item.dataset.peer = entry.peerId;
|
|
2535
|
-
item.dataset.waving = String(waving.has(peerTerm(entry.peerId)));
|
|
2536
|
-
const avatar = document.createElement("span");
|
|
2537
|
-
avatar.className = "node-avatar";
|
|
2538
|
-
avatar.textContent = nodeInitials(entry.name);
|
|
2539
|
-
avatar.title = entry.connected
|
|
2540
|
-
? "connected"
|
|
2541
|
-
: "away \\u2014 closed the tab or dropped offline; everything it dug stays";
|
|
2542
|
-
const dot = document.createElement("i");
|
|
2543
|
-
dot.className = "node-dot";
|
|
2544
|
-
avatar.appendChild(dot);
|
|
2545
|
-
const hand = document.createElement("span");
|
|
2546
|
-
hand.className = "node-hand";
|
|
2547
|
-
hand.textContent = "\\u{1F44B}";
|
|
2548
|
-
avatar.appendChild(hand);
|
|
2549
|
-
const name = document.createElement("span");
|
|
2550
|
-
name.className = "node-name";
|
|
2551
|
-
name.textContent = entry.name;
|
|
2552
|
-
const when = document.createElement("span");
|
|
2553
|
-
when.className = "node-when";
|
|
2554
|
-
when.textContent = relativeWhen(entry.lastActiveAt, nowMs);
|
|
2555
|
-
const fact = document.createElement("span");
|
|
2556
|
-
fact.className = "node-fact";
|
|
2557
|
-
if (entry.lastFact) {
|
|
2558
|
-
const line = entry.lastFact.subject + " " + String(entry.lastFact.predicate || "").replace(/^[a-z0-9_-]+:/i, "") + " " + entry.lastFact.object;
|
|
2559
|
-
fact.textContent = line;
|
|
2560
|
-
fact.title = "last shared: " + line;
|
|
2561
|
-
} else {
|
|
2562
|
-
fact.textContent = "nothing shared yet";
|
|
2563
|
-
}
|
|
2564
|
-
const waveOne = document.createElement("button");
|
|
2565
|
-
waveOne.type = "button";
|
|
2566
|
-
waveOne.className = "node-wave-btn";
|
|
2567
|
-
waveOne.textContent = "\\u{1F44B}";
|
|
2568
|
-
waveOne.title = entry.isSelf ? "wave at everyone" : "wave at " + entry.name + " \\u2014 they see it as a fact arriving";
|
|
2569
|
-
waveOne.setAttribute("aria-label", waveOne.title);
|
|
2570
|
-
waveOne.addEventListener("click", function () { waveAtNode(entry.isSelf ? null : entry.peerId, waveOne); });
|
|
2571
|
-
item.appendChild(avatar);
|
|
2572
|
-
item.appendChild(name);
|
|
2573
|
-
item.appendChild(when);
|
|
2574
|
-
item.appendChild(waveOne);
|
|
2575
|
-
item.appendChild(fact);
|
|
2576
|
-
listEl.appendChild(item);
|
|
2577
|
-
}
|
|
2578
|
-
el("nodeEmpty").hidden = rows.length > 1;
|
|
2579
|
-
renderScene();
|
|
2580
|
-
clearTimeout(nodeWaveTimer);
|
|
2581
|
-
if (waving.size) nodeWaveTimer = setTimeout(renderNodes, 1000);
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
// A node's own wave, distinct from an animal's: the fact's subject is the
|
|
2585
|
-
// peer, and its object names the audience — everyone, or one node.
|
|
2586
|
-
async function waveAtNode(targetPeerId, anchor) {
|
|
2587
|
-
try {
|
|
2588
|
-
const active = await ensureRoom();
|
|
2589
|
-
await active.wave("peer:" + myPeerId, targetPeerId ? "peer:" + targetPeerId : null);
|
|
2590
|
-
const audience = targetPeerId ? (active.displayNameFor(targetPeerId) || "one node") : "everyone";
|
|
2591
|
-
renderNodes();
|
|
2592
|
-
if (anchor) flashTip(anchor, "you waved at " + audience);
|
|
2593
|
-
} catch (err) {
|
|
2594
|
-
el("wireStateNote").textContent = "couldn't wave (" + (err && err.message ? err.message : err) + ").";
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
const openNetPanel = function () {
|
|
2599
|
-
const overlay = el("netPanel");
|
|
2600
|
-
overlay.hidden = false;
|
|
2601
|
-
el("statePill").setAttribute("aria-expanded", "true");
|
|
2602
|
-
const card = overlay.querySelector(".so-card");
|
|
2603
|
-
if (card) card.focus({ preventScroll: true });
|
|
2604
|
-
};
|
|
2605
|
-
const closeNetPanel = function () {
|
|
2606
|
-
el("netPanel").hidden = true;
|
|
2607
|
-
el("statePill").setAttribute("aria-expanded", "false");
|
|
2608
|
-
};
|
|
2609
|
-
|
|
2610
|
-
function renderInviteShare() {
|
|
2611
|
-
const facts = room ? room.factRows().length : 0;
|
|
2612
|
-
const factsPart = facts ? " \\u00b7 " + facts + (facts === 1 ? " fact travels" : " facts travel") + " when they join" : "";
|
|
2613
|
-
el("inviteSummary").textContent = "invites one person into \\u201c" + worldName + "\\u201d" + factsPart;
|
|
2614
|
-
const message = shareMessageFor({ worldName: worldName, link: el("shareLink").value, thing: "burrow" });
|
|
2615
|
-
el("waShareBtn").href = whatsAppShareUrl(message);
|
|
2616
|
-
el("webShareBtn").hidden = !navigator.share;
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
function renderReplyShare() {
|
|
2620
|
-
const message = replyMessageFor({ worldName: worldName, blob: el("replyOut").value });
|
|
2621
|
-
el("replyWaBtn").href = whatsAppShareUrl(message);
|
|
2622
|
-
el("joinWaBtn").href = whatsAppShareUrl(message);
|
|
2623
|
-
el("replyShareBtn").hidden = !navigator.share;
|
|
2624
|
-
el("joinShareBtn").hidden = !navigator.share;
|
|
2625
|
-
}
|
|
2626
|
-
|
|
2627
|
-
async function mintInvite(anchor) {
|
|
2628
|
-
try {
|
|
2629
|
-
const active = await ensureRoom();
|
|
2630
|
-
const minted = await active.startSharing();
|
|
2631
|
-
mintedBlob = minted.blob;
|
|
2632
|
-
el("shareLink").value = inviteLinkFor(window.location.href, {
|
|
2633
|
-
blob: minted.blob, world: worldId, worldName: worldName,
|
|
2634
|
-
});
|
|
2635
|
-
setOverlayRole("sponsor", "");
|
|
2636
|
-
renderInviteShare();
|
|
2637
|
-
el("replyProblem").hidden = true;
|
|
2638
|
-
openNetPanel();
|
|
2639
|
-
renderWire();
|
|
2640
|
-
await copyText(el("shareLink").value);
|
|
2641
|
-
flashTip(anchor, "link copied \\u2014 send it to one person");
|
|
2642
|
-
} catch (err) {
|
|
2643
|
-
el("replyProblem").textContent = "couldn't create an invite (" + (err && err.message ? err.message : err) + ").";
|
|
2644
|
-
el("replyProblem").hidden = false;
|
|
2645
|
-
openNetPanel();
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
function wireNetPanel() {
|
|
2650
|
-
el("netPanelClose").addEventListener("click", closeNetPanel);
|
|
2651
|
-
el("netPanel").addEventListener("click", function (e) {
|
|
2652
|
-
if (e.target === el("netPanel")) closeNetPanel();
|
|
2653
|
-
});
|
|
2654
|
-
document.addEventListener("keydown", function (e) {
|
|
2655
|
-
if (e.key === "Escape" && !el("netPanel").hidden) closeNetPanel();
|
|
2656
|
-
});
|
|
2657
|
-
el("statePill").addEventListener("click", function () {
|
|
2658
|
-
if (el("netPanel").hidden) openNetPanel(); else closeNetPanel();
|
|
2659
|
-
});
|
|
2660
|
-
if (isProbablyMobile()) el("netPanel").classList.add("so-mobile");
|
|
2661
|
-
// The blobs are made to be copied whole — a click selects everything, so
|
|
2662
|
-
// a manual copy can never take half a code.
|
|
2663
|
-
for (const boxId of ["shareLink", "replyOut", "joinReply"]) {
|
|
2664
|
-
el(boxId).addEventListener("focus", function (e) { e.target.select(); });
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
el("shareBtn").addEventListener("click", async function () {
|
|
2668
|
-
const shareBtn = el("shareBtn");
|
|
2669
|
-
shareBtn.disabled = true;
|
|
2670
|
-
try {
|
|
2671
|
-
await mintInvite(shareBtn);
|
|
2672
|
-
} finally {
|
|
2673
|
-
shareBtn.disabled = false;
|
|
2674
|
-
}
|
|
2675
|
-
});
|
|
2676
|
-
for (const mintId of ["mintInviteBtn", "remintBtn"]) {
|
|
2677
|
-
el(mintId).addEventListener("click", async function () {
|
|
2678
|
-
const btn = el(mintId);
|
|
2679
|
-
btn.disabled = true;
|
|
2680
|
-
try {
|
|
2681
|
-
await mintInvite(btn);
|
|
2682
|
-
} finally {
|
|
2683
|
-
btn.disabled = false;
|
|
2684
|
-
}
|
|
2685
|
-
});
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
el("copyLinkBtn").addEventListener("click", async function () {
|
|
2689
|
-
await copyText(el("shareLink").value);
|
|
2690
|
-
flashTip(el("copyLinkBtn"), "link copied \\u2014 send it to one person");
|
|
2691
|
-
});
|
|
2692
|
-
el("copyCodeBtn").addEventListener("click", async function () {
|
|
2693
|
-
await copyText(mintedBlob);
|
|
2694
|
-
flashTip(el("copyCodeBtn"), "code copied \\u2014 they paste it under step 3 on their page");
|
|
2695
|
-
});
|
|
2696
|
-
el("webShareBtn").addEventListener("click", function () {
|
|
2697
|
-
navigator.share({
|
|
2698
|
-
title: "join \\u201c" + worldName + "\\u201d",
|
|
2699
|
-
text: shareMessageFor({ worldName: worldName, link: el("shareLink").value, thing: "burrow" }),
|
|
2700
|
-
}).catch(function () { /* the sheet was closed — the copy buttons still stand */ });
|
|
2701
|
-
});
|
|
2702
|
-
const shareReplyViaSheet = function () {
|
|
2703
|
-
navigator.share({
|
|
2704
|
-
title: "my reply to \\u201c" + worldName + "\\u201d",
|
|
2705
|
-
text: replyMessageFor({ worldName: worldName, blob: el("replyOut").value }),
|
|
2706
|
-
}).catch(function () { /* the sheet was closed — the copy buttons still stand */ });
|
|
2707
|
-
};
|
|
2708
|
-
el("replyShareBtn").addEventListener("click", shareReplyViaSheet);
|
|
2709
|
-
el("joinShareBtn").addEventListener("click", shareReplyViaSheet);
|
|
2710
|
-
el("waveAllBtn").addEventListener("click", function () { waveAtNode(null, el("waveAllBtn")); });
|
|
2711
|
-
|
|
2712
|
-
// The inviter's page has exactly one paste target, so there is no wrong box
|
|
2713
|
-
// to choose. A rejected paste keeps its text so the copy can be fixed.
|
|
2714
|
-
el("replyBtn").addEventListener("click", async function () {
|
|
2715
|
-
let active = room;
|
|
2716
|
-
if (!active) {
|
|
2717
|
-
try { active = await ensureRoom(); } catch { return; }
|
|
2718
|
-
}
|
|
2719
|
-
const outcome = await active.completeInvite(el("replyBox").value);
|
|
2720
|
-
if (outcome && outcome.error) {
|
|
2721
|
-
el("replyProblem").textContent = outcome.message;
|
|
2722
|
-
el("replyProblem").hidden = false;
|
|
2723
|
-
return;
|
|
2724
|
-
}
|
|
2725
|
-
el("replyProblem").hidden = true;
|
|
2726
|
-
el("replyBox").value = "";
|
|
2727
|
-
renderWire();
|
|
2728
|
-
});
|
|
2729
|
-
|
|
2730
|
-
el("copyReplyBtn").addEventListener("click", async function () {
|
|
2731
|
-
await copyText(el("replyOut").value);
|
|
2732
|
-
flashTip(el("copyReplyBtn"), "reply copied \\u2014 send it back the same way");
|
|
2733
|
-
});
|
|
2734
|
-
|
|
2735
|
-
// The link flow lands somebody on the join card before they have dug
|
|
2736
|
-
// anything. This is the other case: a burrow already open, an invite that
|
|
2737
|
-
// arrived in a message, and no reason to throw the burrow away to take it.
|
|
2738
|
-
el("joinOpenBtn").addEventListener("click", function () {
|
|
2739
|
-
openNetPanel();
|
|
2740
|
-
const box = el("inviteBox");
|
|
2741
|
-
if (box.offsetParent !== null) box.focus();
|
|
2742
|
-
});
|
|
2743
|
-
|
|
2744
|
-
el("inviteBtn").addEventListener("click", async function () {
|
|
2745
|
-
const problem = el("inviteProblem");
|
|
2746
|
-
const pasted = el("inviteBox").value.trim();
|
|
2747
|
-
if (!pasted) {
|
|
2748
|
-
problem.textContent = "paste the invite you were sent, then try again";
|
|
2749
|
-
problem.hidden = false;
|
|
2750
|
-
return;
|
|
2751
|
-
}
|
|
2752
|
-
if (room) {
|
|
2753
|
-
problem.textContent = "this burrow is already shared. Reset it first, then take the invite.";
|
|
2754
|
-
problem.hidden = false;
|
|
2755
|
-
return;
|
|
2756
|
-
}
|
|
2757
|
-
// A whole link and a bare blob are both what people actually paste, so
|
|
2758
|
-
// both are read. The blob's own envelope decides either way.
|
|
2759
|
-
const blob = offerBlobIn(pasted);
|
|
2760
|
-
const mod = await loadP2p();
|
|
2761
|
-
const decoded = mod.decodeInviteBlob(blob);
|
|
2762
|
-
if (decoded.error || decoded.value.kind !== "offer") {
|
|
2763
|
-
problem.textContent = decoded.error
|
|
2764
|
-
? "this invite looks cut short \\u2014 ask for it to be sent again"
|
|
2765
|
-
: "that's a reply, not an invite \\u2014 it belongs on the page that sent the invite";
|
|
2766
|
-
problem.hidden = false;
|
|
2767
|
-
return;
|
|
2768
|
-
}
|
|
2769
|
-
// The world id has to be the invite's before the room is opened: a room
|
|
2770
|
-
// that minted its own would refuse the invite as belonging elsewhere.
|
|
2771
|
-
await ensureIdentity();
|
|
2772
|
-
worldId = decoded.value.world || worldId;
|
|
2773
|
-
if (decoded.value.worldName) {
|
|
2774
|
-
worldName = decoded.value.worldName;
|
|
2775
|
-
el("worldNameInput").value = worldName;
|
|
2776
|
-
}
|
|
2777
|
-
try {
|
|
2778
|
-
const active = await ensureRoom();
|
|
2779
|
-
const outcome = await active.acceptInvite(blob);
|
|
2780
|
-
if (outcome && outcome.error) {
|
|
2781
|
-
problem.textContent = outcome.message;
|
|
2782
|
-
problem.hidden = false;
|
|
2783
|
-
return;
|
|
2784
|
-
}
|
|
2785
|
-
problem.hidden = true;
|
|
2786
|
-
el("inviteBox").value = "";
|
|
2787
|
-
el("replyOut").value = outcome.blob;
|
|
2788
|
-
setOverlayRole("joiner", "paste");
|
|
2789
|
-
renderReplyShare();
|
|
2790
|
-
renderWire();
|
|
2791
|
-
await copyText(outcome.blob);
|
|
2792
|
-
flashTip(el("copyReplyBtn"), "reply copied \\u2014 send it back the same way");
|
|
2793
|
-
} catch (err) {
|
|
2794
|
-
problem.textContent = "couldn't make a reply (" + (err && err.message ? err.message : err) + ").";
|
|
2795
|
-
problem.hidden = false;
|
|
2796
|
-
}
|
|
2797
|
-
});
|
|
2798
|
-
|
|
2799
|
-
el("nodeNameInput").addEventListener("change", function () {
|
|
2800
|
-
const name = el("nodeNameInput").value.trim();
|
|
2801
|
-
if (!name || name === myDisplayName) { el("nodeNameInput").value = myDisplayName; return; }
|
|
2802
|
-
myDisplayName = name;
|
|
2803
|
-
writeStoredNodeName(name);
|
|
2804
|
-
if (!room) { renderNodes(); return; }
|
|
2805
|
-
room.setMyDisplayName(name).then(renderNodes).catch(function () { /* the next broadcast carries it */ });
|
|
2806
|
-
});
|
|
2807
|
-
el("worldNameInput").addEventListener("change", function () {
|
|
2808
|
-
const input = el("worldNameInput");
|
|
2809
|
-
if (input.readOnly) { input.value = worldName; return; }
|
|
2810
|
-
const name = input.value.trim();
|
|
2811
|
-
if (name) worldName = name;
|
|
2812
|
-
input.value = worldName;
|
|
2813
|
-
});
|
|
2814
|
-
|
|
2815
|
-
el("joinBtn").addEventListener("click", async function () {
|
|
2816
|
-
const joinBtn = el("joinBtn");
|
|
2817
|
-
joinBtn.disabled = true;
|
|
2818
|
-
try {
|
|
2819
|
-
const active = await ensureRoom();
|
|
2820
|
-
const outcome = await active.acceptInvite(invite.offer);
|
|
2821
|
-
if (outcome && outcome.error) {
|
|
2822
|
-
el("joinProblem").textContent = outcome.message;
|
|
2823
|
-
el("joinProblem").hidden = false;
|
|
2824
|
-
joinBtn.disabled = false;
|
|
2825
|
-
joinBtn.textContent = "try again";
|
|
2826
|
-
return;
|
|
2827
|
-
}
|
|
2828
|
-
el("joinProblem").hidden = true;
|
|
2829
|
-
el("joinReply").value = outcome.blob;
|
|
2830
|
-
el("replyOut").value = outcome.blob;
|
|
2831
|
-
el("joinReplyWrap").hidden = false;
|
|
2832
|
-
joinBtn.textContent = "reply created";
|
|
2833
|
-
el("joinDismiss").textContent = "close this and start digging";
|
|
2834
|
-
renderReplyShare();
|
|
2835
|
-
renderWire();
|
|
2836
|
-
await copyText(outcome.blob);
|
|
2837
|
-
flashTip(el("joinCopyBtn"), "reply copied \\u2014 send it back the same way");
|
|
2838
|
-
} catch (err) {
|
|
2839
|
-
el("joinProblem").textContent = "couldn't make a reply (" + (err && err.message ? err.message : err) + ").";
|
|
2840
|
-
el("joinProblem").hidden = false;
|
|
2841
|
-
joinBtn.disabled = false;
|
|
2842
|
-
}
|
|
2843
|
-
});
|
|
2844
|
-
el("joinCopyBtn").addEventListener("click", async function () {
|
|
2845
|
-
await copyText(el("joinReply").value);
|
|
2846
|
-
flashTip(el("joinCopyBtn"), "reply copied \\u2014 send it back the same way");
|
|
2847
|
-
});
|
|
2848
|
-
el("joinDismiss").addEventListener("click", closeNetPanel);
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
// A link that lost part of itself on the way reads as a mangled link, never
|
|
2852
|
-
// as no invite at all.
|
|
2853
|
-
async function prepareJoinCard() {
|
|
2854
|
-
setOverlayRole("joiner", "link");
|
|
2855
|
-
el("joinCard").hidden = false;
|
|
2856
|
-
el("joinWorld").textContent = invite.worldName || "a shared burrow";
|
|
2857
|
-
renderSteps();
|
|
2858
|
-
openNetPanel();
|
|
2859
|
-
el("joinBtn").focus();
|
|
2860
|
-
const mod = await loadP2p();
|
|
2861
|
-
const decoded = mod.decodeInviteBlob(invite.offer);
|
|
2862
|
-
if (decoded.error || decoded.value.kind !== "offer") {
|
|
2863
|
-
el("joinBtn").hidden = true;
|
|
2864
|
-
el("joinEyebrow").textContent = "this link didn't arrive in one piece";
|
|
2865
|
-
el("joinWorld").textContent = invite.worldName || "an invitation";
|
|
2866
|
-
el("joinBody").textContent = decoded.error
|
|
2867
|
-
? "Part of the link was lost on the way here. Ask for it to be sent again, and check the whole thing travels."
|
|
2868
|
-
: "That link carries a reply rather than an invite. It belongs in the box on the page that sent the invite.";
|
|
2869
|
-
el("joinDismiss").textContent = "dig on your own instead";
|
|
2870
|
-
return;
|
|
2871
|
-
}
|
|
2872
|
-
if (decoded.value.world) invite.world = decoded.value.world;
|
|
2873
|
-
if (decoded.value.worldName) {
|
|
2874
|
-
invite.worldName = decoded.value.worldName;
|
|
2875
|
-
el("joinWorld").textContent = decoded.value.worldName;
|
|
2876
|
-
}
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
1864
|
// ---- the control deck ----------------------------------------------------
|
|
2880
1865
|
// The players slider runs on detents (1, 2, 4), so its own value is the
|
|
2881
1866
|
// index of the count, never the count.
|
|
@@ -2962,7 +1947,7 @@ function pageScript() {
|
|
|
2962
1947
|
const wasEditing = editing;
|
|
2963
1948
|
if (wasEditing) await exitEditMode();
|
|
2964
1949
|
scenarioIndex = picked;
|
|
2965
|
-
await boot(
|
|
1950
|
+
await boot();
|
|
2966
1951
|
if (wasEditing) await enterEditMode();
|
|
2967
1952
|
});
|
|
2968
1953
|
}
|
|
@@ -3020,31 +2005,13 @@ function pageScript() {
|
|
|
3020
2005
|
// session on the floor rather than binding it to the stage the newer boot
|
|
3021
2006
|
// has already drawn.
|
|
3022
2007
|
let bootSeq = 0;
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
bootRun = openWorld(note);
|
|
3026
|
-
return bootRun;
|
|
2008
|
+
function boot() {
|
|
2009
|
+
return openWorld();
|
|
3027
2010
|
}
|
|
3028
2011
|
|
|
3029
|
-
async function openWorld(
|
|
2012
|
+
async function openWorld() {
|
|
3030
2013
|
const seq = bootSeq += 1;
|
|
3031
2014
|
autoOn = false;
|
|
3032
|
-
// A live link survives a recast: once the new burrow's store is open the
|
|
3033
|
-
// room re-binds to it (below), pushing the fresh world to every connected
|
|
3034
|
-
// node instead of quietly abandoning them. The recast moves the world one
|
|
3035
|
-
// epoch forward, read from the store the peers have all converged on, so
|
|
3036
|
-
// nobody's leftover snapshots from the old run can outrank the new one.
|
|
3037
|
-
const liveRoom = room;
|
|
3038
|
-
let nextEpoch = 0;
|
|
3039
|
-
if (liveRoom && session) {
|
|
3040
|
-
try {
|
|
3041
|
-
nextEpoch = (await session.snapshot()).state.epoch + 1;
|
|
3042
|
-
} catch (err) {
|
|
3043
|
-
nextEpoch = 1;
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
claimedElsewhere = {};
|
|
3047
|
-
wavingNow = [];
|
|
3048
2015
|
const playBtn = el("autoToggle");
|
|
3049
2016
|
playBtn.setAttribute("aria-pressed", "false");
|
|
3050
2017
|
playBtn.textContent = "\\u25B6 play";
|
|
@@ -3083,30 +2050,16 @@ function pageScript() {
|
|
|
3083
2050
|
renderStage();
|
|
3084
2051
|
// slotOf has to match cast from the same synchronous stretch that just
|
|
3085
2052
|
// rebuilt the DOM for it \\u2014 a render triggered anywhere in the awaits
|
|
3086
|
-
// below
|
|
3087
|
-
// scenario's
|
|
3088
|
-
//
|
|
2053
|
+
// below must never see this scenario's cast paired with the LAST
|
|
2054
|
+
// scenario's pane ids, which is what a null pane-element lookup in
|
|
2055
|
+
// renderAll means when it happens.
|
|
3089
2056
|
for (let i = 0; i < cast.length; i += 1) slotOf[cast[i]] = slots[i];
|
|
3090
2057
|
const opened = await window.tmct.open(scenario().worldPayload, {
|
|
3091
|
-
characters: everyone(),
|
|
2058
|
+
characters: everyone(),
|
|
3092
2059
|
getTeachEnabled: function () { return el("teachToggle").checked; },
|
|
3093
2060
|
});
|
|
3094
2061
|
if (seq !== bootSeq) return;
|
|
3095
2062
|
session = opened;
|
|
3096
|
-
if (liveRoom) {
|
|
3097
|
-
try {
|
|
3098
|
-
await liveRoom.rebind({ memoryDir: opened.memoryDir, worldName: worldName, myDisplayName: myDisplayName });
|
|
3099
|
-
renderWire();
|
|
3100
|
-
renderNodes();
|
|
3101
|
-
// After renderWire, the same way dropRoom writes its own reason: the
|
|
3102
|
-
// render restates the state's stock note, and WHY this recast kept
|
|
3103
|
-
// the link is the thing worth saying over it.
|
|
3104
|
-
el("wireStateNote").textContent = note || "the burrow recast \\u2014 still linked.";
|
|
3105
|
-
await claimMyAnimals();
|
|
3106
|
-
} catch (err) {
|
|
3107
|
-
dropRoom("the link didn't survive the recast \\u2014 share again to link up.");
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
2063
|
bindPanes();
|
|
3111
2064
|
for (const character of cast) {
|
|
3112
2065
|
const w = paneIdFor(character);
|
|
@@ -3122,20 +2075,6 @@ function pageScript() {
|
|
|
3122
2075
|
}
|
|
3123
2076
|
|
|
3124
2077
|
wireDeck();
|
|
3125
|
-
wireNetPanel();
|
|
3126
|
-
renderWire();
|
|
3127
|
-
renderNodes();
|
|
3128
2078
|
boot();
|
|
3129
|
-
// Off the boot path on purpose: this fetches the shared P2P asset so the
|
|
3130
|
-
// panel can show a real node name and burrow name before anyone clicks
|
|
3131
|
-
// anything. A failure here costs sharing, never the digging.
|
|
3132
|
-
ensureIdentity().then(renderNodes).catch(function () { /* sharing stays unavailable; the burrow does not */ });
|
|
3133
|
-
if (invite) {
|
|
3134
|
-
prepareJoinCard().catch(function (err) {
|
|
3135
|
-
el("joinProblem").textContent = "the networking asset didn't load (" + (err && err.message ? err.message : err) + ").";
|
|
3136
|
-
el("joinProblem").hidden = false;
|
|
3137
|
-
el("joinBtn").hidden = true;
|
|
3138
|
-
});
|
|
3139
|
-
}
|
|
3140
2079
|
})();`;
|
|
3141
2080
|
}
|