@polycode-projects/the-mechanical-code-talker 5.0.0 → 5.0.2
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/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +35 -5
- package/corpus/worlds/shards/town-square-chapel.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square-market.jsonl.gz +0 -0
- package/corpus/worlds/shards/town-square.jsonl.gz +0 -0
- package/corpus/worlds/src/town-square-chapel.jsonl +929 -0
- package/corpus/worlds/src/town-square-market.jsonl +455 -0
- package/corpus/worlds/src/town-square.jsonl +687 -0
- package/package.json +5 -3
- package/src/domain/inflect.mjs +67 -0
- package/src/domain/memory/compaction.mjs +2 -0
- package/src/domain/town-square-world.mjs +416 -0
- package/src/services/chat.mjs +24 -0
- package/src/services/mud-editor.mjs +27 -0
- package/src/services/mudiii-scene.mjs +766 -0
- package/src/services/mudiii-turn.mjs +670 -0
- package/src/services/mudiii-viz.mjs +1247 -0
- package/src/services/predator-prey.mjs +916 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -127
- package/src/surfaces/web/mudiii-browser-entry.mjs +224 -0
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
// mudiii-scene.mjs — the town square's own three.js scene, reached from
|
|
2
|
+
// mudiii-viz.mjs (the page shell) through exactly one frozen function:
|
|
3
|
+
// `mudiiiSceneScript(opts) -> string`, a standalone inline <script> the page
|
|
4
|
+
// embeds next to its own (see mudiii-viz.mjs's own header on the split: two
|
|
5
|
+
// separate IIFEs, no shared lexical scope). This module owns the 3D scene
|
|
6
|
+
// only — the deck, the HUD, the chat lane and the map panel are the page
|
|
7
|
+
// shell's.
|
|
8
|
+
//
|
|
9
|
+
// Built from exactly two kinds of input, holding no world state of its own:
|
|
10
|
+
// the prop facts once at boot, and the tick payload every tick. Everything
|
|
11
|
+
// this file renders is a read of one of those two — never a second, locally
|
|
12
|
+
// invented notion of where an agent stands.
|
|
13
|
+
//
|
|
14
|
+
// PLAN_MUD_MUDIII.md's own render-layer section is corrected in two places
|
|
15
|
+
// this file follows rather than the doc's original text: GLTFLoader ships in
|
|
16
|
+
// the vendor bundle (handles KHR_mesh_quantization and EXT_texture_webp
|
|
17
|
+
// natively; only EXT_meshopt_compression needs a decoder line), and there is
|
|
18
|
+
// no globalThis.tmctMudiii — pages publish one globalThis.tmct.
|
|
19
|
+
//
|
|
20
|
+
// The page-shell <-> scene handshake, since the two scripts share no scope:
|
|
21
|
+
// - the page shell already defines `window.mudiiiHandleSceneClick(cellId)`
|
|
22
|
+
// (mudiii-viz.mjs) — this file calls it on a raycast hit and owns no
|
|
23
|
+
// other write path into the world; "one code path, one provenance stamp"
|
|
24
|
+
// means the click is never resolved here.
|
|
25
|
+
// - this file defines `window.mudiiiScene`, the reverse direction:
|
|
26
|
+
// .boot({ propPlacements, assetManifest, gridSize?, cellSize? })
|
|
27
|
+
// once per world (fresh load, reset, scenario switch) — assetManifest
|
|
28
|
+
// is data/mudiii-assets.json's own `assets` rows; propPlacements is
|
|
29
|
+
// mudiii-viz.mjs's own propPlacementsFrom(...) output (each entry
|
|
30
|
+
// already carries its resolved `.asset` row). gridSize/cellSize
|
|
31
|
+
// override the values baked into this script at build time, for a
|
|
32
|
+
// scenario whose own board differs from the opening one.
|
|
33
|
+
// .applyTick({ agents, items, ecology }) every tick, the exact shape
|
|
34
|
+
// test/fixtures/mudiii-ticks.json's own _readme.tickPayload
|
|
35
|
+
// documents (agents/items keyed by id, ecology a tagged-union
|
|
36
|
+
// array).
|
|
37
|
+
// .setCamera({ mode, selectedId }) whenever the page's own camera
|
|
38
|
+
// state changes — a mode button, the agent-select dropdown, or a
|
|
39
|
+
// nextCameraSelection fallback the page shell already computes and
|
|
40
|
+
// writes into #sceneStatus.
|
|
41
|
+
// .cellOf(id) — read-only, the AUTHORITATIVE stored cell for a live
|
|
42
|
+
// agent or item, never the mid-lerp position. This is what an e2e
|
|
43
|
+
// assertion reads, and what a later screenshot ready-check reads too
|
|
44
|
+
// (see test-e2e/pages-mudiii.test.mjs's own header).
|
|
45
|
+
// .ready() — whether boot() has finished at least once.
|
|
46
|
+
// These three calls are not yet wired into mudiii-viz.mjs's own
|
|
47
|
+
// boot()/applyTickResult()/camera handlers — that file is owned by the viz
|
|
48
|
+
// track, not this one; the coordinator's own report names the exact call
|
|
49
|
+
// sites needed, mirroring how window.mudiiiHandleSceneClick was already
|
|
50
|
+
// added for the reverse direction.
|
|
51
|
+
//
|
|
52
|
+
// Reused from mudiii-viz.mjs rather than re-derived, off its own frozen
|
|
53
|
+
// exports: `roleOfAgentId`, `cellToWorld`, `cellFromGroundPoint`,
|
|
54
|
+
// `cameraRigFor`, `clipForAction`. The two files import each other by design,
|
|
55
|
+
// which a static cycle handles: every binding crossing it is a hoisted
|
|
56
|
+
// `export function` declaration, a live binding available at link time, and
|
|
57
|
+
// nothing calls one while either module's top-level body is still running.
|
|
58
|
+
//
|
|
59
|
+
// It must stay a static import. A top-level `await import()` on both sides
|
|
60
|
+
// turns the same cycle into a deadlock — each module waits for the other to
|
|
61
|
+
// finish evaluating, neither ever does, and the symptom is a silent hang with
|
|
62
|
+
// no error rather than a resolution failure.
|
|
63
|
+
import {
|
|
64
|
+
roleOfAgentId, cellToWorld, cellFromGroundPoint, cameraRigFor, clipForAction, modelUrlFor,
|
|
65
|
+
} from "./mudiii-viz.mjs";
|
|
66
|
+
import { prefersReducedMotion } from "./viz-ticker.mjs";
|
|
67
|
+
|
|
68
|
+
/** The movement/camera tween's duration under normal motion — three.js has
|
|
69
|
+
* no CSS transition, so this is the explicit lerp duration spider-fly-viz.mjs's
|
|
70
|
+
* own `transition: left .25s ease` becomes in a requestAnimationFrame loop. */
|
|
71
|
+
export const TWEEN_DURATION_MS = 250;
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Pure geometry/tween helpers — unit-tested directly in Node, then spliced
|
|
75
|
+
// via `.toString()` into the standalone browser IIFE below, the same
|
|
76
|
+
// splice-safe convention viz-boot.mjs/viz-ticker.mjs already establish.
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/** Linear interpolation between `a` and `b` at `t` (0..1), no clamping — a
|
|
80
|
+
* caller past the tween's own duration already clamps `t` to 1 upstream. */
|
|
81
|
+
export function lerp(a, b, t) {
|
|
82
|
+
return a + (b - a) * t;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One tween's position at `now`: `{ from: {..fields}, to: {..fields},
|
|
86
|
+
* startedAt, durationMs }`, every field in `to` interpolated the same way
|
|
87
|
+
* (works for `{x,z}` ground positions and `{x,y,z}` camera points alike).
|
|
88
|
+
* Returns `{ ...point, t, done }` — `t` is the eased-nothing linear
|
|
89
|
+
* fraction, `done` is whether `now` has reached the tween's own end.
|
|
90
|
+
* `durationMs <= 0` (the reduced-motion collapse) jumps straight to `to`
|
|
91
|
+
* regardless of `now` — invariant 3 of the movement contract. Pure. */
|
|
92
|
+
export function tweenStep(tween, now) {
|
|
93
|
+
if (!tween) return null;
|
|
94
|
+
const { from, to, startedAt, durationMs } = tween;
|
|
95
|
+
if (!durationMs || durationMs <= 0) return { ...to, t: 1, done: true };
|
|
96
|
+
const elapsed = now - startedAt;
|
|
97
|
+
if (elapsed <= 0) return { ...from, t: 0, done: false };
|
|
98
|
+
if (elapsed >= durationMs) return { ...to, t: 1, done: true };
|
|
99
|
+
const t = elapsed / durationMs;
|
|
100
|
+
const point = {};
|
|
101
|
+
for (const key of Object.keys(to)) point[key] = lerp(from[key] ?? to[key], to[key], t);
|
|
102
|
+
return { ...point, t, done: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A fresh tween from `from` to `to`, starting at `now`. Pure. */
|
|
106
|
+
export function startTween(from, to, now, durationMs = TWEEN_DURATION_MS) {
|
|
107
|
+
return { from, to, startedAt: now, durationMs };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Invariant 2 of the movement contract: a tick arriving mid-lerp re-seeds
|
|
111
|
+
* the start from wherever the mesh CURRENTLY is, not from the old tween's
|
|
112
|
+
* own start or end. `existingTween` may be null (the agent was not
|
|
113
|
+
* tweening); `to` is the new destination point. Pure — `tweenStep` is
|
|
114
|
+
* called on the OLD tween at `now` to read the current position before
|
|
115
|
+
* building the new one. */
|
|
116
|
+
export function reseedTween(existingTween, to, now, durationMs = TWEEN_DURATION_MS) {
|
|
117
|
+
const current = existingTween ? tweenStep(existingTween, now) : null;
|
|
118
|
+
const from = current ? Object.fromEntries(Object.keys(to).map((k) => [k, current[k]])) : to;
|
|
119
|
+
return startTween(from, to, now, durationMs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Chebyshev distance between two `cell-<x>-<y>` ids, or `Infinity` for a
|
|
123
|
+
* malformed id — used only to tell a one-cell hop (tweened) apart from a
|
|
124
|
+
* multi-cell move or teleport (snapped, spawn flourish, no path animation —
|
|
125
|
+
* the movement contract's third invariant). Pure. */
|
|
126
|
+
export function chebyshevDistanceBetweenCells(a, b) {
|
|
127
|
+
const ma = /^cell-(\d+)-(\d+)$/.exec(String(a == null ? "" : a));
|
|
128
|
+
const mb = /^cell-(\d+)-(\d+)$/.exec(String(b == null ? "" : b));
|
|
129
|
+
if (!ma || !mb) return Infinity;
|
|
130
|
+
return Math.max(Math.abs(Number(ma[1]) - Number(mb[1])), Math.abs(Number(ma[2]) - Number(mb[2])));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A facing word to a Y rotation in radians, `south` (the world's own
|
|
134
|
+
* `defaultFacing`) at zero — matches `cameraRigFor`'s own FACING_VECTOR
|
|
135
|
+
* convention (`south: {x:0,z:1}`, `north: {x:0,z:-1}`, `east: {x:1,z:0}`,
|
|
136
|
+
* `west: {x:-1,z:0}`) so an agent mesh turns to face the same way its own
|
|
137
|
+
* follow/pov camera rig would sit behind it. A rig's own forward axis may
|
|
138
|
+
* need a per-model offset once real assets are in place; this is the
|
|
139
|
+
* world-facing convention, not a guarantee about any one GLB's neutral
|
|
140
|
+
* pose. Pure. */
|
|
141
|
+
export function yawForFacing(facing) {
|
|
142
|
+
const YAW = { south: 0, north: Math.PI, east: -Math.PI / 2, west: Math.PI / 2 };
|
|
143
|
+
return YAW[facing] ?? YAW.south;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether `agentId` (predator or prey) currently believes an agent of the
|
|
147
|
+
* opposing role — the one belief-derived bit that separates a predator's
|
|
148
|
+
* "chase" rung from "wander", and a prey's "evade" rung from "forage"/
|
|
149
|
+
* "wander" (PLAN_MUD_MUDIII.md's own priority chains). Read entirely off
|
|
150
|
+
* the tick payload's own `role`/`belief` fields — no rung name travels on
|
|
151
|
+
* the wire, so this reconstructs which rung applied rather than trusting a
|
|
152
|
+
* label. Pure. */
|
|
153
|
+
export function threatEngagedFor(agentId, agentsById) {
|
|
154
|
+
const agent = agentsById[agentId];
|
|
155
|
+
if (!agent || !agent.belief) return false;
|
|
156
|
+
const opposingRole = agent.role === "predator" ? "prey" : agent.role === "prey" ? "predator" : null;
|
|
157
|
+
if (!opposingRole) return false;
|
|
158
|
+
return Object.keys(agentsById).some(
|
|
159
|
+
(id) => id !== agentId && agentsById[id] && agentsById[id].role === opposingRole && agent.belief[id],
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The movement rung driving clip selection while `agentId` is actually
|
|
164
|
+
* moving: "chase" for a predator that believes prey, "evade" for a prey
|
|
165
|
+
* that believes a predator, "wander" otherwise (forage and wander both
|
|
166
|
+
* resolve to the same walk clip in `clipForAction`'s own table, so this
|
|
167
|
+
* never needs to tell them apart). Pure. */
|
|
168
|
+
export function movementRungFor(agentId, agentsById) {
|
|
169
|
+
const agent = agentsById[agentId];
|
|
170
|
+
if (!agent) return "wander";
|
|
171
|
+
if (threatEngagedFor(agentId, agentsById)) return agent.role === "predator" ? "chase" : "evade";
|
|
172
|
+
return "wander";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The clip-selection ACTION word for one agent this tick: "idle" whenever
|
|
176
|
+
* it is not tweening (invariant 1 of the animation rule — "walk while the
|
|
177
|
+
* tween runs, idle otherwise"), `movementRungFor`'s own result while it is.
|
|
178
|
+
* Pure, and independent of `clipForAction` itself — this only decides which
|
|
179
|
+
* action word to ask for, not which literal clip name answers it. */
|
|
180
|
+
export function currentActionFor(agentId, agentsById, moving) {
|
|
181
|
+
return moving ? movementRungFor(agentId, agentsById) : "idle";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** One ecology event to the flourish it drives, or null for an event this
|
|
185
|
+
* scene has nothing visual to do for on its own (`spawn-food`/`spawn-prey`/
|
|
186
|
+
* `place-food` need no separate action here — the agent/item already gets
|
|
187
|
+
* its spawn flourish the moment it is new to a tick's own agents/items map,
|
|
188
|
+
* so acting on the ecology row too would double it). `kind` is one of
|
|
189
|
+
* "death" (the rig's own Death clip, then scale to zero) or "consume" (an
|
|
190
|
+
* eaten item scales to zero, no clip — items are primitive geometry).
|
|
191
|
+
* Pure. */
|
|
192
|
+
export function flourishForEcologyEvent(event) {
|
|
193
|
+
if (!event || !event.type) return null;
|
|
194
|
+
switch (event.type) {
|
|
195
|
+
case "eat-agent":
|
|
196
|
+
return { kind: "death", targetId: event.prey, cell: event.cell };
|
|
197
|
+
case "starve":
|
|
198
|
+
return { kind: "death", targetId: event.agent, cell: event.cell };
|
|
199
|
+
case "eat-item":
|
|
200
|
+
return { kind: "consume", targetId: event.item, cell: event.cell };
|
|
201
|
+
default:
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A bounded-concurrency job queue — three.js's own recommendation for a
|
|
207
|
+
* GLTFLoader shared across many models is to not open every request at
|
|
208
|
+
* once. `run(fn)` queues `fn` (returning a promise) and resolves/rejects
|
|
209
|
+
* with its own settlement once a slot frees up; at most `limit` run at a
|
|
210
|
+
* time. Pure enough to unit-test with fake async jobs, no three.js needed. */
|
|
211
|
+
export function createConcurrencyQueue(limit) {
|
|
212
|
+
let active = 0;
|
|
213
|
+
const waiting = [];
|
|
214
|
+
function pump() {
|
|
215
|
+
if (active >= limit || waiting.length === 0) return;
|
|
216
|
+
active += 1;
|
|
217
|
+
const { fn, resolve, reject } = waiting.shift();
|
|
218
|
+
fn().then(
|
|
219
|
+
(value) => { active -= 1; resolve(value); pump(); },
|
|
220
|
+
(err) => { active -= 1; reject(err); pump(); },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function run(fn) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
waiting.push({ fn, resolve, reject });
|
|
226
|
+
pump();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return { run };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** A promise cache keyed by whatever key `loadFn` is called with, WITH
|
|
233
|
+
* rejected-promise eviction: a failed load is removed from the cache the
|
|
234
|
+
* moment it rejects, so the next call for the same key retries instead of
|
|
235
|
+
* replaying the same failure forever (world-of-claudecraft's own "black
|
|
236
|
+
* void" bug this discipline exists to avoid — a cached rejection reads as a
|
|
237
|
+
* model that will never load, when the real fix might be one dropped
|
|
238
|
+
* request away). A resolved entry stays cached for the life of the page.
|
|
239
|
+
* Pure enough to unit-test with a fake `loadFn`. */
|
|
240
|
+
export function createCachedLoader(loadFn) {
|
|
241
|
+
const cache = new Map();
|
|
242
|
+
function load(key) {
|
|
243
|
+
if (cache.has(key)) return cache.get(key);
|
|
244
|
+
const promise = Promise.resolve().then(() => loadFn(key));
|
|
245
|
+
promise.catch(() => cache.delete(key));
|
|
246
|
+
cache.set(key, promise);
|
|
247
|
+
return promise;
|
|
248
|
+
}
|
|
249
|
+
return { load };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The memoized, timeout-raced, never-throwing lazy-vendor-loader pattern
|
|
253
|
+
* viz-boot.mjs's `loadWinkVendor` establishes, followed here for
|
|
254
|
+
* `./vendor/three.js` rather than `./vendor/wink.js` — a different vendor
|
|
255
|
+
* shape (`{ THREE, GLTFLoader, OrbitControls, MeshoptDecoder }`, not
|
|
256
|
+
* `{ winkNLP, model }`), so this is a sibling implementation of the same
|
|
257
|
+
* shape rather than a call into that one. Resolves `{ ok: true, THREE,
|
|
258
|
+
* GLTFLoader, OrbitControls, MeshoptDecoder }` on success or `{ ok: false,
|
|
259
|
+
* error }` on a timeout or load failure — never throws, never blocks the
|
|
260
|
+
* rest of the page on a stalled 3D asset. `importVendor` is injectable so a
|
|
261
|
+
* test can supply a resolved/rejected/never-settling promise with no real
|
|
262
|
+
* vendor asset on disk. */
|
|
263
|
+
export function createThreeVendorLoader({
|
|
264
|
+
timeoutMs = 15000,
|
|
265
|
+
importVendor = () => import("./vendor/three.js"),
|
|
266
|
+
} = {}) {
|
|
267
|
+
let ready = null;
|
|
268
|
+
return function loadThreeVendorOnce() {
|
|
269
|
+
if (ready) return ready;
|
|
270
|
+
ready = (async () => {
|
|
271
|
+
let timer;
|
|
272
|
+
const timeout = new Promise((_, reject) => {
|
|
273
|
+
timer = setTimeout(() => reject(new Error("three vendor asset load timed out")), timeoutMs);
|
|
274
|
+
});
|
|
275
|
+
try {
|
|
276
|
+
const mod = await Promise.race([importVendor(), timeout]);
|
|
277
|
+
return {
|
|
278
|
+
ok: true, THREE: mod.THREE, GLTFLoader: mod.GLTFLoader,
|
|
279
|
+
OrbitControls: mod.OrbitControls, MeshoptDecoder: mod.MeshoptDecoder,
|
|
280
|
+
};
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// eslint-disable-next-line no-console
|
|
283
|
+
console.warn("tmct: the three.js vendor asset failed to load, the town square cannot render", error);
|
|
284
|
+
return { ok: false, error };
|
|
285
|
+
} finally {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
return ready;
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// The frozen contract.
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
/** The town square's own standalone <script> source, a self-contained IIFE
|
|
298
|
+
* with no lexical scope shared with the page shell (see this module's own
|
|
299
|
+
* header on the handshake). `opts`:
|
|
300
|
+
* - `canvasId`/`statusId`: the page shell's own element ids
|
|
301
|
+
* (`#sceneCanvas`, `#sceneStatus` in mudiii-viz.mjs's markup) this
|
|
302
|
+
* script mounts the renderer into and writes a load failure to.
|
|
303
|
+
* - `gridSize`/`cellSize`: the OPENING scenario's own board size, baked
|
|
304
|
+
* in at page-build time; `window.mudiiiScene.boot()` accepts its own
|
|
305
|
+
* `gridSize`/`cellSize` to override this for a later scenario whose
|
|
306
|
+
* board differs, since `opts` itself is fixed once the page is built.
|
|
307
|
+
* Pure — identical output for identical input; nothing here reads the DOM
|
|
308
|
+
* or three.js until the returned string actually runs in a browser. */
|
|
309
|
+
export function mudiiiSceneScript({ canvasId, statusId, gridSize, cellSize } = {}) {
|
|
310
|
+
return `(function () {
|
|
311
|
+
"use strict";
|
|
312
|
+
var CANVAS_ID = ${JSON.stringify(canvasId)};
|
|
313
|
+
var STATUS_ID = ${JSON.stringify(statusId)};
|
|
314
|
+
var GRID_SIZE = ${JSON.stringify(Number(gridSize) || 12)};
|
|
315
|
+
var CELL_SIZE = ${JSON.stringify(Number(cellSize) || 1)};
|
|
316
|
+
|
|
317
|
+
var lerp = ${lerp.toString()};
|
|
318
|
+
var tweenStep = ${tweenStep.toString()};
|
|
319
|
+
var startTween = ${startTween.toString()};
|
|
320
|
+
var reseedTween = ${reseedTween.toString()};
|
|
321
|
+
var chebyshevDistanceBetweenCells = ${chebyshevDistanceBetweenCells.toString()};
|
|
322
|
+
var yawForFacing = ${yawForFacing.toString()};
|
|
323
|
+
var threatEngagedFor = ${threatEngagedFor.toString()};
|
|
324
|
+
var movementRungFor = ${movementRungFor.toString()};
|
|
325
|
+
var currentActionFor = ${currentActionFor.toString()};
|
|
326
|
+
var flourishForEcologyEvent = ${flourishForEcologyEvent.toString()};
|
|
327
|
+
var createConcurrencyQueue = ${createConcurrencyQueue.toString()};
|
|
328
|
+
var createCachedLoader = ${createCachedLoader.toString()};
|
|
329
|
+
var createThreeVendorLoader = ${createThreeVendorLoader.toString()};
|
|
330
|
+
var prefersReducedMotion = ${prefersReducedMotion.toString()};
|
|
331
|
+
var roleOfAgentId = ${roleOfAgentId.toString()};
|
|
332
|
+
var cellToWorld = ${cellToWorld.toString()};
|
|
333
|
+
var cellFromGroundPoint = ${cellFromGroundPoint.toString()};
|
|
334
|
+
var cameraRigFor = ${cameraRigFor.toString()};
|
|
335
|
+
var clipForAction = ${clipForAction.toString()};
|
|
336
|
+
var modelUrlFor = ${modelUrlFor.toString()};
|
|
337
|
+
|
|
338
|
+
var canvas = document.getElementById(CANVAS_ID);
|
|
339
|
+
var statusEl = document.getElementById(STATUS_ID);
|
|
340
|
+
if (!canvas) return;
|
|
341
|
+
|
|
342
|
+
function setStatus(text) { if (statusEl) statusEl.textContent = text || ""; }
|
|
343
|
+
|
|
344
|
+
var reduced = prefersReducedMotion();
|
|
345
|
+
var tweenDurationMs = reduced ? 0 : ${TWEEN_DURATION_MS};
|
|
346
|
+
if (typeof window.matchMedia === "function") {
|
|
347
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener("change", function (e) {
|
|
348
|
+
reduced = e.matches;
|
|
349
|
+
tweenDurationMs = reduced ? 0 : ${TWEEN_DURATION_MS};
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
var loadThreeVendor = createThreeVendorLoader();
|
|
354
|
+
var THREE = null, GLTFLoaderCtor = null, OrbitControlsCtor = null, MeshoptDecoderRef = null;
|
|
355
|
+
var gltfLoader = null, loadQueue = createConcurrencyQueue(4), rawLoader = null;
|
|
356
|
+
var scene = null, camera3 = null, renderer = null, orbitControls = null, groundMesh = null, raycaster = null;
|
|
357
|
+
var agentGroups = {};
|
|
358
|
+
var itemMeshes = {};
|
|
359
|
+
var propTemplates = {};
|
|
360
|
+
var propInstances = [];
|
|
361
|
+
var manifestByKind = {};
|
|
362
|
+
var lastAgentsById = {};
|
|
363
|
+
var lastItemsById = {};
|
|
364
|
+
var cameraState = { mode: "overhead", selectedId: null };
|
|
365
|
+
var cameraTween = null, lookAtTween = null;
|
|
366
|
+
var lastFrameTs = null;
|
|
367
|
+
var booted = false;
|
|
368
|
+
|
|
369
|
+
function loadGlbRaw(url) {
|
|
370
|
+
return loadQueue.run(function () {
|
|
371
|
+
return new Promise(function (resolve, reject) {
|
|
372
|
+
gltfLoader.load(url, resolve, undefined, reject);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
var glbCache = null;
|
|
377
|
+
function loadGlb(url) { return glbCache.load(url); }
|
|
378
|
+
|
|
379
|
+
async function ensureThree() {
|
|
380
|
+
if (THREE) return true;
|
|
381
|
+
var result = await loadThreeVendor();
|
|
382
|
+
if (!result.ok) { setStatus("the town square could not load its 3D scene"); return false; }
|
|
383
|
+
THREE = result.THREE; GLTFLoaderCtor = result.GLTFLoader;
|
|
384
|
+
OrbitControlsCtor = result.OrbitControls; MeshoptDecoderRef = result.MeshoptDecoder;
|
|
385
|
+
gltfLoader = new GLTFLoaderCtor();
|
|
386
|
+
gltfLoader.setMeshoptDecoder(MeshoptDecoderRef);
|
|
387
|
+
glbCache = createCachedLoader(loadGlbRaw);
|
|
388
|
+
setUpScene();
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function setUpScene() {
|
|
393
|
+
scene = new THREE.Scene();
|
|
394
|
+
scene.background = new THREE.Color(0x10161b);
|
|
395
|
+
camera3 = new THREE.PerspectiveCamera(55, (canvas.clientWidth || 640) / Math.max(1, canvas.clientHeight || 360), 0.1, 500);
|
|
396
|
+
camera3.position.set(0, 8, 8);
|
|
397
|
+
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
|
|
398
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.2));
|
|
399
|
+
var sun = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
400
|
+
sun.position.set(5, 10, 3);
|
|
401
|
+
scene.add(sun);
|
|
402
|
+
|
|
403
|
+
rebuildGround();
|
|
404
|
+
|
|
405
|
+
raycaster = new THREE.Raycaster();
|
|
406
|
+
orbitControls = new OrbitControlsCtor(camera3, renderer.domElement);
|
|
407
|
+
orbitControls.enabled = false;
|
|
408
|
+
orbitControls.target.set(0, 0, 0);
|
|
409
|
+
|
|
410
|
+
canvas.addEventListener("pointerdown", onPointerDown);
|
|
411
|
+
window.addEventListener("resize", onResize);
|
|
412
|
+
watchFoodPill();
|
|
413
|
+
onResize();
|
|
414
|
+
requestAnimationFrame(renderLoop);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function rebuildGround() {
|
|
418
|
+
if (groundMesh) { scene.remove(groundMesh); groundMesh.geometry.dispose(); groundMesh.material.dispose(); }
|
|
419
|
+
var groundSize = GRID_SIZE * CELL_SIZE;
|
|
420
|
+
var groundMat = new THREE.MeshStandardMaterial({ color: 0x7c9a5b });
|
|
421
|
+
groundMesh = new THREE.Mesh(new THREE.PlaneGeometry(groundSize, groundSize), groundMat);
|
|
422
|
+
groundMesh.name = "ground";
|
|
423
|
+
groundMesh.rotation.x = -Math.PI / 2;
|
|
424
|
+
scene.add(groundMesh);
|
|
425
|
+
var existingGrid = scene.getObjectByName("townSquareGrid");
|
|
426
|
+
if (existingGrid) scene.remove(existingGrid);
|
|
427
|
+
var grid = new THREE.GridHelper(groundSize, GRID_SIZE);
|
|
428
|
+
grid.name = "townSquareGrid";
|
|
429
|
+
grid.position.y = 0.01;
|
|
430
|
+
scene.add(grid);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function onResize() {
|
|
434
|
+
if (!renderer || !camera3) return;
|
|
435
|
+
var w = canvas.clientWidth || 640, h = canvas.clientHeight || 360;
|
|
436
|
+
renderer.setSize(w, h, false);
|
|
437
|
+
camera3.aspect = w / Math.max(1, h);
|
|
438
|
+
camera3.updateProjectionMatrix();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function pointFromEvent(evt) {
|
|
442
|
+
var rect = canvas.getBoundingClientRect();
|
|
443
|
+
return {
|
|
444
|
+
x: ((evt.clientX - rect.left) / rect.width) * 2 - 1,
|
|
445
|
+
y: -((evt.clientY - rect.top) / rect.height) * 2 + 1,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// The raycast target is the ground mesh ALONE — never scene.children — so
|
|
450
|
+
// a click a hair off a prop or an agent still resolves to the cell under
|
|
451
|
+
// the cursor rather than hitting whatever mesh happens to sit in front.
|
|
452
|
+
function onPointerDown(evt) {
|
|
453
|
+
if (!scene || !camera3 || !groundMesh) return;
|
|
454
|
+
var ndc = pointFromEvent(evt);
|
|
455
|
+
raycaster.setFromCamera(ndc, camera3);
|
|
456
|
+
var hits = raycaster.intersectObject(groundMesh, false);
|
|
457
|
+
if (!hits.length) return;
|
|
458
|
+
var cellId = cellFromGroundPoint({ x: hits[0].point.x, z: hits[0].point.z }, GRID_SIZE, CELL_SIZE);
|
|
459
|
+
if (!cellId) return;
|
|
460
|
+
if (typeof window.mudiiiHandleSceneClick === "function") window.mudiiiHandleSceneClick(cellId);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Cursor feedback while the food pill is armed reads the pill's own
|
|
464
|
+
// aria-pressed attribute directly (a read-only coupling to a stable markup
|
|
465
|
+
// id the page shell already exposes) rather than needing a page-script
|
|
466
|
+
// edit just to mirror one boolean.
|
|
467
|
+
function watchFoodPill() {
|
|
468
|
+
var foodPill = document.getElementById("foodPill");
|
|
469
|
+
if (!foodPill || typeof MutationObserver === "undefined") return;
|
|
470
|
+
function sync() { canvas.style.cursor = foodPill.getAttribute("aria-pressed") === "true" ? "crosshair" : ""; }
|
|
471
|
+
sync();
|
|
472
|
+
new MutationObserver(sync).observe(foodPill, { attributes: true, attributeFilter: ["aria-pressed"] });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ---- props ---------------------------------------------------------------
|
|
476
|
+
function normalizeToHeight(object3D, targetHeight) {
|
|
477
|
+
var box = new THREE.Box3().setFromObject(object3D);
|
|
478
|
+
var size = new THREE.Vector3();
|
|
479
|
+
box.getSize(size);
|
|
480
|
+
var height = size.y || 1;
|
|
481
|
+
var scale = targetHeight && height ? targetHeight / height : 1;
|
|
482
|
+
object3D.scale.setScalar(scale);
|
|
483
|
+
var seated = new THREE.Box3().setFromObject(object3D);
|
|
484
|
+
object3D.position.y -= seated.min.y;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function loadPropTemplate(asset) {
|
|
488
|
+
var url = modelUrlFor(asset.destPath);
|
|
489
|
+
if (propTemplates[url]) return propTemplates[url];
|
|
490
|
+
var gltf = await loadGlb(url);
|
|
491
|
+
normalizeToHeight(gltf.scene, asset.targetHeight);
|
|
492
|
+
propTemplates[url] = gltf.scene;
|
|
493
|
+
return gltf.scene;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function placeProps(propPlacements) {
|
|
497
|
+
for (var i = 0; i < propInstances.length; i += 1) scene.remove(propInstances[i]);
|
|
498
|
+
propInstances = [];
|
|
499
|
+
for (var j = 0; j < (propPlacements || []).length; j += 1) {
|
|
500
|
+
var placement = propPlacements[j];
|
|
501
|
+
if (!placement.asset || !placement.asset.destPath) continue;
|
|
502
|
+
var world = cellToWorld(placement.cell, GRID_SIZE, CELL_SIZE);
|
|
503
|
+
if (!world) continue;
|
|
504
|
+
try {
|
|
505
|
+
var template = await loadPropTemplate(placement.asset);
|
|
506
|
+
var instance = template.clone();
|
|
507
|
+
instance.position.set(world.x, template.position.y, world.z);
|
|
508
|
+
instance.rotation.y = ((Number(placement.rotation) || 0) * Math.PI) / 180;
|
|
509
|
+
scene.add(instance);
|
|
510
|
+
propInstances.push(instance);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
// eslint-disable-next-line no-console
|
|
513
|
+
console.warn("tmct: a prop model failed to load", placement.model, err);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ---- items: crumbs and morsels, primitive geometry ------------------------
|
|
519
|
+
var CRUMB_RADIUS = 0.08, MORSEL_RADIUS = 0.18;
|
|
520
|
+
function itemGeometryFor(kind) { return new THREE.SphereGeometry(kind === "morsel" ? MORSEL_RADIUS : CRUMB_RADIUS, 10, 8); }
|
|
521
|
+
function itemMaterialFor(kind) { return new THREE.MeshStandardMaterial({ color: kind === "morsel" ? 0xd98a2b : 0x8c6a3f }); }
|
|
522
|
+
function itemHeightFor(kind) { return kind === "morsel" ? MORSEL_RADIUS : CRUMB_RADIUS; }
|
|
523
|
+
|
|
524
|
+
function animateScaleTo(object3D, target, now) {
|
|
525
|
+
var duration = tweenDurationMs;
|
|
526
|
+
var start = object3D.scale.x;
|
|
527
|
+
function step(ts) {
|
|
528
|
+
var elapsed = duration <= 0 ? duration : ts - now;
|
|
529
|
+
var t = duration <= 0 ? 1 : Math.min(1, Math.max(0, elapsed / duration));
|
|
530
|
+
var v = lerp(start, target, t);
|
|
531
|
+
object3D.scale.setScalar(v);
|
|
532
|
+
if (t < 1) requestAnimationFrame(step);
|
|
533
|
+
else if (target === 0 && object3D.parent) object3D.parent.remove(object3D);
|
|
534
|
+
}
|
|
535
|
+
requestAnimationFrame(step);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function applyItemTick(id, item, now) {
|
|
539
|
+
var world = cellToWorld(item.cell, GRID_SIZE, CELL_SIZE);
|
|
540
|
+
if (!world) return;
|
|
541
|
+
var entry = itemMeshes[id];
|
|
542
|
+
if (!entry) {
|
|
543
|
+
var mesh = new THREE.Mesh(itemGeometryFor(item.kind), itemMaterialFor(item.kind));
|
|
544
|
+
mesh.name = "item-" + id;
|
|
545
|
+
mesh.position.set(world.x, itemHeightFor(item.kind), world.z);
|
|
546
|
+
mesh.scale.setScalar(0);
|
|
547
|
+
scene.add(mesh);
|
|
548
|
+
entry = { mesh: mesh, cell: item.cell, kind: item.kind };
|
|
549
|
+
itemMeshes[id] = entry;
|
|
550
|
+
animateScaleTo(mesh, 1, now);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
entry.cell = item.cell;
|
|
554
|
+
entry.mesh.position.set(world.x, itemHeightFor(item.kind), world.z);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function removeItem(id, withFlourish) {
|
|
558
|
+
var entry = itemMeshes[id];
|
|
559
|
+
if (!entry) return;
|
|
560
|
+
delete itemMeshes[id];
|
|
561
|
+
if (withFlourish) animateScaleTo(entry.mesh, 0, performance.now());
|
|
562
|
+
else if (entry.mesh.parent) entry.mesh.parent.remove(entry.mesh);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---- agents: one persistent Group per live id, one GLTF INSTANCE per
|
|
566
|
+
// live agent (never a cloned template — Object3D.clone() does not
|
|
567
|
+
// correctly clone a SkinnedMesh, and at this roster size — a handful of
|
|
568
|
+
// foxes and goblins — the extra memory of a separate load per agent is
|
|
569
|
+
// trivial next to that whole class of bug). ---------------------------------
|
|
570
|
+
function ensureAgent(id, agent) {
|
|
571
|
+
if (agentGroups[id]) return agentGroups[id];
|
|
572
|
+
var kind = roleOfAgentId(id);
|
|
573
|
+
var entry = {
|
|
574
|
+
group: new THREE.Group(), tween: null, cell: null, facing: agent.facing, role: agent.role,
|
|
575
|
+
kind: kind, mixer: null, actions: {}, currentClip: null, clipMap: null,
|
|
576
|
+
};
|
|
577
|
+
entry.group.visible = false;
|
|
578
|
+
scene.add(entry.group);
|
|
579
|
+
agentGroups[id] = entry;
|
|
580
|
+
var asset = manifestByKind[kind];
|
|
581
|
+
if (asset) {
|
|
582
|
+
loadGlb(modelUrlFor(asset.destPath)).then(function (gltf) {
|
|
583
|
+
normalizeToHeight(gltf.scene, asset.targetHeight);
|
|
584
|
+
entry.group.add(gltf.scene);
|
|
585
|
+
entry.group.visible = true;
|
|
586
|
+
entry.clipMap = asset.clips || {};
|
|
587
|
+
entry.mixer = new THREE.AnimationMixer(gltf.scene);
|
|
588
|
+
for (var i = 0; i < (gltf.animations || []).length; i += 1) {
|
|
589
|
+
entry.actions[gltf.animations[i].name] = entry.mixer.clipAction(gltf.animations[i]);
|
|
590
|
+
}
|
|
591
|
+
}).catch(function (err) {
|
|
592
|
+
// eslint-disable-next-line no-console
|
|
593
|
+
console.warn("tmct: an agent model failed to load", id, err);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return entry;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function playClip(entry, clipName) {
|
|
600
|
+
if (!entry.mixer || !clipName || entry.currentClip === clipName) return;
|
|
601
|
+
var next = entry.actions[clipName];
|
|
602
|
+
if (!next) return;
|
|
603
|
+
var prev = entry.currentClip ? entry.actions[entry.currentClip] : null;
|
|
604
|
+
next.reset().fadeIn(0.15).play();
|
|
605
|
+
if (prev && prev !== next) prev.fadeOut(0.15);
|
|
606
|
+
entry.currentClip = clipName;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function playSpawnFlourish(group, now) {
|
|
610
|
+
group.scale.setScalar(0);
|
|
611
|
+
animateScaleTo(group, 1, now);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function applyAgentTick(id, agent, now) {
|
|
615
|
+
var entry = ensureAgent(id, agent);
|
|
616
|
+
var world = cellToWorld(agent.cell, GRID_SIZE, CELL_SIZE);
|
|
617
|
+
if (!world) return;
|
|
618
|
+
var previousCell = entry.cell;
|
|
619
|
+
var isFirstSighting = previousCell == null;
|
|
620
|
+
var singleHop = !isFirstSighting && previousCell !== agent.cell
|
|
621
|
+
&& chebyshevDistanceBetweenCells(previousCell, agent.cell) === 1;
|
|
622
|
+
var held = !isFirstSighting && previousCell === agent.cell;
|
|
623
|
+
entry.cell = agent.cell;
|
|
624
|
+
entry.facing = agent.facing;
|
|
625
|
+
entry.group.rotation.y = yawForFacing(agent.facing);
|
|
626
|
+
if (singleHop) {
|
|
627
|
+
entry.tween = reseedTween(entry.tween, { x: world.x, z: world.z }, now, tweenDurationMs);
|
|
628
|
+
} else {
|
|
629
|
+
entry.tween = null;
|
|
630
|
+
entry.group.position.set(world.x, entry.group.position.y, world.z);
|
|
631
|
+
if (!held) playSpawnFlourish(entry.group, now);
|
|
632
|
+
}
|
|
633
|
+
if (entry.clipMap) {
|
|
634
|
+
var moving = singleHop;
|
|
635
|
+
playClip(entry, clipForAction(agent.role, currentActionFor(id, lastAgentsById, moving), entry.clipMap));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function removeAgent(id) {
|
|
640
|
+
var entry = agentGroups[id];
|
|
641
|
+
if (!entry) return;
|
|
642
|
+
delete agentGroups[id];
|
|
643
|
+
var deathClip = entry.clipMap && entry.clipMap.death;
|
|
644
|
+
var deathClipName = Array.isArray(deathClip) ? deathClip[0] : deathClip;
|
|
645
|
+
if (deathClipName) {
|
|
646
|
+
playClip(entry, deathClipName);
|
|
647
|
+
animateScaleTo(entry.group, 0, performance.now());
|
|
648
|
+
} else if (entry.group.parent) {
|
|
649
|
+
entry.group.parent.remove(entry.group);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function applyEcology(ecology) {
|
|
654
|
+
for (var i = 0; i < (ecology || []).length; i += 1) {
|
|
655
|
+
var flourish = flourishForEcologyEvent(ecology[i]);
|
|
656
|
+
if (!flourish) continue;
|
|
657
|
+
if (flourish.kind === "death") removeAgent(flourish.targetId);
|
|
658
|
+
else if (flourish.kind === "consume") removeItem(flourish.targetId, true);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ---- camera ---------------------------------------------------------------
|
|
663
|
+
function agentSnapshotFor(id) {
|
|
664
|
+
var entry = agentGroups[id];
|
|
665
|
+
return entry ? { cell: entry.cell, facing: entry.facing } : null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function setCamera(state) {
|
|
669
|
+
cameraState = { mode: (state && state.mode) || "overhead", selectedId: (state && state.selectedId) || null };
|
|
670
|
+
var agent = cameraState.selectedId ? agentSnapshotFor(cameraState.selectedId) : null;
|
|
671
|
+
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE) || cameraRigFor("overhead", null, GRID_SIZE);
|
|
672
|
+
if (!rig) return;
|
|
673
|
+
var now = performance.now();
|
|
674
|
+
cameraTween = reseedTween(cameraTween, rig.position, now, tweenDurationMs);
|
|
675
|
+
lookAtTween = reseedTween(lookAtTween, rig.lookAt, now, tweenDurationMs);
|
|
676
|
+
if (orbitControls) orbitControls.enabled = cameraState.mode === "overhead";
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Re-tween the camera toward its own rig after a tick lands, in case the
|
|
680
|
+
// followed agent moved — follow/pov track the agent, so their own rig
|
|
681
|
+
// target changes every tick even with no explicit setCamera() call.
|
|
682
|
+
function refreshCameraTween(now) {
|
|
683
|
+
if (cameraState.mode === "overhead") return;
|
|
684
|
+
if (!cameraState.selectedId) return;
|
|
685
|
+
var agent = agentSnapshotFor(cameraState.selectedId);
|
|
686
|
+
var rig = cameraRigFor(cameraState.mode, agent, GRID_SIZE);
|
|
687
|
+
if (!rig) return;
|
|
688
|
+
cameraTween = reseedTween(cameraTween, rig.position, now, tweenDurationMs);
|
|
689
|
+
lookAtTween = reseedTween(lookAtTween, rig.lookAt, now, tweenDurationMs);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ---- boot / tick / render loop --------------------------------------------
|
|
693
|
+
function buildManifestByKind(assetManifest) {
|
|
694
|
+
var byKind = {};
|
|
695
|
+
for (var i = 0; i < (assetManifest || []).length; i += 1) {
|
|
696
|
+
if (assetManifest[i] && assetManifest[i].key) byKind[assetManifest[i].key] = assetManifest[i];
|
|
697
|
+
}
|
|
698
|
+
return byKind;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function boot(input) {
|
|
702
|
+
var ok = await ensureThree();
|
|
703
|
+
if (!ok) return;
|
|
704
|
+
if (input && input.gridSize) { GRID_SIZE = Number(input.gridSize) || GRID_SIZE; }
|
|
705
|
+
if (input && input.cellSize) { CELL_SIZE = Number(input.cellSize) || CELL_SIZE; }
|
|
706
|
+
rebuildGround();
|
|
707
|
+
for (var id in agentGroups) if (agentGroups[id].group.parent) agentGroups[id].group.parent.remove(agentGroups[id].group);
|
|
708
|
+
agentGroups = {};
|
|
709
|
+
for (var itemId in itemMeshes) if (itemMeshes[itemId].mesh.parent) itemMeshes[itemId].mesh.parent.remove(itemMeshes[itemId].mesh);
|
|
710
|
+
itemMeshes = {};
|
|
711
|
+
lastAgentsById = {};
|
|
712
|
+
lastItemsById = {};
|
|
713
|
+
manifestByKind = buildManifestByKind(input && input.assetManifest);
|
|
714
|
+
await placeProps((input && input.propPlacements) || []);
|
|
715
|
+
setCamera({ mode: "overhead", selectedId: null });
|
|
716
|
+
booted = true;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function applyTick(tick) {
|
|
720
|
+
if (!scene) return;
|
|
721
|
+
var now = performance.now();
|
|
722
|
+
var agents = (tick && tick.agents) || {};
|
|
723
|
+
var items = (tick && tick.items) || {};
|
|
724
|
+
for (var id in agents) if (Object.prototype.hasOwnProperty.call(agents, id)) applyAgentTick(id, agents[id], now);
|
|
725
|
+
for (var itemId in items) if (Object.prototype.hasOwnProperty.call(items, itemId)) applyItemTick(itemId, items[itemId], now);
|
|
726
|
+
for (var goneAgent in lastAgentsById) if (!(goneAgent in agents)) removeAgent(goneAgent);
|
|
727
|
+
for (var goneItem in lastItemsById) if (!(goneItem in items)) removeItem(goneItem, false);
|
|
728
|
+
applyEcology(tick && tick.ecology);
|
|
729
|
+
lastAgentsById = agents;
|
|
730
|
+
lastItemsById = items;
|
|
731
|
+
refreshCameraTween(now);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function renderLoop(ts) {
|
|
735
|
+
requestAnimationFrame(renderLoop);
|
|
736
|
+
if (!scene || !renderer || !camera3) return;
|
|
737
|
+
var deltaSec = lastFrameTs == null ? 0 : (ts - lastFrameTs) / 1000;
|
|
738
|
+
lastFrameTs = ts;
|
|
739
|
+
for (var id in agentGroups) {
|
|
740
|
+
var entry = agentGroups[id];
|
|
741
|
+
if (entry.tween) {
|
|
742
|
+
var p = tweenStep(entry.tween, ts);
|
|
743
|
+
entry.group.position.set(p.x, entry.group.position.y, p.z);
|
|
744
|
+
if (p.done) entry.tween = null;
|
|
745
|
+
}
|
|
746
|
+
if (entry.mixer) entry.mixer.update(deltaSec);
|
|
747
|
+
}
|
|
748
|
+
if (cameraTween) { var cp = tweenStep(cameraTween, ts); camera3.position.set(cp.x, cp.y, cp.z); }
|
|
749
|
+
if (lookAtTween) { var lp = tweenStep(lookAtTween, ts); camera3.lookAt(lp.x, lp.y, lp.z); }
|
|
750
|
+
if (orbitControls && orbitControls.enabled) orbitControls.update();
|
|
751
|
+
renderer.render(scene, camera3);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
window.mudiiiScene = {
|
|
755
|
+
boot: boot,
|
|
756
|
+
applyTick: applyTick,
|
|
757
|
+
setCamera: setCamera,
|
|
758
|
+
cellOf: function (id) {
|
|
759
|
+
if (agentGroups[id]) return agentGroups[id].cell;
|
|
760
|
+
if (itemMeshes[id]) return itemMeshes[id].cell;
|
|
761
|
+
return null;
|
|
762
|
+
},
|
|
763
|
+
ready: function () { return booted; },
|
|
764
|
+
};
|
|
765
|
+
})();`;
|
|
766
|
+
}
|