@polycode-projects/the-mechanical-code-talker 3.2.0 → 3.3.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/corpus/sprites/src/sprite-facts.jsonl +10 -0
- package/corpus/tier2/generate.mjs +10 -1
- package/corpus/tier2/human.jsonl +23 -0
- package/corpus/tier2/manifest.json +3 -3
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +15 -5
- package/corpus/worlds/shards/mud-garden.jsonl.gz +0 -0
- package/corpus/worlds/src/mud-garden.jsonl +68 -0
- package/package.json +2 -1
- package/src/domain/game-config.mjs +46 -0
- package/src/domain/grammar/ace.mjs +11 -3
- package/src/domain/grammar/lexicon-core.json +3 -0
- package/src/domain/memory/trust.mjs +13 -0
- package/src/services/adventure.mjs +351 -62
- package/src/services/chat-session.mjs +28 -3
- package/src/services/chat.mjs +2 -2
- package/src/services/mud-turn.mjs +395 -0
- package/src/services/mud-viz.mjs +752 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +60 -60
- package/src/surfaces/web/mud-browser-entry.mjs +164 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
// mud-viz.mjs — mud.html: the self-contained proof of the shared,
|
|
2
|
+
// multi-character shape PLAN_MUD.md's "Demo phase" section describes. Four
|
|
3
|
+
// burrowing animals (mole-1, vole-1, badger-2, groundhog-1 — mud-garden.jsonl
|
|
4
|
+
// ships all four) each get their own window over ONE shared live world
|
|
5
|
+
// (mud-browser-entry.mjs's createMudSession); a fifth, omniscient view — the
|
|
6
|
+
// soil cross-section world map — sits in the middle with no fog of war at
|
|
7
|
+
// all, the one deliberate exception PLAN_MUD.md names.
|
|
8
|
+
//
|
|
9
|
+
// Shaped after adventure-viz.mjs/spider-fly-viz.mjs: one inlined <style>
|
|
10
|
+
// over viz-theme.mjs's shared tokens, behaviour as an inlined IIFE, the
|
|
11
|
+
// engine arriving via a sibling <script src="./mud-browser.bundle.js">
|
|
12
|
+
// (mirroring adventure-viz.mjs's own worldPayload-embedding rationale — the
|
|
13
|
+
// world's canonical source is a Node-only gzipped JSONL shard the browser
|
|
14
|
+
// cannot read, so it is read ONCE at build time through the real worlds-pack
|
|
15
|
+
// provider and embedded as page data). `createTicker` and a small set of
|
|
16
|
+
// self-contained, `.toString()`-splice-safe room-scene helpers are spliced
|
|
17
|
+
// into the inline script exactly the way spider-fly-viz.mjs splices its own
|
|
18
|
+
// render-glue — `roomSceneObjects`/`spriteClassForObject`/`visibleRoomOf`/
|
|
19
|
+
// `roomKindForRoom` are REUSED directly from adventure-viz.mjs rather than
|
|
20
|
+
// re-derived: mud-garden ships no individual named "player" (the whole point
|
|
21
|
+
// of the multi-character demo), so those functions' own hardcoded "player"
|
|
22
|
+
// exclusion never fires for a mud character — every character reads back as
|
|
23
|
+
// an ordinary visible object of its room until this page's own code filters
|
|
24
|
+
// the CURRENT viewing character out by name (mudRoomSceneObjects, below).
|
|
25
|
+
//
|
|
26
|
+
// A room's graphic is a rendering of worldDigestRows'/roomAffordances' own
|
|
27
|
+
// text digest, never a replacement for it (PLAN_MUD.md's own "Room view"
|
|
28
|
+
// spec) — this page keeps the layout intentionally simpler than Ashcombe
|
|
29
|
+
// Hall's wall/floor stacking (adventure-viz.mjs's roomSceneLayout): a soil-
|
|
30
|
+
// toned canvas backdrop (roomKindForRoom picks outdoor/underground), the
|
|
31
|
+
// viewing character's own sprite drawn center-low, every room-mate and loose
|
|
32
|
+
// object laid out as a plain wrapped tray of sprites above it. PLAN_MUD.md
|
|
33
|
+
// specifies the canvas+sprite-layer TECHNIQUE and the ground-truth-vs-
|
|
34
|
+
// rendering relationship, not stacking physics, so this is this page's own
|
|
35
|
+
// engineering call on the gap PLAN_MUD.md leaves open, not a cut from its
|
|
36
|
+
// spec.
|
|
37
|
+
//
|
|
38
|
+
// FIVE ticker instances (viz-ticker.mjs's createTicker): one per window, so
|
|
39
|
+
// each can play/pause/step independently, plus the rail's own "auto" control
|
|
40
|
+
// that just calls .play()/.pause() on the four window tickers at once. Every
|
|
41
|
+
// tick, from ANY window, is funneled through one shared async queue
|
|
42
|
+
// (serializeTick, spliced below) so two characters' turns can never
|
|
43
|
+
// interleave their reads/writes of the one shared memoryDir — mud-browser-
|
|
44
|
+
// entry.mjs's own header names this as the caller's responsibility, and this
|
|
45
|
+
// is where that responsibility is discharged. The queue is also what makes
|
|
46
|
+
// the rail's GLOBAL turn counter well-defined: it increments in the exact
|
|
47
|
+
// order turns actually executed, never a race between windows.
|
|
48
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson, embedScriptText } from "./viz-theme.mjs";
|
|
49
|
+
import { createTicker } from "./viz-ticker.mjs";
|
|
50
|
+
import {
|
|
51
|
+
roomSceneObjects, scenePlacement, spriteClassForObject, spriteAncestryRows,
|
|
52
|
+
visibleRoomOf, roomKindForRoom, factsForSubject,
|
|
53
|
+
} from "./adventure-viz.mjs";
|
|
54
|
+
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
55
|
+
|
|
56
|
+
const DEFAULT_TITLE = "tmct — the mud";
|
|
57
|
+
// No display-face embedding pipeline exists anywhere in this project yet
|
|
58
|
+
// (grep turns up no @font-face/font-display in any *-viz.mjs) — Fraunces
|
|
59
|
+
// itself never ships, so DISPLAY_STACK is SERIF_STACK's own web-safe serif
|
|
60
|
+
// fallback, named separately only so a later session that DOES add a font
|
|
61
|
+
// pipeline has one obvious constant to point at Fraunces instead of typing
|
|
62
|
+
// "Georgia" into headings by hand.
|
|
63
|
+
const DISPLAY_STACK = SERIF_STACK;
|
|
64
|
+
const SANS_STACK = `"IBM Plex Sans", "Inter", -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
65
|
+
|
|
66
|
+
const ROOT_ROOM = "garden";
|
|
67
|
+
const SLOTS = ["nw", "ne", "sw", "se"];
|
|
68
|
+
const DEFAULT_DELAY_MS = 650;
|
|
69
|
+
const DEFAULT_MAX_TURNS = 400;
|
|
70
|
+
|
|
71
|
+
const MUD_NOTE_LINES = [
|
|
72
|
+
"Four burrowing animals share one world here. Each one only knows what it has dug up, asked about, or been told. Nobody sees the whole map except you, watching from the middle.",
|
|
73
|
+
`This is a MUD, short for Multi Underground creature Dig. The name nods to 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.`,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** A character id's species — "mole-1" -> "mole", "groundhog-1" ->
|
|
77
|
+
* "groundhog" — mirroring spider-fly-viz.mjs's own classOfAgentId. Self-
|
|
78
|
+
* contained, `.toString()`-splice safe. */
|
|
79
|
+
export function speciesOfCharacter(id) {
|
|
80
|
+
return String(id).replace(/-\d+$/, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every object visible in `here` EXCEPT `viewer` itself — `roomSceneObjects`
|
|
84
|
+
* (adventure-viz.mjs) only ever excludes the literal id "player", which no
|
|
85
|
+
* mud-garden character is ever named, so every OTHER character sharing the
|
|
86
|
+
* room (and every loose object) comes back exactly like any prop; this page
|
|
87
|
+
* draws the viewer's own sprite separately, so it is the one subject this
|
|
88
|
+
* wrapper drops. Pure, self-contained (roomSceneObjects is spliced
|
|
89
|
+
* alongside it). */
|
|
90
|
+
export function mudRoomSceneObjects(rows, state, here, viewer) {
|
|
91
|
+
return roomSceneObjects(rows, state, here).filter((o) => o.subject !== viewer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Every object `character` carries, sorted, each with its sprite class —
|
|
95
|
+
* mud's own version of adventure-viz.mjs's carriedItems, parametrized by
|
|
96
|
+
* character instead of hardcoded to "player" (mud-garden ships no such
|
|
97
|
+
* individual). Pure, self-contained. */
|
|
98
|
+
export function carriedItemsFor(rows, state, character) {
|
|
99
|
+
return [...state.placements]
|
|
100
|
+
.filter(([, p]) => p.predicate === "mgx:located-in" && p.object === character)
|
|
101
|
+
.map(([subject]) => ({ subject, spriteClass: spriteClassForObject(rows, subject) }))
|
|
102
|
+
.sort((a, b) => a.subject.localeCompare(b.subject));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Every room's depth level, `Map<roomId, number>`, BFS from `root` at level
|
|
106
|
+
* 0: an "up"/"down" exit moves the level by ∓ 1, any other direction
|
|
107
|
+
* keeps the level unchanged — the soil cross-section's own row assignment,
|
|
108
|
+
* and each window's own minimap filter ("this character's own level only").
|
|
109
|
+
* A room unreachable from `root` (should not happen — every dug room writes
|
|
110
|
+
* a two-way exit back) is simply absent from the map rather than guessed
|
|
111
|
+
* at. Pure, self-contained. */
|
|
112
|
+
export function levelsOf(state, root = "garden") {
|
|
113
|
+
const levels = new Map([[root, 0]]);
|
|
114
|
+
const queue = [root];
|
|
115
|
+
while (queue.length) {
|
|
116
|
+
const room = queue.shift();
|
|
117
|
+
const level = levels.get(room);
|
|
118
|
+
for (const [direction, target] of state.exits.get(room) ?? []) {
|
|
119
|
+
if (levels.has(target)) continue;
|
|
120
|
+
const delta = direction === "down" ? -1 : direction === "up" ? 1 : 0;
|
|
121
|
+
levels.set(target, level + delta);
|
|
122
|
+
queue.push(target);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return levels;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `levels` grouped into `{ level, rooms: [roomId...] }` rows, deepest last
|
|
129
|
+
* so a caller can lay the soil cross-section out top (level 0) to bottom —
|
|
130
|
+
* rooms within a level sort by id for a stable, deterministic column order.
|
|
131
|
+
* Pure. */
|
|
132
|
+
export function levelBands(levels) {
|
|
133
|
+
const byLevel = new Map();
|
|
134
|
+
for (const [room, level] of levels) {
|
|
135
|
+
if (!byLevel.has(level)) byLevel.set(level, []);
|
|
136
|
+
byLevel.get(level).push(room);
|
|
137
|
+
}
|
|
138
|
+
return [...byLevel.entries()]
|
|
139
|
+
.sort((a, b) => b[0] - a[0])
|
|
140
|
+
.map(([level, rooms]) => ({ level, rooms: rooms.sort() }));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Which characters currently stand in `room` — the omniscient world map's
|
|
144
|
+
* own per-room roster, and a per-window minimap's own (visited-only)
|
|
145
|
+
* roster. Pure. */
|
|
146
|
+
export function charactersInRoom(state, room, characters) {
|
|
147
|
+
return characters.filter((c) => state.placements.get(c)?.object === room);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function slugForSlot(slot) {
|
|
151
|
+
return `window-${slot}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The self-contained mud.html page. Pure — identical output for identical
|
|
155
|
+
* input; every other piece of state (the live world, chat, ticks) is
|
|
156
|
+
* computed in the browser once the sibling bundle loads.
|
|
157
|
+
* `characters` is `[{ id, slot, species }]`, one entry per window, in NW,
|
|
158
|
+
* NE, SW, SE order. `worldPayload` is `{ name, facts, rules, opening }`,
|
|
159
|
+
* read once at build time through the real worlds-pack provider (see this
|
|
160
|
+
* module's own header). `spriteTemplates` is the large-tier sprite set
|
|
161
|
+
* (data/sprites-large/*.toml) so mole/vole/badger/groundhog/meerkat all
|
|
162
|
+
* resolve their own art instead of falling back to the flat animal icon.
|
|
163
|
+
* `mudConfig` defaults to game-config.mjs's own DEFAULT_GAME_CONFIG.mud —
|
|
164
|
+
* the per-species mass/speed/dig-reach reference table the Creature Stats
|
|
165
|
+
* panel reads for flavor (see this module's header on why the live
|
|
166
|
+
* simulation does not yet act on speed/dig-reach itself).
|
|
167
|
+
* `engineBundleJs` inlines the built mud-browser bundle instead of the
|
|
168
|
+
* sibling `<script src>`, mirroring spider-fly-viz.mjs's own standalone-
|
|
169
|
+
* export knob; default empty keeps the site build's sibling-file
|
|
170
|
+
* arrangement unchanged. */
|
|
171
|
+
export function renderMudHtml({
|
|
172
|
+
title = DEFAULT_TITLE,
|
|
173
|
+
worldPayload,
|
|
174
|
+
characters = [],
|
|
175
|
+
spriteTemplates = [],
|
|
176
|
+
mudConfig = DEFAULT_GAME_CONFIG.mud,
|
|
177
|
+
engineBundleJs = "",
|
|
178
|
+
} = {}) {
|
|
179
|
+
const pageData = embedJson({
|
|
180
|
+
worldPayload, characters, spriteTemplates, mudConfig,
|
|
181
|
+
rootRoom: ROOT_ROOM,
|
|
182
|
+
defaultDelayMs: DEFAULT_DELAY_MS,
|
|
183
|
+
defaultMaxTurns: DEFAULT_MAX_TURNS,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const windowHtml = characters.map((c) => windowMarkup(c)).join("\n");
|
|
187
|
+
|
|
188
|
+
return `<!doctype html>
|
|
189
|
+
<html lang="en">
|
|
190
|
+
<head>
|
|
191
|
+
<meta charset="utf-8">
|
|
192
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
193
|
+
<title>${escapeHtml(title)}</title>
|
|
194
|
+
${engineBundleJs ? "" : `<link rel="icon" href="./favicon.svg" type="image/svg+xml">
|
|
195
|
+
<link rel="icon" href="./favicon.ico" sizes="any">
|
|
196
|
+
<link rel="apple-touch-icon" href="./apple-touch-icon.png">`}
|
|
197
|
+
<style>
|
|
198
|
+
${THEME_TOKENS_CSS}
|
|
199
|
+
${MUD_STYLE}
|
|
200
|
+
</style>
|
|
201
|
+
</head>
|
|
202
|
+
<body>
|
|
203
|
+
<main>
|
|
204
|
+
<div class="eyebrow">tmct · mud</div>
|
|
205
|
+
<h1>Multiple actors, one shared world</h1>
|
|
206
|
+
<div class="mud-stage" id="mudStage">
|
|
207
|
+
${windowHtml}
|
|
208
|
+
<div class="world-map" id="worldMap" aria-label="the whole world, every level, every character">
|
|
209
|
+
<div class="world-map-head">
|
|
210
|
+
<span class="world-map-title">the whole burrow</span>
|
|
211
|
+
<span class="mono world-map-turn" id="worldMapTurn">turn 0</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="world-map-bands" id="worldMapBands"></div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="rail" id="rail" aria-label="simulation controls">
|
|
217
|
+
<div class="rail-row">
|
|
218
|
+
<button type="button" id="autoToggle" aria-pressed="false">▶ auto</button>
|
|
219
|
+
<span class="mono rail-turns" id="globalTurnCount">turns: 0</span>
|
|
220
|
+
<label class="rail-slider">delay
|
|
221
|
+
<input type="range" id="delaySlider" min="80" max="2000" step="20" value="${DEFAULT_DELAY_MS}">
|
|
222
|
+
<span class="mono" id="delayValue">${DEFAULT_DELAY_MS}ms</span>
|
|
223
|
+
</label>
|
|
224
|
+
<label class="rail-slider">max turns
|
|
225
|
+
<input type="range" id="maxTurnsSlider" min="20" max="2000" step="20" value="${DEFAULT_MAX_TURNS}">
|
|
226
|
+
<span class="mono" id="maxTurnsValue">${DEFAULT_MAX_TURNS}</span>
|
|
227
|
+
</label>
|
|
228
|
+
<button type="button" id="resetBtn">reset</button>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="rail-note mud-note">
|
|
231
|
+
<p>${escapeHtml(MUD_NOTE_LINES[0])}</p>
|
|
232
|
+
<p>${escapeHtml(MUD_NOTE_LINES[1])}</p>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</main>
|
|
236
|
+
<script>
|
|
237
|
+
const MUD_PAGE_DATA = ${pageData};
|
|
238
|
+
</script>
|
|
239
|
+
${engineBundleJs ? `<script>\n${embedScriptText(engineBundleJs)}\n</script>` : `<script src="./mud-browser.bundle.js"></script>`}
|
|
240
|
+
<script>
|
|
241
|
+
${embedScriptText(pageScript())}
|
|
242
|
+
</script>
|
|
243
|
+
</body>
|
|
244
|
+
</html>
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function windowMarkup({ id, slot, species }) {
|
|
249
|
+
const w = slugForSlot(slot);
|
|
250
|
+
return `<div class="mud-window" id="${w}" data-slot="${escapeHtml(slot)}" data-character="${escapeHtml(id)}">
|
|
251
|
+
<div class="mud-window-head">
|
|
252
|
+
<h2>${escapeHtml(species)} <span class="mono char-id">${escapeHtml(id)}</span></h2>
|
|
253
|
+
<span class="mono window-turn" id="${w}-turn">turn 0</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="room-view" id="${w}-room">
|
|
256
|
+
<canvas id="${w}-canvas" width="360" height="220" aria-hidden="true"></canvas>
|
|
257
|
+
<div class="sprite-layer" id="${w}-sprites"></div>
|
|
258
|
+
<div class="speech-bubble" id="${w}-bubble" hidden></div>
|
|
259
|
+
<div class="dig-flourish" id="${w}-flourish" hidden></div>
|
|
260
|
+
</div>
|
|
261
|
+
<p class="room-caption" id="${w}-caption"></p>
|
|
262
|
+
<div class="window-columns">
|
|
263
|
+
<div class="pouch" id="${w}-pouch" aria-label="pouch">
|
|
264
|
+
<h3>pouch</h3>
|
|
265
|
+
<ul class="pouch-list" id="${w}-pouch-list"></ul>
|
|
266
|
+
<p class="mono stat-line" id="${w}-stats"></p>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="minimap" aria-label="what this character has discovered">
|
|
269
|
+
<h3>known ground</h3>
|
|
270
|
+
<div class="minimap-rooms" id="${w}-minimap"></div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="window-controls controls-row">
|
|
274
|
+
<button type="button" id="${w}-play" disabled>▶ play</button>
|
|
275
|
+
<button type="button" id="${w}-step" disabled>step</button>
|
|
276
|
+
<span class="mono" id="${w}-count">0 turns</span>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="chat">
|
|
279
|
+
<div class="chatlog" id="${w}-chatlog" aria-live="polite"></div>
|
|
280
|
+
<form class="chatask" id="${w}-chatform">
|
|
281
|
+
<span class="prompt mono">tmct></span>
|
|
282
|
+
<input id="${w}-chatq" type="text" placeholder="dig north" aria-label="talk to ${escapeHtml(id)}" disabled>
|
|
283
|
+
</form>
|
|
284
|
+
<div class="chatpills" id="${w}-chatpills" role="group" aria-label="quick commands">
|
|
285
|
+
<button type="button" class="pill" data-fill="look" disabled>look</button>
|
|
286
|
+
<button type="button" class="pill" data-fill="what do you know about food" disabled>what do you know about food</button>
|
|
287
|
+
<button type="button" class="pill" data-fill="dig north" disabled>dig north</button>
|
|
288
|
+
<button type="button" class="pill" data-fill="dig down" disabled>dig down</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const MUD_STYLE = `
|
|
295
|
+
:root {
|
|
296
|
+
--soil-deep: #2B1D14; --soil-mid: #4A3324; --soil-light: #7A5A3D;
|
|
297
|
+
--root-moss: #6B7A4F; --parchment: #EFE6D8; --mud-ink: #2A211A; --burrow-glow: #E8A33D;
|
|
298
|
+
}
|
|
299
|
+
html { background: var(--soil-deep); }
|
|
300
|
+
body { margin: 0; background: linear-gradient(180deg, var(--root-moss) 0%, var(--soil-light) 18%, var(--soil-mid) 55%, var(--soil-deep) 100%) fixed; color: var(--mud-ink); font-family: ${SANS_STACK}; font-size: 15px; line-height: 1.5; }
|
|
301
|
+
.mono { font-family: ${MONO_STACK}; }
|
|
302
|
+
main { max-width: 1280px; margin: 0 auto; padding: 1.4rem 1.2rem 2.4rem; }
|
|
303
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--parchment); opacity: .85; }
|
|
304
|
+
h1 { font-family: ${DISPLAY_STACK}; font-weight: 600; font-size: 1.7rem; margin: .25rem 0 1.1rem; color: var(--parchment); text-wrap: balance; }
|
|
305
|
+
h2 { font-family: ${DISPLAY_STACK}; font-size: 1rem; margin: 0; text-transform: capitalize; }
|
|
306
|
+
h3 { font-family: ${DISPLAY_STACK}; font-size: .78rem; margin: 0 0 .35rem; text-transform: uppercase; letter-spacing: .04em; color: var(--soil-mid); }
|
|
307
|
+
button { font: inherit; color: inherit; background: none; cursor: pointer; }
|
|
308
|
+
button:focus-visible, input:focus-visible { outline: 2px solid var(--burrow-glow); outline-offset: 2px; }
|
|
309
|
+
button:disabled { opacity: .45; cursor: default; }
|
|
310
|
+
|
|
311
|
+
.mud-stage {
|
|
312
|
+
position: relative;
|
|
313
|
+
display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; gap: 1rem;
|
|
314
|
+
}
|
|
315
|
+
@media (max-width: 760px) { .mud-stage { grid-template-columns: 1fr; } .world-map { position: static; margin: .5rem 0; transform: none; } }
|
|
316
|
+
|
|
317
|
+
.mud-window {
|
|
318
|
+
background: var(--parchment); border: 1px solid var(--soil-mid); border-radius: 4px;
|
|
319
|
+
box-shadow: 0 2px 0 rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.35);
|
|
320
|
+
padding: .7rem .8rem; display: flex; flex-direction: column; gap: .5rem; min-width: 0;
|
|
321
|
+
}
|
|
322
|
+
.mud-window-head { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
|
|
323
|
+
.char-id { font-size: .68rem; color: var(--soil-mid); }
|
|
324
|
+
.window-turn { font-size: .7rem; color: var(--soil-mid); }
|
|
325
|
+
|
|
326
|
+
.room-view { position: relative; border-radius: 3px; overflow: hidden; border: 1px solid var(--soil-mid); }
|
|
327
|
+
.room-view canvas { display: block; width: 100%; height: auto; }
|
|
328
|
+
.sprite-layer { position: absolute; inset: 0; display: flex; align-items: flex-end; flex-wrap: wrap; gap: 2%; padding: 4%; box-sizing: border-box; }
|
|
329
|
+
.sprite { width: 15%; min-width: 28px; transition: transform .2s ease; }
|
|
330
|
+
.sprite.self { width: 20%; min-width: 34px; order: -1; }
|
|
331
|
+
.sprite svg { width: 100%; display: block; }
|
|
332
|
+
@media (prefers-reduced-motion: reduce) { .sprite { transition: none; } }
|
|
333
|
+
|
|
334
|
+
.speech-bubble {
|
|
335
|
+
position: absolute; top: 6%; left: 6%; max-width: 78%;
|
|
336
|
+
background: var(--parchment); color: var(--mud-ink); border: 1px solid var(--soil-mid); border-radius: 8px;
|
|
337
|
+
padding: .3rem .55rem; font-size: .72rem; line-height: 1.3; box-shadow: 0 2px 4px rgba(0,0,0,.25);
|
|
338
|
+
opacity: 0; transition: opacity .35s ease;
|
|
339
|
+
}
|
|
340
|
+
.speech-bubble.shown { opacity: 1; }
|
|
341
|
+
@media (prefers-reduced-motion: reduce) { .speech-bubble { transition: none; } }
|
|
342
|
+
|
|
343
|
+
.dig-flourish { position: absolute; inset: 0; pointer-events: none; opacity: 0; background: radial-gradient(circle at 50% 70%, var(--burrow-glow) 0%, transparent 60%); transition: opacity .5s ease; }
|
|
344
|
+
.dig-flourish.shown { opacity: .55; }
|
|
345
|
+
@media (prefers-reduced-motion: reduce) { .dig-flourish { transition: none; opacity: 0 !important; } }
|
|
346
|
+
|
|
347
|
+
.room-caption { font-size: .8rem; margin: 0; color: var(--soil-mid); }
|
|
348
|
+
|
|
349
|
+
.window-columns { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
|
|
350
|
+
.pouch, .minimap { background: rgba(255,255,255,.35); border: 1px solid var(--soil-light); border-radius: 3px; padding: .4rem .5rem; }
|
|
351
|
+
.pouch-list { list-style: none; margin: 0; padding: 0; font-size: .74rem; display: flex; flex-direction: column; gap: .15rem; }
|
|
352
|
+
.pouch-list:empty::after { content: "carrying nothing"; color: var(--soil-mid); font-style: italic; }
|
|
353
|
+
.stat-line { margin: .3rem 0 0; font-size: .64rem; color: var(--soil-mid); }
|
|
354
|
+
.minimap-rooms { display: flex; flex-wrap: wrap; gap: .25rem; font-family: ${MONO_STACK}; font-size: .62rem; }
|
|
355
|
+
.minimap-room { padding: .12rem .35rem; background: var(--soil-light); color: var(--parchment); border-radius: 2px; }
|
|
356
|
+
.minimap-room.current { background: var(--burrow-glow); color: var(--mud-ink); font-weight: 600; }
|
|
357
|
+
.minimap-rooms:empty::after { content: "nothing dug yet"; color: var(--soil-mid); font-family: ${SANS_STACK}; font-size: .68rem; font-style: italic; }
|
|
358
|
+
|
|
359
|
+
.window-controls button, .rail button { font-family: ${MONO_STACK}; font-size: .72rem; text-transform: uppercase; letter-spacing: .02em; padding: .3rem .6rem; border: 1px solid var(--soil-mid); border-radius: 3px; background: var(--parchment); }
|
|
360
|
+
.window-controls button:hover:not(:disabled), .rail button:hover:not(:disabled) { border-color: var(--burrow-glow); }
|
|
361
|
+
.window-controls { gap: .4rem; }
|
|
362
|
+
.window-controls .mono { margin-left: auto; font-size: .7rem; }
|
|
363
|
+
|
|
364
|
+
.chat { display: flex; flex-direction: column; gap: .35rem; }
|
|
365
|
+
.chatlog { display: flex; flex-direction: column; gap: .3rem; max-height: 140px; overflow-y: auto; }
|
|
366
|
+
.chatlog .u { font-family: ${MONO_STACK}; font-size: .68rem; color: var(--soil-mid); }
|
|
367
|
+
.chatlog .u::before { content: "tmct> "; color: var(--burrow-glow); }
|
|
368
|
+
.chatlog .a { font-size: .76rem; line-height: 1.35; }
|
|
369
|
+
.chatask { display: flex; align-items: center; gap: .4rem; }
|
|
370
|
+
.chatask .prompt { color: var(--burrow-glow); font-size: .72rem; }
|
|
371
|
+
.chatask input { flex: 1; min-width: 0; font-family: ${MONO_STACK}; font-size: .72rem; background: rgba(255,255,255,.6); color: var(--mud-ink); border: 1px solid var(--soil-mid); border-radius: 2px; padding: .28rem .45rem; box-sizing: border-box; }
|
|
372
|
+
.chatpills { display: flex; flex-wrap: wrap; gap: .25rem; }
|
|
373
|
+
.pill { font-family: ${MONO_STACK}; font-size: .62rem; padding: .15rem .5rem; border: 1px solid var(--soil-mid); border-radius: 99px; background: rgba(255,255,255,.5); }
|
|
374
|
+
.pill:hover:not(:disabled) { border-color: var(--burrow-glow); }
|
|
375
|
+
|
|
376
|
+
.world-map {
|
|
377
|
+
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 5;
|
|
378
|
+
width: min(420px, 90%); background: var(--soil-deep); color: var(--parchment);
|
|
379
|
+
border: 2px solid var(--burrow-glow); border-radius: 6px; padding: .5rem .6rem .7rem;
|
|
380
|
+
box-shadow: 0 6px 24px rgba(0,0,0,.45);
|
|
381
|
+
}
|
|
382
|
+
.world-map-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: .35rem; }
|
|
383
|
+
.world-map-title { font-family: ${DISPLAY_STACK}; font-size: .82rem; letter-spacing: .02em; }
|
|
384
|
+
.world-map-turn { font-size: .66rem; opacity: .8; }
|
|
385
|
+
.world-map-bands { display: flex; flex-direction: column; gap: 2px; }
|
|
386
|
+
.map-band { display: flex; align-items: center; gap: .35rem; padding: .3rem .4rem; border-radius: 2px; }
|
|
387
|
+
.map-band .band-level { font-family: ${MONO_STACK}; font-size: .6rem; width: 2.4em; opacity: .75; }
|
|
388
|
+
.map-band .band-rooms { display: flex; flex-wrap: wrap; gap: .3rem; flex: 1; }
|
|
389
|
+
.map-room { font-family: ${MONO_STACK}; font-size: .6rem; padding: .12rem .4rem; border-radius: 2px; background: rgba(239,230,216,.14); }
|
|
390
|
+
.map-room .room-cast { margin-left: .25rem; opacity: .85; }
|
|
391
|
+
.map-room.freshly-dug { animation: dig-pulse 1.1s ease-out; }
|
|
392
|
+
@keyframes dig-pulse { 0% { box-shadow: 0 0 0 0 var(--burrow-glow); } 100% { box-shadow: 0 0 0 8px transparent; } }
|
|
393
|
+
@media (prefers-reduced-motion: reduce) { .map-room.freshly-dug { animation: none; } }
|
|
394
|
+
|
|
395
|
+
.rail { margin-top: 1.2rem; background: var(--parchment); border: 1px solid var(--soil-mid); border-radius: 4px; padding: .7rem .9rem; }
|
|
396
|
+
.rail-row { display: flex; flex-wrap: wrap; align-items: center; gap: .7rem; }
|
|
397
|
+
.rail-turns { font-size: .78rem; }
|
|
398
|
+
.rail-slider { display: flex; align-items: center; gap: .35rem; font-size: .68rem; color: var(--soil-mid); }
|
|
399
|
+
.rail-slider input[type="range"] { accent-color: var(--burrow-glow); }
|
|
400
|
+
.rail-note { margin-top: .6rem; padding-top: .55rem; border-top: 1px solid var(--soil-light); font-size: .78rem; color: var(--soil-mid); }
|
|
401
|
+
.rail-note p { margin: 0 0 .4rem; }
|
|
402
|
+
.rail-note p:last-child { margin-bottom: 0; }
|
|
403
|
+
`;
|
|
404
|
+
|
|
405
|
+
/** The inlined page script, as a plain function body handed to
|
|
406
|
+
* embedScriptText — mirrors spider-fly-viz.mjs's own `(function () {
|
|
407
|
+
* "use strict"; ... })()` IIFE shape, with every spliced helper listed at
|
|
408
|
+
* the top of the closure exactly like that page's own const bindings. */
|
|
409
|
+
function pageScript() {
|
|
410
|
+
return `(function () {
|
|
411
|
+
"use strict";
|
|
412
|
+
const DATA = MUD_PAGE_DATA;
|
|
413
|
+
const createTicker = ${createTicker.toString()};
|
|
414
|
+
const esc = ${escapeHtml.toString()};
|
|
415
|
+
const speciesOfCharacter = ${speciesOfCharacter.toString()};
|
|
416
|
+
const mudRoomSceneObjects = ${mudRoomSceneObjects.toString()};
|
|
417
|
+
const carriedItemsFor = ${carriedItemsFor.toString()};
|
|
418
|
+
const levelsOf = ${levelsOf.toString()};
|
|
419
|
+
const levelBands = ${levelBands.toString()};
|
|
420
|
+
const charactersInRoom = ${charactersInRoom.toString()};
|
|
421
|
+
const roomSceneObjects = ${roomSceneObjects.toString()};
|
|
422
|
+
const scenePlacement = ${scenePlacement.toString()};
|
|
423
|
+
const spriteClassForObject = ${spriteClassForObject.toString()};
|
|
424
|
+
const spriteAncestryRows = ${spriteAncestryRows.toString()};
|
|
425
|
+
const visibleRoomOf = ${visibleRoomOf.toString()};
|
|
426
|
+
const roomKindForRoom = ${roomKindForRoom.toString()};
|
|
427
|
+
const factsForSubject = ${factsForSubject.toString()};
|
|
428
|
+
|
|
429
|
+
const el = (id) => document.getElementById(id);
|
|
430
|
+
const characters = DATA.characters;
|
|
431
|
+
const characterIds = characters.map((c) => c.id);
|
|
432
|
+
const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
433
|
+
|
|
434
|
+
// ---- the one shared tick queue -----------------------------------------
|
|
435
|
+
// Every window's ticker calls into this instead of session.windows[...]
|
|
436
|
+
// directly, so two characters' turns can never interleave their reads and
|
|
437
|
+
// writes of the one shared memoryDir, and the global turn counter always
|
|
438
|
+
// increments in real execution order.
|
|
439
|
+
let tickChain = Promise.resolve();
|
|
440
|
+
function serializeTick(fn) {
|
|
441
|
+
const run = tickChain.then(fn, fn);
|
|
442
|
+
tickChain = run.catch(function () {});
|
|
443
|
+
return run;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let session = null;
|
|
447
|
+
let globalTurn = 0;
|
|
448
|
+
let maxTurns = DATA.defaultMaxTurns;
|
|
449
|
+
let delayMs = DATA.defaultDelayMs;
|
|
450
|
+
const wait = function (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); };
|
|
451
|
+
const liveWait = function () { return wait(delayMs); };
|
|
452
|
+
|
|
453
|
+
const knownRoomIds = new Set();
|
|
454
|
+
let freshlyDugRoom = null;
|
|
455
|
+
const speechBubbles = new Map(); // room -> { character, text, expiresAtTurn }
|
|
456
|
+
|
|
457
|
+
const tickers = {};
|
|
458
|
+
let autoOn = false;
|
|
459
|
+
|
|
460
|
+
function hasNext() { return globalTurn < maxTurns; }
|
|
461
|
+
|
|
462
|
+
async function runOneTurn(character) {
|
|
463
|
+
return serializeTick(async function () {
|
|
464
|
+
globalTurn += 1;
|
|
465
|
+
const result = await session.windows[character].autoplayTick(globalTurn);
|
|
466
|
+
afterEngineTurn(character, result);
|
|
467
|
+
return result;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function afterEngineTurn(character, result) {
|
|
472
|
+
if (!result || !result.room) { renderAll(); return; }
|
|
473
|
+
if (!knownRoomIds.has(result.room)) knownRoomIds.add(result.room);
|
|
474
|
+
if (result.roomAfter && !knownRoomIds.has(result.roomAfter)) {
|
|
475
|
+
freshlyDugRoom = result.roomAfter;
|
|
476
|
+
knownRoomIds.add(result.roomAfter);
|
|
477
|
+
}
|
|
478
|
+
const ask = (result.actions || []).find(function (a) { return a.step === "investigate" && a.kind === "ask"; });
|
|
479
|
+
if (ask) {
|
|
480
|
+
speechBubbles.set(result.room, {
|
|
481
|
+
character: character, text: character + " asks about food \\u2014 hears about " + ask.thing + ".",
|
|
482
|
+
expiresAtTurn: globalTurn + 1,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
renderAll();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function ensureTicker(character) {
|
|
489
|
+
if (tickers[character]) return tickers[character];
|
|
490
|
+
const w = windowIdFor(character);
|
|
491
|
+
const playBtn = el(w + "-play");
|
|
492
|
+
const stepBtn = el(w + "-step");
|
|
493
|
+
const ticker = createTicker({
|
|
494
|
+
onTick: function () { return runOneTurn(character); },
|
|
495
|
+
onRender: function (state) {
|
|
496
|
+
playBtn.textContent = state.playing ? "\\u23F8 pause" : "\\u25B6 play";
|
|
497
|
+
},
|
|
498
|
+
hasNext: hasNext,
|
|
499
|
+
wait: liveWait,
|
|
500
|
+
});
|
|
501
|
+
playBtn.addEventListener("click", function () { ticker.play(); });
|
|
502
|
+
stepBtn.addEventListener("click", function () { ticker.stepOnce(); });
|
|
503
|
+
tickers[character] = ticker;
|
|
504
|
+
return ticker;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function windowIdFor(character) {
|
|
508
|
+
const c = characters.find(function (x) { return x.id === character; });
|
|
509
|
+
return "window-" + c.slot;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ---- chat docks ---------------------------------------------------------
|
|
513
|
+
function wireChat(character) {
|
|
514
|
+
const w = windowIdFor(character);
|
|
515
|
+
const form = el(w + "-chatform");
|
|
516
|
+
const input = el(w + "-chatq");
|
|
517
|
+
const log = el(w + "-chatlog");
|
|
518
|
+
const pillsEl = el(w + "-chatpills");
|
|
519
|
+
function append(cls, text) {
|
|
520
|
+
const d = document.createElement("div");
|
|
521
|
+
d.className = cls;
|
|
522
|
+
d.textContent = text;
|
|
523
|
+
log.appendChild(d);
|
|
524
|
+
log.scrollTop = log.scrollHeight;
|
|
525
|
+
}
|
|
526
|
+
form.addEventListener("submit", function (e) {
|
|
527
|
+
e.preventDefault();
|
|
528
|
+
const line = input.value.trim();
|
|
529
|
+
if (!line) return;
|
|
530
|
+
input.value = "";
|
|
531
|
+
append("u", line);
|
|
532
|
+
serializeTick(function () { return session.windows[character].turn(line); }).then(function (res) {
|
|
533
|
+
append("a", res.answer);
|
|
534
|
+
renderAll();
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
for (const pill of pillsEl.querySelectorAll(".pill")) {
|
|
538
|
+
pill.addEventListener("click", function () {
|
|
539
|
+
input.value = pill.getAttribute("data-fill");
|
|
540
|
+
input.focus();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ---- room-view rendering -------------------------------------------------
|
|
546
|
+
function roomKindTint(kind) {
|
|
547
|
+
if (kind === "outdoor") return "var(--root-moss)";
|
|
548
|
+
return "var(--soil-mid)";
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function spriteSvgFor(species, rows) {
|
|
552
|
+
if (window.tmctMud && window.tmctMud.resolveSpriteAsset) {
|
|
553
|
+
return window.tmctMud.resolveSpriteAsset(species, rows, [], DATA.spriteTemplates, window.tmctMud.SPRITE_REGISTRY);
|
|
554
|
+
}
|
|
555
|
+
return "";
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function objectSvgFor(spriteClass, rows) {
|
|
559
|
+
if (window.tmctMud && window.tmctMud.resolveSpriteForClass) {
|
|
560
|
+
return window.tmctMud.resolveSpriteForClass(spriteClass, rows, window.tmctMud.SPRITE_REGISTRY);
|
|
561
|
+
}
|
|
562
|
+
return "";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function drawRoomBackdrop(canvas, kind) {
|
|
566
|
+
const ctx = canvas.getContext("2d");
|
|
567
|
+
const w = canvas.width, h = canvas.height;
|
|
568
|
+
ctx.clearRect(0, 0, w, h);
|
|
569
|
+
const grad = ctx.createLinearGradient(0, 0, 0, h);
|
|
570
|
+
if (kind === "outdoor") {
|
|
571
|
+
grad.addColorStop(0, "#9BB077"); grad.addColorStop(1, "#6B7A4F");
|
|
572
|
+
} else {
|
|
573
|
+
grad.addColorStop(0, "#5A4130"); grad.addColorStop(1, "#2B1D14");
|
|
574
|
+
}
|
|
575
|
+
ctx.fillStyle = grad;
|
|
576
|
+
ctx.fillRect(0, 0, w, h);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function renderRoomView(character, rows, state, here) {
|
|
580
|
+
const w = windowIdFor(character);
|
|
581
|
+
const canvas = el(w + "-canvas");
|
|
582
|
+
const kind = roomKindForRoom(rows, here);
|
|
583
|
+
drawRoomBackdrop(canvas, kind);
|
|
584
|
+
|
|
585
|
+
const layer = el(w + "-sprites");
|
|
586
|
+
layer.innerHTML = "";
|
|
587
|
+
const selfNode = document.createElement("div");
|
|
588
|
+
selfNode.className = "sprite self";
|
|
589
|
+
selfNode.innerHTML = spriteSvgFor(speciesOfCharacter(character), rows);
|
|
590
|
+
layer.appendChild(selfNode);
|
|
591
|
+
|
|
592
|
+
for (const obj of mudRoomSceneObjects(rows, state, here, character)) {
|
|
593
|
+
const node = document.createElement("div");
|
|
594
|
+
const isCast = characterIds.indexOf(obj.subject) !== -1;
|
|
595
|
+
node.className = "sprite" + (isCast ? " roommate" : "");
|
|
596
|
+
node.innerHTML = isCast ? spriteSvgFor(speciesOfCharacter(obj.subject), rows) : objectSvgFor(obj.spriteClass, rows);
|
|
597
|
+
node.title = obj.subject;
|
|
598
|
+
layer.appendChild(node);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const bubbleEl = el(w + "-bubble");
|
|
602
|
+
const bubble = speechBubbles.get(here);
|
|
603
|
+
if (bubble && bubble.expiresAtTurn >= globalTurn) {
|
|
604
|
+
bubbleEl.textContent = bubble.text;
|
|
605
|
+
bubbleEl.hidden = false;
|
|
606
|
+
bubbleEl.classList.add("shown");
|
|
607
|
+
} else {
|
|
608
|
+
bubbleEl.classList.remove("shown");
|
|
609
|
+
bubbleEl.hidden = true;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const flourishEl = el(w + "-flourish");
|
|
613
|
+
if (freshlyDugRoom && freshlyDugRoom === here && !reduceMotion) {
|
|
614
|
+
flourishEl.hidden = false;
|
|
615
|
+
flourishEl.classList.add("shown");
|
|
616
|
+
setTimeout(function () { flourishEl.classList.remove("shown"); flourishEl.hidden = true; }, 900);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
el(w + "-caption").textContent = roomCaptionFor(rows, state, here, character);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function roomCaptionFor(rows, state, here, character) {
|
|
623
|
+
if (!window.tmctMud) return here;
|
|
624
|
+
const view = window.tmctMud.worldDigestRows(rows, state, character);
|
|
625
|
+
const lines = view.filter(function (r) { return r.subject.toLowerCase() === here.toLowerCase(); })
|
|
626
|
+
.map(function (r) { return r.subject + " " + r.predicate + " " + r.object + "."; });
|
|
627
|
+
return lines.length ? lines.join(" ") : ("You are in the " + here + ".");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function renderPouch(character, rows, state) {
|
|
631
|
+
const w = windowIdFor(character);
|
|
632
|
+
const list = el(w + "-pouch-list");
|
|
633
|
+
const items = carriedItemsFor(rows, state, character);
|
|
634
|
+
list.innerHTML = items.map(function (i) { return "<li>" + esc(i.subject) + "</li>"; }).join("");
|
|
635
|
+
const mass = state.masses.get(character);
|
|
636
|
+
const cfgKey = speciesOfCharacter(character);
|
|
637
|
+
const drain = DATA.mudConfig[cfgKey + "MassDecrementPerTurn"];
|
|
638
|
+
el(w + "-stats").textContent = "mass " + (mass ? mass.value : "?") + (drain !== undefined ? " \\u00b7 drain/turn " + drain : "");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function renderMinimap(character, rows, state, here) {
|
|
642
|
+
const w = windowIdFor(character);
|
|
643
|
+
const mapEl = el(w + "-minimap");
|
|
644
|
+
const visited = session.windows[character].visitedRoomIds();
|
|
645
|
+
const levels = levelsOf(state, DATA.rootRoom);
|
|
646
|
+
const myLevel = levels.get(here);
|
|
647
|
+
const rooms = visited.filter(function (r) { return levels.get(r) === myLevel; }).sort();
|
|
648
|
+
mapEl.innerHTML = rooms.map(function (r) {
|
|
649
|
+
return "<span class=\\"minimap-room" + (r === here ? " current" : "") + "\\">" + esc(r) + "</span>";
|
|
650
|
+
}).join("");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function renderWorldMap(rows, state) {
|
|
654
|
+
const levels = levelsOf(state, DATA.rootRoom);
|
|
655
|
+
const bands = levelBands(levels);
|
|
656
|
+
el("worldMapBands").innerHTML = bands.map(function (band) {
|
|
657
|
+
const rooms = band.rooms.map(function (r) {
|
|
658
|
+
const cast = charactersInRoom(state, r, characterIds);
|
|
659
|
+
const castText = cast.length ? "<span class=\\"room-cast\\">" + cast.map(function (c) { return speciesOfCharacter(c); }).join(",") + "</span>" : "";
|
|
660
|
+
const fresh = r === freshlyDugRoom ? " freshly-dug" : "";
|
|
661
|
+
return "<span class=\\"map-room" + fresh + "\\">" + esc(r) + castText + "</span>";
|
|
662
|
+
}).join("");
|
|
663
|
+
return "<div class=\\"map-band\\"><span class=\\"band-level\\">L" + band.level + "</span><span class=\\"band-rooms\\">" + rooms + "</span></div>";
|
|
664
|
+
}).join("");
|
|
665
|
+
el("worldMapTurn").textContent = "turn " + globalTurn;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function renderAll() {
|
|
669
|
+
if (!session) return;
|
|
670
|
+
updateGlobalTurnLabel();
|
|
671
|
+
session.snapshot().then(function (snap) {
|
|
672
|
+
renderWorldMap(snap.rows, snap.state);
|
|
673
|
+
for (const c of characters) {
|
|
674
|
+
const here = snap.state.placements.get(c.id) ? snap.state.placements.get(c.id).object : null;
|
|
675
|
+
const w = "window-" + c.slot;
|
|
676
|
+
el(w + "-turn").textContent = "turn " + globalTurn;
|
|
677
|
+
el(w + "-count").textContent = globalTurn + " turns";
|
|
678
|
+
if (!here) continue;
|
|
679
|
+
renderRoomView(c.id, snap.rows, snap.state, here);
|
|
680
|
+
renderPouch(c.id, snap.rows, snap.state);
|
|
681
|
+
renderMinimap(c.id, snap.rows, snap.state, here);
|
|
682
|
+
}
|
|
683
|
+
freshlyDugRoom = null;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ---- the rail -------------------------------------------------------------
|
|
688
|
+
function wireRail() {
|
|
689
|
+
const autoBtn = el("autoToggle");
|
|
690
|
+
const delaySlider = el("delaySlider");
|
|
691
|
+
const maxTurnsSlider = el("maxTurnsSlider");
|
|
692
|
+
const resetBtn = el("resetBtn");
|
|
693
|
+
autoBtn.addEventListener("click", function () {
|
|
694
|
+
autoOn = !autoOn;
|
|
695
|
+
autoBtn.setAttribute("aria-pressed", autoOn ? "true" : "false");
|
|
696
|
+
autoBtn.textContent = autoOn ? "\\u23F8 auto" : "\\u25B6 auto";
|
|
697
|
+
for (const c of characters) {
|
|
698
|
+
const ticker = tickers[c.id];
|
|
699
|
+
if (!ticker) continue;
|
|
700
|
+
if (autoOn) ticker.play(); else ticker.pause();
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
delaySlider.addEventListener("input", function () {
|
|
704
|
+
delayMs = Number(delaySlider.value);
|
|
705
|
+
el("delayValue").textContent = delayMs + "ms";
|
|
706
|
+
});
|
|
707
|
+
maxTurnsSlider.addEventListener("input", function () {
|
|
708
|
+
maxTurns = Number(maxTurnsSlider.value);
|
|
709
|
+
el("maxTurnsValue").textContent = String(maxTurns);
|
|
710
|
+
});
|
|
711
|
+
resetBtn.addEventListener("click", function () { boot(); });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function updateGlobalTurnLabel() {
|
|
715
|
+
el("globalTurnCount").textContent = "turns: " + globalTurn;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function boot() {
|
|
719
|
+
for (const c of characters) {
|
|
720
|
+
const ticker = tickers[c.id];
|
|
721
|
+
if (ticker) ticker.pause();
|
|
722
|
+
}
|
|
723
|
+
globalTurn = 0;
|
|
724
|
+
knownRoomIds.clear();
|
|
725
|
+
freshlyDugRoom = null;
|
|
726
|
+
speechBubbles.clear();
|
|
727
|
+
tickChain = Promise.resolve();
|
|
728
|
+
for (const c of characters) {
|
|
729
|
+
const w = "window-" + c.slot;
|
|
730
|
+
el(w + "-chatlog").innerHTML = "";
|
|
731
|
+
}
|
|
732
|
+
session = await window.tmctMud.createMudSession(DATA.worldPayload, { characters: characterIds });
|
|
733
|
+
for (const c of characters) {
|
|
734
|
+
const w = "window-" + c.slot;
|
|
735
|
+
el(w + "-play").disabled = false;
|
|
736
|
+
el(w + "-step").disabled = false;
|
|
737
|
+
el(w + "-chatq").disabled = false;
|
|
738
|
+
for (const pill of el(w + "-chatpills").querySelectorAll(".pill")) pill.disabled = false;
|
|
739
|
+
ensureTicker(c.id);
|
|
740
|
+
wireChat(c.id);
|
|
741
|
+
}
|
|
742
|
+
updateGlobalTurnLabel();
|
|
743
|
+
renderAll();
|
|
744
|
+
// Default on load: only the NW window plays, the rest start paused.
|
|
745
|
+
const nw = characters.find(function (c) { return c.slot === "nw"; });
|
|
746
|
+
if (nw) tickers[nw.id].play();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
wireRail();
|
|
750
|
+
boot();
|
|
751
|
+
})();`;
|
|
752
|
+
}
|