incanto 0.51.0 → 0.52.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.
Files changed (54) hide show
  1. package/dist/2d.d.ts +37 -2
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +3 -3
  4. package/dist/3d.js +4 -4
  5. package/dist/{behavior-rZNfzVbH.d.ts → behavior-uPEuZrUB.d.ts} +10 -0
  6. package/dist/{create-game-CkzKZ4v5.js → create-game-B_e9hJf7.js} +92 -10
  7. package/dist/{create-game-BNaOC3Px.js → create-game-CGnoypjL.js} +6 -6
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/debug.js +1 -1
  10. package/dist/{duplicate-EybyYeyL.js → duplicate-B-OtSRFL.js} +1 -1
  11. package/dist/editor.js +1917 -1579
  12. package/dist/{environment-presets-C08pOC6H.js → environment-presets-DSZwsKPs.js} +3 -3
  13. package/dist/{gameplay-bStgtZBV.js → gameplay-B6jqvYeM.js} +6 -4
  14. package/dist/gameplay.d.ts +1 -1
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.d.ts +65 -5
  17. package/dist/index.js +7 -7
  18. package/dist/{loader-B2asghWa.js → loader-B-Gft32x.js} +32 -32
  19. package/dist/{loader-Dkbn56KC.d.ts → loader-B9iTqs27.d.ts} +1 -1
  20. package/dist/net.d.ts +15 -410
  21. package/dist/net.js +1 -822
  22. package/dist/{pathfinding-Bz34pvQD.d.ts → pathfinding-CAR9DjQQ.d.ts} +1 -1
  23. package/dist/{physics-2d-Ccq2L9R0.js → physics-2d-Dns-oZlE.js} +2 -2
  24. package/dist/{physics-3d-B2c5KNYh.js → physics-3d-BhI0ehpe.js} +3 -3
  25. package/dist/react.d.ts +1 -1
  26. package/dist/react.js +1 -1
  27. package/dist/{register-CGq-Hee8.js → register-DVwlnZAZ.js} +2 -2
  28. package/dist/{register-B_BaaGx-.js → register-Trx7WHnD.js} +3 -3
  29. package/dist/{registry-IyWCGe4q.js → registry-C7u42TID.js} +23 -1
  30. package/dist/{replay-DIP2_as4.d.ts → replay-BU1CCM15.d.ts} +1 -1
  31. package/dist/{replay-BgKxGcXH.js → replay-BicPOMX0.js} +195 -2
  32. package/dist/split-screen-CZ9ccBBQ.js +1267 -0
  33. package/dist/split-screen-paxkQs_q.d.ts +441 -0
  34. package/dist/{src-C3UwzYXl.js → src-5gbZO47I.js} +1 -1
  35. package/dist/{teardown-BKTCzLek.js → teardown-ks3d5W9n.js} +2 -1
  36. package/dist/{test-D2gRpk5V.js → test-DAojuFdb.js} +119 -22
  37. package/dist/test.d.ts +60 -5
  38. package/dist/test.js +3 -3
  39. package/dist/vite.js +2 -2
  40. package/editor/assets/{agent8-DbX_msaO.js → agent8-CCvckvbw.js} +1 -1
  41. package/editor/assets/{debug-C-kMxdl1.js → debug-CLNCOnbc.js} +1 -1
  42. package/editor/assets/{index-CUc1U7wm.js → index-Cb5Brupb.js} +99 -92
  43. package/editor/index.html +1 -1
  44. package/package.json +1 -1
  45. package/skills/incanto-assets.md +4 -1
  46. package/skills/incanto-editor.md +48 -11
  47. package/skills/incanto-gameplay-behaviors.md +14 -5
  48. package/skills/incanto-multiplayer.md +39 -3
  49. package/skills/incanto-verifying-your-game.md +56 -0
  50. package/templates/agent8-server.ts +79 -2
  51. package/templates-app/beacon-isle-3d/package.json +1 -1
  52. package/templates-app/tps-3d/package.json +1 -1
  53. package/templates-app/village-quest-3d/package.json +1 -1
  54. package/dist/register-CLVhzWcI.js +0 -374
@@ -0,0 +1,1267 @@
1
+ import { C as diagnose, M as Signal, f as Node, n as loadScene, t as buildNodeJson } from "./loader-B-Gft32x.js";
2
+ import { h as Engine, t as registerCoreNodes } from "./register-DVwlnZAZ.js";
3
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
+ import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
+ import { l as registerNode } from "./registry-C7u42TID.js";
6
+ //#region src/net/loopback.ts
7
+ /**
8
+ * In-memory multiplayer hub implementing the SAME kernel contract as
9
+ * `templates/agent8-server.js` — local split-screen demos, offline development,
10
+ * and headless tests run real multi-client flows without any infrastructure.
11
+ */
12
+ var LoopbackHub = class {
13
+ rooms = /* @__PURE__ */ new Map();
14
+ accountSeq = 0;
15
+ roomSeq = 0;
16
+ entitySeq = 0;
17
+ /** Per-room listener registries, keyed by roomId. */
18
+ listeners = {
19
+ roomState: /* @__PURE__ */ new Map(),
20
+ allUsers: /* @__PURE__ */ new Map(),
21
+ myState: /* @__PURE__ */ new Map(),
22
+ collection: /* @__PURE__ */ new Map(),
23
+ message: /* @__PURE__ */ new Map(),
24
+ join: /* @__PURE__ */ new Map(),
25
+ leave: /* @__PURE__ */ new Map()
26
+ };
27
+ createClient(account) {
28
+ this.accountSeq += 1;
29
+ return new LoopbackTransport(this, account ?? `user${this.accountSeq}`);
30
+ }
31
+ /** @internal */
32
+ _room(roomId) {
33
+ let room = this.rooms.get(roomId);
34
+ if (!room) {
35
+ room = {
36
+ users: /* @__PURE__ */ new Map(),
37
+ state: {},
38
+ collections: /* @__PURE__ */ new Map()
39
+ };
40
+ this.rooms.set(roomId, room);
41
+ }
42
+ return room;
43
+ }
44
+ /** @internal The server.js kernel, in memory. */
45
+ _call(account, fn, args) {
46
+ switch (fn) {
47
+ case "joinRoom": {
48
+ const roomId = args[0] ?? `room${++this.roomSeq}`;
49
+ this._room(roomId).users.set(account, {});
50
+ this.fire(this.listeners.join.get(roomId), account);
51
+ this.fireAllUsers(roomId);
52
+ return roomId;
53
+ }
54
+ case "leaveRoom": {
55
+ const roomId = args[0];
56
+ this._room(roomId).users.delete(account);
57
+ this.fire(this.listeners.leave.get(roomId), account);
58
+ this.fireAllUsers(roomId);
59
+ return true;
60
+ }
61
+ case "setMyState": {
62
+ const [roomId, patch] = args;
63
+ const room = this._room(roomId);
64
+ const prev = room.users.get(account) ?? {};
65
+ room.users.set(account, {
66
+ ...prev,
67
+ ...jsonClone(patch)
68
+ });
69
+ for (const entry of this.listeners.myState.get(roomId) ?? []) if (entry.account === account) entry.cb(jsonClone(room.users.get(account) ?? {}));
70
+ this.fireAllUsers(roomId);
71
+ return true;
72
+ }
73
+ case "patchRoomState": {
74
+ const [roomId, patch] = args;
75
+ const room = this._room(roomId);
76
+ room.state = {
77
+ ...room.state,
78
+ ...jsonClone(patch)
79
+ };
80
+ this.fire(this.listeners.roomState.get(roomId), jsonClone(room.state));
81
+ return true;
82
+ }
83
+ case "addEntity": {
84
+ const [roomId, collectionId, entity] = args;
85
+ const room = this._room(roomId);
86
+ let coll = room.collections.get(collectionId);
87
+ if (!coll) {
88
+ coll = /* @__PURE__ */ new Map();
89
+ room.collections.set(collectionId, coll);
90
+ }
91
+ const id = entity.__id ?? `e${++this.entitySeq}`;
92
+ coll.set(id, {
93
+ ...jsonClone(entity),
94
+ __id: id
95
+ });
96
+ this.fireCollection(roomId, collectionId);
97
+ return id;
98
+ }
99
+ case "updateEntity": {
100
+ const [roomId, collectionId, id, patch] = args;
101
+ const coll = this._room(roomId).collections.get(collectionId);
102
+ const prev = coll?.get(id);
103
+ if (coll && prev) {
104
+ coll.set(id, {
105
+ ...prev,
106
+ ...jsonClone(patch),
107
+ __id: id
108
+ });
109
+ this.fireCollection(roomId, collectionId);
110
+ }
111
+ return true;
112
+ }
113
+ case "removeEntity": {
114
+ const [roomId, collectionId, id] = args;
115
+ this._room(roomId).collections.get(collectionId)?.delete(id);
116
+ this.fireCollection(roomId, collectionId);
117
+ return true;
118
+ }
119
+ case "sendEvent": {
120
+ const [roomId, type, payload] = args;
121
+ for (const entry of this.listeners.message.get(roomId) ?? []) if (entry.type === type) entry.cb(jsonClone(payload));
122
+ return true;
123
+ }
124
+ default: throw new IncantoError("BAD_FORMAT", `Loopback kernel has no remote function '${fn}'. Available: joinRoom, leaveRoom, setMyState, patchRoomState, addEntity, updateEntity, removeEntity, sendEvent.`);
125
+ }
126
+ }
127
+ /** @internal */
128
+ _subscribe(kind, roomId, entry) {
129
+ const registry = this.listeners[kind];
130
+ let set = registry.get(roomId);
131
+ if (!set) {
132
+ set = /* @__PURE__ */ new Set();
133
+ registry.set(roomId, set);
134
+ }
135
+ set.add(entry);
136
+ return () => set?.delete(entry);
137
+ }
138
+ /** @internal */
139
+ _snapshotUsers(roomId) {
140
+ return Object.fromEntries([...this._room(roomId).users.entries()].map(([a, s]) => [a, jsonClone(s)]));
141
+ }
142
+ /** @internal */
143
+ _snapshotCollection(roomId, collectionId) {
144
+ const coll = this._room(roomId).collections.get(collectionId);
145
+ return coll ? Object.fromEntries([...coll.entries()].map(([i, e]) => [i, jsonClone(e)])) : {};
146
+ }
147
+ /** @internal */
148
+ _snapshotRoomState(roomId) {
149
+ return jsonClone(this._room(roomId).state);
150
+ }
151
+ fire(set, value) {
152
+ for (const cb of set ?? []) cb(value);
153
+ }
154
+ fireAllUsers(roomId) {
155
+ const snapshot = this._snapshotUsers(roomId);
156
+ for (const cb of this.listeners.allUsers.get(roomId) ?? []) cb(snapshot);
157
+ }
158
+ fireCollection(roomId, collectionId) {
159
+ const snapshot = this._snapshotCollection(roomId, collectionId);
160
+ for (const entry of this.listeners.collection.get(roomId) ?? []) if (entry.id === collectionId) entry.cb(snapshot);
161
+ }
162
+ };
163
+ /** A client on a LoopbackHub. Subscriptions fire immediately with a snapshot. */
164
+ var LoopbackTransport = class {
165
+ hub;
166
+ account;
167
+ connected = false;
168
+ constructor(hub, account) {
169
+ this.hub = hub;
170
+ this.account = account;
171
+ }
172
+ connect() {
173
+ this.connected = true;
174
+ return Promise.resolve(true);
175
+ }
176
+ disconnect() {
177
+ this.connected = false;
178
+ return Promise.resolve();
179
+ }
180
+ remoteFunction(fn, args = []) {
181
+ return Promise.resolve(this.hub._call(this.account, fn, args));
182
+ }
183
+ subscribeRoomState(roomId, cb) {
184
+ const off = this.hub._subscribe("roomState", roomId, cb);
185
+ cb(this.hub._snapshotRoomState(roomId));
186
+ return off;
187
+ }
188
+ subscribeRoomMyState(roomId, cb) {
189
+ const off = this.hub._subscribe("myState", roomId, {
190
+ account: this.account,
191
+ cb
192
+ });
193
+ cb(jsonClone(this.hub._room(roomId).users.get(this.account) ?? {}));
194
+ return off;
195
+ }
196
+ subscribeRoomAllUserStates(roomId, cb) {
197
+ const off = this.hub._subscribe("allUsers", roomId, cb);
198
+ cb(this.hub._snapshotUsers(roomId));
199
+ return off;
200
+ }
201
+ subscribeRoomCollection(roomId, collectionId, cb) {
202
+ const off = this.hub._subscribe("collection", roomId, {
203
+ id: collectionId,
204
+ cb
205
+ });
206
+ cb(this.hub._snapshotCollection(roomId, collectionId));
207
+ return off;
208
+ }
209
+ onRoomMessage(roomId, type, cb) {
210
+ return this.hub._subscribe("message", roomId, {
211
+ type,
212
+ cb
213
+ });
214
+ }
215
+ onRoomUserJoin(roomId, cb) {
216
+ return this.hub._subscribe("join", roomId, cb);
217
+ }
218
+ onRoomUserLeave(roomId, cb) {
219
+ return this.hub._subscribe("leave", roomId, cb);
220
+ }
221
+ };
222
+ //#endregion
223
+ //#region src/net/local-game-server.ts
224
+ /**
225
+ * Run a multiplayer game's REAL agent8-sdk-v2 `Server` class **locally**, in
226
+ * memory, with NO cloud and NO auth — the preview/dev path.
227
+ *
228
+ * `LoopbackHub` answers only the fixed built-in room protocol (joinRoom,
229
+ * setMyState, collections, events). Real games add server-AUTHORITATIVE rules
230
+ * (`awardPoint`, `castSpell`, `$roomTick` match clocks) in `server/src/server.ts`
231
+ * against the v2 contexts `$sender`/`$global`/`$room`/`$lock`. LocalGameServer
232
+ * runs that exact class body so the whole game — client + server logic — is
233
+ * playable in dev before it ever touches the Agent8 platform. Swap
234
+ * `local.createClient(account)` for `await createAgent8Server()` to go live;
235
+ * nothing else changes.
236
+ *
237
+ * How it stays faithful to the isolated-vm platform:
238
+ * - A FRESH `Server` instance runs per request, so `this.*` never persists.
239
+ * - The v2 globals are injected on `globalThis` for the duration of each call
240
+ * (the SAME `server/src/server.ts` body that reads bare `$room`/`$sender`
241
+ * runs unmodified).
242
+ * - Calls are SERIALIZED through one queue, so a global binding can't leak across
243
+ * another request's `await`s (the platform isolates per request; we serialize).
244
+ * - `$roomTick(deltaMS, roomId)` runs only while a room has users — drive it from
245
+ * the engine: `engine.updated.connect((dt) => local.tick(dt * 1000))`.
246
+ *
247
+ * NOT a substitute for the cloud: no isolated-vm sandboxing, no persistence, no
248
+ * rate limits — a faithful FUNCTIONAL emulator for local play and tests.
249
+ */
250
+ var LocalGameServer = class {
251
+ hub = new LoopbackHub();
252
+ serverClass;
253
+ /** Each client's current room — binds `$sender.roomId` on later calls. */
254
+ clientRooms = /* @__PURE__ */ new Map();
255
+ /** All remoteFunction/tick calls run one-at-a-time so injected globals never overlap. */
256
+ queue = Promise.resolve();
257
+ /** Per-key mutex backing `$lock`. */
258
+ locks = /* @__PURE__ */ new Map();
259
+ /** Persistent (process-lifetime) store backing the `$global` + `$asset` contexts. */
260
+ g_state = {};
261
+ g_userStates = /* @__PURE__ */ new Map();
262
+ g_collections = /* @__PURE__ */ new Map();
263
+ g_assets = /* @__PURE__ */ new Map();
264
+ g_seq = 0;
265
+ /** Client-side global/asset subscription registries (fanned out on mutation). */
266
+ subState = /* @__PURE__ */ new Set();
267
+ subUser = /* @__PURE__ */ new Map();
268
+ subColl = /* @__PURE__ */ new Map();
269
+ subAsset = /* @__PURE__ */ new Map();
270
+ subMsg = /* @__PURE__ */ new Map();
271
+ constructor(serverClass) {
272
+ this.serverClass = serverClass;
273
+ }
274
+ /** A client on this server. Distinct `account` per player (default auto-assigned). */
275
+ createClient(account) {
276
+ return new LocalGameServerTransport(this, this.hub.createClient(account));
277
+ }
278
+ collectionSnapshot(id) {
279
+ const c = this.g_collections.get(id);
280
+ return c ? Object.fromEntries([...c.entries()].map(([k, v]) => [k, jsonClone(v)])) : {};
281
+ }
282
+ assetSnapshot(acct) {
283
+ return Object.fromEntries(this.g_assets.get(acct) ?? []);
284
+ }
285
+ /** @internal */
286
+ subscribeGlobalState(cb) {
287
+ this.subState.add(cb);
288
+ cb(jsonClone(this.g_state));
289
+ return () => this.subState.delete(cb);
290
+ }
291
+ /** @internal */
292
+ subscribeGlobalUserState(account, cb) {
293
+ const off = addToSetMap(this.subUser, account, cb);
294
+ cb(jsonClone(this.g_userStates.get(account) ?? {}));
295
+ return off;
296
+ }
297
+ /** @internal */
298
+ subscribeGlobalCollection(id, cb) {
299
+ const off = addToSetMap(this.subColl, id, cb);
300
+ cb(this.collectionSnapshot(id));
301
+ return off;
302
+ }
303
+ /** @internal */
304
+ subscribeAsset(account, cb) {
305
+ const off = addToSetMap(this.subAsset, account, cb);
306
+ cb(this.assetSnapshot(account));
307
+ return off;
308
+ }
309
+ /** @internal Messages are events — no immediate snapshot. */
310
+ onGlobalMessage(account, type, cb) {
311
+ const entry = {
312
+ account,
313
+ cb
314
+ };
315
+ return addToSetMap(this.subMsg, type, entry);
316
+ }
317
+ fireState() {
318
+ const s = jsonClone(this.g_state);
319
+ for (const cb of [...this.subState]) cb(s);
320
+ }
321
+ fireUser(account) {
322
+ const s = jsonClone(this.g_userStates.get(account) ?? {});
323
+ for (const cb of [...this.subUser.get(account) ?? []]) cb(s);
324
+ }
325
+ fireColl(id) {
326
+ const r = this.collectionSnapshot(id);
327
+ for (const cb of [...this.subColl.get(id) ?? []]) cb(r);
328
+ }
329
+ fireAsset(account) {
330
+ const w = this.assetSnapshot(account);
331
+ for (const cb of [...this.subAsset.get(account) ?? []]) cb(w);
332
+ }
333
+ fireMsg(type, message, target) {
334
+ for (const e of [...this.subMsg.get(type) ?? []]) if (!target || e.account === target) e.cb(jsonClone(message));
335
+ }
336
+ /**
337
+ * Advance server-driven periodic logic. Calls the `Server`'s `$roomTick(deltaMS,
338
+ * roomId)` once per room that currently has users. No-op without a `$roomTick`.
339
+ */
340
+ tick(deltaMS) {
341
+ return this.enqueue(() => this.runTick(deltaMS));
342
+ }
343
+ /** @internal Dispatch a remoteFunction (serialized). */
344
+ call(account, fn, args) {
345
+ return this.enqueue(() => this.dispatch(account, fn, args));
346
+ }
347
+ enqueue(run) {
348
+ const result = this.queue.then(run, run);
349
+ this.queue = result.then(() => void 0, () => void 0);
350
+ return result;
351
+ }
352
+ async dispatch(account, fn, args) {
353
+ const server = this.serverClass ? new this.serverClass() : null;
354
+ const method = server && typeof server[fn] === "function" ? server[fn] : null;
355
+ const roomId = fn === "joinRoom" ? void 0 : this.clientRooms.get(account) ?? args[0];
356
+ let out;
357
+ if (method && server) {
358
+ const restore = this.bindGlobals(account, roomId);
359
+ try {
360
+ out = await method.apply(server, args);
361
+ } finally {
362
+ restore();
363
+ }
364
+ } else out = this.hub._call(account, fn, args);
365
+ if (fn === "joinRoom" && typeof out === "string") this.clientRooms.set(account, out);
366
+ else if (fn === "leaveRoom") this.clientRooms.delete(account);
367
+ return out;
368
+ }
369
+ async runTick(deltaMS) {
370
+ if (!this.serverClass) return;
371
+ if (typeof new this.serverClass().$roomTick !== "function") return;
372
+ for (const roomId of new Set(this.clientRooms.values())) {
373
+ const server = new this.serverClass();
374
+ const restore = this.bindGlobals("", roomId);
375
+ try {
376
+ await server.$roomTick.call(server, deltaMS, roomId);
377
+ } finally {
378
+ restore();
379
+ }
380
+ }
381
+ }
382
+ /** Install the v2 globals for one dispatch; returns a restorer. */
383
+ bindGlobals(account, roomId) {
384
+ const g = globalThis;
385
+ const prev = {
386
+ $sender: g.$sender,
387
+ $global: g.$global,
388
+ $room: g.$room,
389
+ $lock: g.$lock,
390
+ $asset: g.$asset
391
+ };
392
+ g.$sender = {
393
+ account,
394
+ roomId: roomId ?? ""
395
+ };
396
+ g.$global = this.globalContext(account);
397
+ g.$room = this.roomContext(account, roomId);
398
+ g.$asset = this.assetContext(account);
399
+ g.$lock = (key, fn) => this.withLock(key, fn);
400
+ return () => {
401
+ g.$sender = prev.$sender;
402
+ g.$global = prev.$global;
403
+ g.$room = prev.$room;
404
+ g.$lock = prev.$lock;
405
+ g.$asset = prev.$asset;
406
+ };
407
+ }
408
+ /**
409
+ * The `$global` context — room membership, persistent GLOBAL state/user-state/
410
+ * collections, room management, and global messaging. Backed by an in-memory
411
+ * store that survives between requests (unlike rooms, which the hub clears when
412
+ * empty) — matching the platform's persistent-global / ephemeral-room split.
413
+ */
414
+ globalContext(account) {
415
+ const hub = this.hub;
416
+ const coll = (id) => {
417
+ let c = this.g_collections.get(id);
418
+ if (!c) {
419
+ c = /* @__PURE__ */ new Map();
420
+ this.g_collections.set(id, c);
421
+ }
422
+ return c;
423
+ };
424
+ const userState = (acct) => this.g_userStates.get(acct) ?? {};
425
+ return {
426
+ joinRoom: (rid) => Promise.resolve(hub._call(account, "joinRoom", [rid])),
427
+ leaveRoom: () => {
428
+ const left = this.clientRooms.get(account) ?? "";
429
+ hub._call(account, "leaveRoom", [left]);
430
+ return Promise.resolve(left);
431
+ },
432
+ getGlobalState: () => Promise.resolve(jsonClone(this.g_state)),
433
+ updateGlobalState: (patch) => {
434
+ Object.assign(this.g_state, jsonClone(patch));
435
+ this.fireState();
436
+ return Promise.resolve(jsonClone(this.g_state));
437
+ },
438
+ getUserState: (acct) => Promise.resolve(jsonClone(userState(acct))),
439
+ updateUserState: (acct, patch) => {
440
+ const next = {
441
+ ...userState(acct),
442
+ ...jsonClone(patch)
443
+ };
444
+ this.g_userStates.set(acct, next);
445
+ this.fireUser(acct);
446
+ return Promise.resolve(jsonClone(next));
447
+ },
448
+ getMyState: () => Promise.resolve(jsonClone(userState(account))),
449
+ updateMyState: (patch) => {
450
+ const next = {
451
+ ...userState(account),
452
+ ...jsonClone(patch)
453
+ };
454
+ this.g_userStates.set(account, next);
455
+ this.fireUser(account);
456
+ return Promise.resolve(jsonClone(next));
457
+ },
458
+ addCollectionItem: (id, item) => {
459
+ const __id = item.__id ?? `g${++this.g_seq}`;
460
+ const stored = {
461
+ ...jsonClone(item),
462
+ __id
463
+ };
464
+ coll(id).set(__id, stored);
465
+ this.fireColl(id);
466
+ return Promise.resolve(jsonClone(stored));
467
+ },
468
+ updateCollectionItem: (id, item) => {
469
+ const { __id, ...patch } = item;
470
+ const c = coll(id);
471
+ const next = {
472
+ ...c.get(__id) ?? {},
473
+ ...jsonClone(patch),
474
+ __id
475
+ };
476
+ c.set(__id, next);
477
+ this.fireColl(id);
478
+ return Promise.resolve(jsonClone(next));
479
+ },
480
+ deleteCollectionItem: (id, itemId) => {
481
+ coll(id).delete(itemId);
482
+ this.fireColl(id);
483
+ return Promise.resolve({ __id: itemId });
484
+ },
485
+ deleteCollection: (id) => {
486
+ this.g_collections.delete(id);
487
+ this.fireColl(id);
488
+ return Promise.resolve(id);
489
+ },
490
+ getCollectionItem: (id, itemId) => Promise.resolve(jsonClone(coll(id).get(itemId) ?? {})),
491
+ getCollectionItems: (id, options) => Promise.resolve(queryItems([...coll(id).values()].map(jsonClone), options)),
492
+ countCollectionItems: (id, options) => Promise.resolve(queryItems([...coll(id).values()].map(jsonClone), options).length),
493
+ countRooms: () => Promise.resolve(new Set(this.clientRooms.values()).size),
494
+ getAllRoomIds: () => Promise.resolve([...new Set(this.clientRooms.values())]),
495
+ getAllRoomStates: () => Promise.resolve([...new Set(this.clientRooms.values())].map((r) => roomStateWithDefaults(hub, r))),
496
+ getRoomUserAccounts: (rid) => Promise.resolve(Object.keys(hub._snapshotUsers(rid))),
497
+ countRoomUsers: (rid) => Promise.resolve(Object.keys(hub._snapshotUsers(rid)).length),
498
+ getRoomState: (rid) => Promise.resolve(roomStateWithDefaults(hub, rid)),
499
+ updateRoomState: (rid, patch) => {
500
+ hub._call(account, "patchRoomState", [rid, patch]);
501
+ return Promise.resolve(roomStateWithDefaults(hub, rid));
502
+ },
503
+ getRoomUserState: (rid, acct) => Promise.resolve(hub._snapshotUsers(rid)[acct] ?? {}),
504
+ updateRoomUserState: (rid, acct, patch) => {
505
+ hub._call(acct, "setMyState", [rid, patch]);
506
+ return Promise.resolve(hub._snapshotUsers(rid)[acct] ?? {});
507
+ },
508
+ broadcastToAll: (type, message) => {
509
+ this.fireMsg(type, message);
510
+ },
511
+ sendMessageToUser: (type, acct, message) => {
512
+ this.fireMsg(type, message, acct);
513
+ }
514
+ };
515
+ }
516
+ /**
517
+ * The `$asset` context — a per-account currency ledger. `burn`/`transfer` throw
518
+ * on an insufficient balance (the documented pattern always checks `has` first),
519
+ * surfacing economy bugs in preview just as the platform would.
520
+ */
521
+ assetContext(account) {
522
+ const wallet = (acct) => {
523
+ let w = this.g_assets.get(acct);
524
+ if (!w) {
525
+ w = /* @__PURE__ */ new Map();
526
+ this.g_assets.set(acct, w);
527
+ }
528
+ return w;
529
+ };
530
+ const all = (acct) => Object.fromEntries(wallet(acct));
531
+ return {
532
+ get: (assetId) => Promise.resolve(wallet(account).get(assetId) ?? 0),
533
+ getAll: () => Promise.resolve(all(account)),
534
+ has: (assetId, amount) => Promise.resolve((wallet(account).get(assetId) ?? 0) >= amount),
535
+ mint: (assetId, amount) => {
536
+ requireAmount("mint", amount);
537
+ wallet(account).set(assetId, (wallet(account).get(assetId) ?? 0) + amount);
538
+ this.fireAsset(account);
539
+ return Promise.resolve(all(account));
540
+ },
541
+ burn: (assetId, amount) => {
542
+ requireAmount("burn", amount);
543
+ const have = wallet(account).get(assetId) ?? 0;
544
+ if (have < amount) throw new IncantoError("BAD_FORMAT", `$asset.burn: insufficient '${assetId}' for ${account} (have ${have}, need ${amount}). Guard with $asset.has first.`);
545
+ wallet(account).set(assetId, have - amount);
546
+ this.fireAsset(account);
547
+ return Promise.resolve(all(account));
548
+ },
549
+ transfer: (toAccount, assetId, amount) => {
550
+ requireAmount("transfer", amount);
551
+ const have = wallet(account).get(assetId) ?? 0;
552
+ if (have < amount) throw new IncantoError("BAD_FORMAT", `$asset.transfer: insufficient '${assetId}' for ${account} (have ${have}, need ${amount}).`);
553
+ wallet(account).set(assetId, have - amount);
554
+ wallet(toAccount).set(assetId, (wallet(toAccount).get(assetId) ?? 0) + amount);
555
+ this.fireAsset(account);
556
+ this.fireAsset(toAccount);
557
+ return Promise.resolve(all(account));
558
+ }
559
+ };
560
+ }
561
+ /** The `$room` context, bound to one room — maps onto the hub's kernel primitives. */
562
+ roomContext(account, roomId) {
563
+ const rid = () => {
564
+ if (!roomId) throw new IncantoError("BAD_FORMAT", "$room used outside a room — joinRoom must resolve a room before $room is touched.");
565
+ return roomId;
566
+ };
567
+ const hub = this.hub;
568
+ return {
569
+ updateMyState: (patch) => Promise.resolve(hub._call(account, "setMyState", [rid(), patch])).then(() => hub._snapshotUsers(rid())[account] ?? {}),
570
+ updateRoomState: (patch) => Promise.resolve(hub._call(account, "patchRoomState", [rid(), patch])).then(() => hub._snapshotRoomState(rid())),
571
+ updateUserState: (acct, patch) => Promise.resolve(hub._call(acct, "setMyState", [rid(), patch])).then(() => hub._snapshotUsers(rid())[acct] ?? {}),
572
+ getUserState: (acct) => Promise.resolve(hub._snapshotUsers(rid())[acct] ?? {}),
573
+ getMyState: () => Promise.resolve(hub._snapshotUsers(rid())[account] ?? {}),
574
+ getRoomState: () => Promise.resolve(roomStateWithDefaults(hub, rid())),
575
+ getAllUserStates: () => Promise.resolve(Object.entries(hub._snapshotUsers(rid())).map(([acct, state]) => ({
576
+ ...state,
577
+ account: acct
578
+ }))),
579
+ countUsers: () => Promise.resolve(Object.keys(hub._snapshotUsers(rid())).length),
580
+ addCollectionItem: (coll, item) => {
581
+ const id = hub._call(account, "addEntity", [
582
+ rid(),
583
+ coll,
584
+ item
585
+ ]);
586
+ return Promise.resolve(hub._snapshotCollection(rid(), coll)[id] ?? { __id: id });
587
+ },
588
+ updateCollectionItem: (coll, item) => {
589
+ const { __id, ...patch } = item;
590
+ hub._call(account, "updateEntity", [
591
+ rid(),
592
+ coll,
593
+ __id,
594
+ patch
595
+ ]);
596
+ return Promise.resolve(hub._snapshotCollection(rid(), coll)[__id] ?? { __id });
597
+ },
598
+ deleteCollectionItem: (coll, itemId) => {
599
+ hub._call(account, "removeEntity", [
600
+ rid(),
601
+ coll,
602
+ itemId
603
+ ]);
604
+ return Promise.resolve({ __id: itemId });
605
+ },
606
+ deleteCollection: (coll) => {
607
+ for (const id of Object.keys(hub._snapshotCollection(rid(), coll))) hub._call(account, "removeEntity", [
608
+ rid(),
609
+ coll,
610
+ id
611
+ ]);
612
+ return Promise.resolve(coll);
613
+ },
614
+ getCollectionItem: (coll, itemId) => Promise.resolve(hub._snapshotCollection(rid(), coll)[itemId] ?? {}),
615
+ getCollectionItems: (coll, options) => Promise.resolve(queryItems(Object.values(hub._snapshotCollection(rid(), coll)), options)),
616
+ countCollectionItems: (coll, options) => Promise.resolve(queryItems(Object.values(hub._snapshotCollection(rid(), coll)), options).length),
617
+ broadcastToRoom: (type, message) => {
618
+ hub._call(account, "sendEvent", [
619
+ rid(),
620
+ type,
621
+ message
622
+ ]);
623
+ },
624
+ sendMessageToUser: (_type, _acct, _message) => {}
625
+ };
626
+ }
627
+ withLock(key, fn) {
628
+ const run = (this.locks.get(key) ?? Promise.resolve()).then(() => fn());
629
+ this.locks.set(key, run.then(() => void 0, () => void 0));
630
+ return run;
631
+ }
632
+ };
633
+ /** Add a value to a Map-of-Sets registry; returns an unsubscribe that removes it. */
634
+ function addToSetMap(map, key, value) {
635
+ let set = map.get(key);
636
+ if (!set) {
637
+ set = /* @__PURE__ */ new Set();
638
+ map.set(key, set);
639
+ }
640
+ set.add(value);
641
+ return () => {
642
+ set?.delete(value);
643
+ };
644
+ }
645
+ /** A room state snapshot with the platform-maintained `roomId` + `$users` defaults. */
646
+ function roomStateWithDefaults(hub, roomId) {
647
+ return {
648
+ ...hub._snapshotRoomState(roomId),
649
+ roomId,
650
+ $users: Object.keys(hub._snapshotUsers(roomId))
651
+ };
652
+ }
653
+ /** Throw unless `amount` is a non-negative finite number (matches the platform; the
654
+ * preview must NOT be more lenient — a negative burn would otherwise ADD currency). */
655
+ function requireAmount(op, amount) {
656
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) throw new IncantoError("BAD_FORMAT", `$asset.${op}: amount must be a non-negative finite number (got ${amount}).`);
657
+ }
658
+ /** Compare two JSON-ish scalars for ordering. Numbers numerically, else by string. */
659
+ function cmp(a, b) {
660
+ if (typeof a === "number" && typeof b === "number") return a - b;
661
+ const sa = String(a);
662
+ const sb = String(b);
663
+ return sa < sb ? -1 : sa > sb ? 1 : 0;
664
+ }
665
+ function jsonEq(a, b) {
666
+ return JSON.stringify(a) === JSON.stringify(b);
667
+ }
668
+ function matchFilter(fieldVal, op, value) {
669
+ switch (op) {
670
+ case "<": return cmp(fieldVal, value) < 0;
671
+ case "<=": return cmp(fieldVal, value) <= 0;
672
+ case ">": return cmp(fieldVal, value) > 0;
673
+ case ">=": return cmp(fieldVal, value) >= 0;
674
+ case "==": return jsonEq(fieldVal, value);
675
+ case "!=": return !jsonEq(fieldVal, value);
676
+ case "array-contains": return Array.isArray(fieldVal) && fieldVal.some((v) => jsonEq(v, value));
677
+ case "in": return Array.isArray(value) && value.some((v) => jsonEq(v, fieldVal));
678
+ case "not-in": return Array.isArray(value) && !value.some((v) => jsonEq(v, fieldVal));
679
+ case "array-contains-any": return Array.isArray(fieldVal) && Array.isArray(value) && fieldVal.some((v) => value.some((w) => jsonEq(v, w)));
680
+ default: throw new IncantoError("BAD_FORMAT", `Unknown collection filter operator '${op}'.`);
681
+ }
682
+ }
683
+ /** Apply CollectionOptions to a list of items (preview emulation of the platform query). */
684
+ function queryItems(items, options) {
685
+ if (!options) return items;
686
+ const { filters, orderBy, limit, startAfter, endBefore } = options;
687
+ if (filters?.length && orderBy?.length) throw new IncantoError("BAD_FORMAT", "CollectionOptions cannot combine filters and orderBy (the platform requires a composite index). Use one or the other.");
688
+ let out = items;
689
+ if (filters?.length) out = out.filter((it) => filters.every((f) => matchFilter(it[f.field], f.operator, f.value)));
690
+ const ob = orderBy?.[0];
691
+ if (ob) {
692
+ const { field } = ob;
693
+ const dir = ob.direction === "desc" ? -1 : 1;
694
+ out = [...out].sort((a, b) => cmp(a[field], b[field]) * dir);
695
+ if (startAfter !== void 0) out = out.filter((it) => cmp(it[field], startAfter) * dir > 0);
696
+ if (endBefore !== void 0) out = out.filter((it) => cmp(it[field], endBefore) * dir < 0);
697
+ }
698
+ if (typeof limit === "number") out = out.slice(0, limit);
699
+ return out;
700
+ }
701
+ /** Construct a local preview server. With no `server`, it equals a raw LoopbackHub. */
702
+ function createLocalGameServer(opts = {}) {
703
+ return new LocalGameServer(opts.server);
704
+ }
705
+ /**
706
+ * A client of a {@link LocalGameServer}. Subscriptions read the shared in-memory
707
+ * store directly (same fan-out as Loopback); `remoteFunction` routes through the
708
+ * server's serialized dispatch so the game's `Server` class actually runs.
709
+ */
710
+ var LocalGameServerTransport = class {
711
+ server;
712
+ loop;
713
+ account;
714
+ connected = false;
715
+ constructor(server, loop) {
716
+ this.server = server;
717
+ this.loop = loop;
718
+ this.account = loop.account;
719
+ }
720
+ async connect() {
721
+ this.connected = true;
722
+ await this.loop.connect();
723
+ return true;
724
+ }
725
+ async disconnect() {
726
+ this.connected = false;
727
+ await this.loop.disconnect();
728
+ }
729
+ remoteFunction(fn, args = []) {
730
+ return this.server.call(this.account, fn, args);
731
+ }
732
+ subscribeRoomState(roomId, cb) {
733
+ return this.loop.subscribeRoomState(roomId, cb);
734
+ }
735
+ subscribeRoomMyState(roomId, cb) {
736
+ return this.loop.subscribeRoomMyState(roomId, cb);
737
+ }
738
+ subscribeRoomAllUserStates(roomId, cb) {
739
+ return this.loop.subscribeRoomAllUserStates(roomId, cb);
740
+ }
741
+ subscribeRoomCollection(roomId, collectionId, cb) {
742
+ return this.loop.subscribeRoomCollection(roomId, collectionId, cb);
743
+ }
744
+ onRoomMessage(roomId, type, cb) {
745
+ return this.loop.onRoomMessage(roomId, type, cb);
746
+ }
747
+ onRoomUserJoin(roomId, cb) {
748
+ return this.loop.onRoomUserJoin(roomId, cb);
749
+ }
750
+ onRoomUserLeave(roomId, cb) {
751
+ return this.loop.onRoomUserLeave(roomId, cb);
752
+ }
753
+ subscribeGlobalState(cb) {
754
+ return this.server.subscribeGlobalState(cb);
755
+ }
756
+ subscribeGlobalMyState(cb) {
757
+ return this.server.subscribeGlobalUserState(this.account, cb);
758
+ }
759
+ subscribeGlobalUserState(account, cb) {
760
+ return this.server.subscribeGlobalUserState(account, cb);
761
+ }
762
+ subscribeGlobalCollection(collectionId, cb) {
763
+ return this.server.subscribeGlobalCollection(collectionId, cb);
764
+ }
765
+ subscribeAsset(account, cb) {
766
+ return this.server.subscribeAsset(account, cb);
767
+ }
768
+ onGlobalMessage(type, cb) {
769
+ return this.server.onGlobalMessage(this.account, type, cb);
770
+ }
771
+ };
772
+ //#endregion
773
+ //#region src/net/network-manager.ts
774
+ const managers = /* @__PURE__ */ new WeakMap();
775
+ /**
776
+ * Per-engine multiplayer hub: joins a room, exposes the room channels as core
777
+ * Signals, replicates `network: {mode:'owner'}` node props on a throttle
778
+ * window, and resolves registered scenes for NetworkSpawner.
779
+ */
780
+ var NetworkManager = class NetworkManager {
781
+ account;
782
+ roomId;
783
+ roomState = new Signal();
784
+ allUserStates = new Signal();
785
+ userJoined = new Signal();
786
+ userLeft = new Signal();
787
+ /**
788
+ * GLOBAL (cross-room, persistent) channels — populated only when the transport
789
+ * supports them (`LocalGameServer`, the agent8 adapter). On a transport without a
790
+ * global tier they stay empty / never fire.
791
+ */
792
+ globalState = new Signal();
793
+ globalMyState = new Signal();
794
+ /** The local account's `$asset` ledger. */
795
+ asset = new Signal();
796
+ /** Latest snapshots for polling consumers (NetworkSpawner). */
797
+ latestUserStates = {};
798
+ latestRoomState = {};
799
+ latestGlobalState = {};
800
+ latestGlobalMyState = {};
801
+ latestAsset = {};
802
+ server;
803
+ engine;
804
+ throttleMs;
805
+ subs = [];
806
+ messageSignals = /* @__PURE__ */ new Map();
807
+ collectionSignals = /* @__PURE__ */ new Map();
808
+ latestCollections = /* @__PURE__ */ new Map();
809
+ globalMessageSignals = /* @__PURE__ */ new Map();
810
+ globalCollectionSignals = /* @__PURE__ */ new Map();
811
+ latestGlobalCollections = /* @__PURE__ */ new Map();
812
+ scenes = /* @__PURE__ */ new Map();
813
+ lastSent = /* @__PURE__ */ new Map();
814
+ sendAccumulator = 0;
815
+ detachReplication = null;
816
+ lastOwner = null;
817
+ boundScene = null;
818
+ static get(engine) {
819
+ return managers.get(engine) ?? null;
820
+ }
821
+ static async create(engine, opts = {}) {
822
+ await managers.get(engine)?.dispose();
823
+ const server = opts.transport ?? await createDefaultTransport(opts.config);
824
+ await server.connect();
825
+ const requested = opts.room ?? engine.scene?.multiplayer?.room ?? "auto";
826
+ const manager = new NetworkManager(engine, server, await server.remoteFunction("joinRoom", [requested === "auto" ? void 0 : requested], { needResponse: true }), opts.throttleMs ?? 50);
827
+ managers.set(engine, manager);
828
+ return manager;
829
+ }
830
+ constructor(engine, server, roomId, throttleMs) {
831
+ this.engine = engine;
832
+ this.server = server;
833
+ this.roomId = roomId;
834
+ this.account = server.account;
835
+ this.throttleMs = Math.max(30, throttleMs);
836
+ this.boundScene = engine.scene;
837
+ this.subs.push(server.subscribeRoomState(roomId, (state) => {
838
+ this.latestRoomState = state;
839
+ this.roomState.emit(state);
840
+ }), server.subscribeRoomAllUserStates(roomId, (states) => {
841
+ this.latestUserStates = states;
842
+ this.allUserStates.emit(states);
843
+ }), server.onRoomUserJoin(roomId, (account) => this.userJoined.emit(account)), server.onRoomUserLeave(roomId, (account) => this.userLeft.emit(account)));
844
+ if (server.subscribeGlobalState) this.subs.push(server.subscribeGlobalState((state) => {
845
+ this.latestGlobalState = state;
846
+ this.globalState.emit(state);
847
+ }));
848
+ if (server.subscribeGlobalMyState) this.subs.push(server.subscribeGlobalMyState((state) => {
849
+ this.latestGlobalMyState = state;
850
+ this.globalMyState.emit(state);
851
+ }));
852
+ if (server.subscribeAsset) this.subs.push(server.subscribeAsset(this.account, (assets) => {
853
+ this.latestAsset = assets;
854
+ this.asset.emit(assets);
855
+ }));
856
+ this.detachReplication = engine.fixedUpdated.connect((dt) => this.replicate(dt));
857
+ }
858
+ /** Signal for a typed room message (`sendEvent` on any client). */
859
+ message(type) {
860
+ let sig = this.messageSignals.get(type);
861
+ if (!sig) {
862
+ sig = new Signal();
863
+ this.messageSignals.set(type, sig);
864
+ this.subs.push(this.server.onRoomMessage(this.roomId, type, (m) => sig?.emit(m)));
865
+ }
866
+ return sig;
867
+ }
868
+ /** Signal + snapshot for a room collection (spawned entities). */
869
+ collection(id) {
870
+ let sig = this.collectionSignals.get(id);
871
+ if (!sig) {
872
+ sig = new Signal();
873
+ this.collectionSignals.set(id, sig);
874
+ this.subs.push(this.server.subscribeRoomCollection(this.roomId, id, (entities) => {
875
+ this.latestCollections.set(id, entities);
876
+ sig?.emit(entities);
877
+ }));
878
+ }
879
+ return sig;
880
+ }
881
+ latestCollection(id) {
882
+ this.collection(id);
883
+ return this.latestCollections.get(id) ?? {};
884
+ }
885
+ /** Signal for a typed GLOBAL message (`$global.broadcastToAll`/`sendMessageToUser`). */
886
+ globalMessage(type) {
887
+ let sig = this.globalMessageSignals.get(type);
888
+ if (!sig) {
889
+ sig = new Signal();
890
+ this.globalMessageSignals.set(type, sig);
891
+ const sub = this.server.onGlobalMessage?.(type, (m) => sig?.emit(m));
892
+ if (sub) this.subs.push(sub);
893
+ }
894
+ return sig;
895
+ }
896
+ /** Signal + snapshot for a GLOBAL collection (persistent, cross-room). */
897
+ globalCollection(id) {
898
+ let sig = this.globalCollectionSignals.get(id);
899
+ if (!sig) {
900
+ sig = new Signal();
901
+ this.globalCollectionSignals.set(id, sig);
902
+ const sub = this.server.subscribeGlobalCollection?.(id, (entities) => {
903
+ this.latestGlobalCollections.set(id, entities);
904
+ sig?.emit(entities);
905
+ });
906
+ if (sub) this.subs.push(sub);
907
+ }
908
+ return sig;
909
+ }
910
+ latestGlobalCollection(id) {
911
+ this.globalCollection(id);
912
+ return this.latestGlobalCollections.get(id) ?? {};
913
+ }
914
+ setMyState(patch) {
915
+ return this.server.remoteFunction("setMyState", [this.roomId, patch], {
916
+ throttle: this.throttleMs,
917
+ throttleKey: "incanto:myState"
918
+ });
919
+ }
920
+ patchRoomState(patch) {
921
+ return this.server.remoteFunction("patchRoomState", [this.roomId, patch]);
922
+ }
923
+ addEntity(collectionId, entity) {
924
+ return this.server.remoteFunction("addEntity", [
925
+ this.roomId,
926
+ collectionId,
927
+ entity
928
+ ]);
929
+ }
930
+ updateEntity(collectionId, id, patch) {
931
+ return this.server.remoteFunction("updateEntity", [
932
+ this.roomId,
933
+ collectionId,
934
+ id,
935
+ patch
936
+ ]);
937
+ }
938
+ removeEntity(collectionId, id) {
939
+ return this.server.remoteFunction("removeEntity", [
940
+ this.roomId,
941
+ collectionId,
942
+ id
943
+ ]);
944
+ }
945
+ sendEvent(type, payload) {
946
+ return this.server.remoteFunction("sendEvent", [
947
+ this.roomId,
948
+ type,
949
+ payload
950
+ ]);
951
+ }
952
+ /**
953
+ * Invoke a CUSTOM server-authoritative function — a method you added to your
954
+ * `server/src/server.ts` `Server` class (run live on the Agent8 platform, or
955
+ * locally via `createLocalGameServer`). The room id is prepended automatically,
956
+ * matching the `(roomId, …)` shape every server method takes:
957
+ *
958
+ * `manager.call('claimCoin', coinId)` → server `claimCoin(roomId, coinId)`
959
+ *
960
+ * Returns the function's result (use it for server-validated outcomes — a
961
+ * rejected claim, a rolled value). This is THE entry point for game rules the
962
+ * client must not be trusted to compute.
963
+ */
964
+ call(fn, ...args) {
965
+ return this.server.remoteFunction(fn, [this.roomId, ...args], { needResponse: true });
966
+ }
967
+ /** Register a scene for NetworkSpawner (`"scene": "<key>"`). */
968
+ /**
969
+ * `json` is `unknown` for the reason `createGame2D.scene` is: an imported
970
+ * `.scene.json` does not satisfy `SceneJson` (`format: number` vs `1`), and
971
+ * the loader validates it properly anyway.
972
+ */
973
+ registerScene(key, json) {
974
+ this.scenes.set(key, json);
975
+ }
976
+ resolveScene(key) {
977
+ const json = this.scenes.get(key);
978
+ if (!json) throw new IncantoError("UNRESOLVED_INSTANCE", `No scene registered for '${key}'. Registered: [${[...this.scenes.keys()].join(", ")}]. Call manager.registerScene('${key}', sceneJson).`);
979
+ return json;
980
+ }
981
+ async dispose() {
982
+ this.detachReplication?.();
983
+ for (const off of this.subs) off();
984
+ await this.server.remoteFunction("leaveRoom", [this.roomId]);
985
+ managers.delete(this.engine);
986
+ }
987
+ replicate(dt) {
988
+ const scene = this.engine.scene;
989
+ if (!scene || scene !== this.boundScene) return;
990
+ this.sendAccumulator += dt * 1e3;
991
+ const owner = findOwnerNode(scene.root, true);
992
+ if (!owner) return;
993
+ if (owner !== this.lastOwner) {
994
+ this.lastOwner = owner;
995
+ this.lastSent.clear();
996
+ }
997
+ const net = owner.network;
998
+ const window = Math.max(30, typeof net.throttleMs === "number" ? net.throttleMs : this.throttleMs);
999
+ const syncKeys = Array.isArray(net.sync) ? net.sync : [];
1000
+ const patch = {};
1001
+ for (const key of syncKeys) {
1002
+ const value = readSyncKey(owner, key);
1003
+ if (value === void 0) continue;
1004
+ if (!jsonEquals(this.lastSent.get(key) ?? null, value)) patch[key] = jsonClone(value);
1005
+ }
1006
+ if (this.sendAccumulator < window) return;
1007
+ if (Object.keys(patch).length === 0) return;
1008
+ for (const [key, value] of Object.entries(patch)) this.lastSent.set(key, jsonClone(value));
1009
+ this.sendAccumulator = 0;
1010
+ this.setMyState({ sync: patch });
1011
+ }
1012
+ };
1013
+ function findOwnerNode(node, enforceSingle = false) {
1014
+ const owners = [];
1015
+ collectOwners(node, owners);
1016
+ if (enforceSingle && owners.length > 1) throw new IncantoError("BAD_FORMAT", `Multiple network owner nodes found (${owners.map((o) => o.getPath()).join(", ")}). Exactly ONE node per player may declare network.mode 'owner' — replicate spawned entities through collections instead.`);
1017
+ return owners[0] ?? null;
1018
+ }
1019
+ function collectOwners(node, out) {
1020
+ if (node.network?.mode === "owner") out.push(node);
1021
+ for (const c of node.children) collectOwners(c, out);
1022
+ }
1023
+ /** `'position'` → own prop; `'Sprite.animation'` → child path + prop (last dot splits). */
1024
+ function readSyncKey(node, key) {
1025
+ const lastDot = key.lastIndexOf(".");
1026
+ const target = lastDot === -1 ? node : node.getNodeOrNull(key.slice(0, lastDot)) ?? void 0;
1027
+ const prop = lastDot === -1 ? key : key.slice(lastDot + 1);
1028
+ if (!target) return void 0;
1029
+ return target[prop];
1030
+ }
1031
+ /**
1032
+ * @internal Applies a replicated flat sync patch (`{'position': …,
1033
+ * 'Sprite.animation': …}`) onto a spawned subtree (NetworkSpawner).
1034
+ */
1035
+ function applySyncPatch(root, patch, setProp, onMissingNode) {
1036
+ for (const [key, value] of Object.entries(patch)) {
1037
+ const lastDot = key.lastIndexOf(".");
1038
+ const target = lastDot === -1 ? root : root.getNodeOrNull(key.slice(0, lastDot));
1039
+ const prop = lastDot === -1 ? key : key.slice(lastDot + 1);
1040
+ if (target) setProp(target, prop, value);
1041
+ else onMissingNode?.(key);
1042
+ }
1043
+ }
1044
+ async function createDefaultTransport(config) {
1045
+ const { createAgent8Server } = await import("./agent8-CvsfVskX.js").then((n) => n.t);
1046
+ return createAgent8Server(config);
1047
+ }
1048
+ //#endregion
1049
+ //#region src/net/nodes/network-spawner.ts
1050
+ /** Platform bookkeeping the game never replicates. */
1051
+ const RESERVED_KEYS = new Set([
1052
+ "__id",
1053
+ "__roomId",
1054
+ "__updated",
1055
+ "__leaved",
1056
+ "account",
1057
+ "roomId"
1058
+ ]);
1059
+ /**
1060
+ * Materializes a registered scene per remote player or collection entity:
1061
+ *
1062
+ * - `source: 'users'` — one instance per OTHER account in the room
1063
+ * (self is skipped; the local player is your own owned node)
1064
+ * - `source: 'collection:<id>'` — one instance per entity (bullets, pickups…)
1065
+ *
1066
+ * Replicated flat `{sync: {...}}` patches apply onto each instance's root;
1067
+ * `position` lerps toward its target when `interpolate` is on. Emits
1068
+ * `spawned(node, key)` / `despawned(node, key)`.
1069
+ */
1070
+ var NetworkSpawner = class extends Node {
1071
+ static typeName = "NetworkSpawner";
1072
+ static signals = ["spawned", "despawned"];
1073
+ static props = {
1074
+ source: { default: "users" },
1075
+ scene: { default: "" },
1076
+ interpolate: { default: true }
1077
+ };
1078
+ source = "users";
1079
+ /** Key registered via `manager.registerScene(key, json)`. */
1080
+ scene = "";
1081
+ interpolate = true;
1082
+ spawned = /* @__PURE__ */ new Map();
1083
+ positionTargets = /* @__PURE__ */ new Map();
1084
+ failedKeys = /* @__PURE__ */ new Set();
1085
+ reported = /* @__PURE__ */ new Set();
1086
+ update(dt) {
1087
+ const engine = this.tree?.engine;
1088
+ if (!engine) return;
1089
+ const manager = NetworkManager.get(engine);
1090
+ if (!manager || this.scene === "") return;
1091
+ const entries = this.currentEntries(manager);
1092
+ for (const [key, state] of Object.entries(entries)) {
1093
+ let instance = this.spawned.get(key);
1094
+ if (!instance) {
1095
+ if (this.failedKeys.has(key)) continue;
1096
+ try {
1097
+ instance = buildNodeJson(manager.resolveScene(this.scene).root);
1098
+ } catch (error) {
1099
+ this.failedKeys.add(key);
1100
+ throw error;
1101
+ }
1102
+ instance.name = sanitizeName(key);
1103
+ this.addChild(instance);
1104
+ this.spawned.set(key, instance);
1105
+ this.emit("spawned", instance, key);
1106
+ }
1107
+ const patch = this.patchOf(state);
1108
+ if (patch) applySyncPatch(instance, patch, (target, prop, value) => this.setProp(target, prop, value), (missing) => this.report(`'${missing}' names a node this scene does not have`));
1109
+ }
1110
+ for (const [key, instance] of [...this.spawned.entries()]) if (!(key in entries)) {
1111
+ this.spawned.delete(key);
1112
+ this.positionTargets.delete(instance);
1113
+ this.emit("despawned", instance, key);
1114
+ instance.free();
1115
+ }
1116
+ if (this.interpolate) this.stepInterpolation(dt);
1117
+ }
1118
+ currentEntries(manager) {
1119
+ if (this.source === "users") {
1120
+ const out = {};
1121
+ for (const [account, state] of Object.entries(manager.latestUserStates)) if (account !== manager.account) out[account] = state;
1122
+ return out;
1123
+ }
1124
+ if (this.source.startsWith("collection:")) return manager.latestCollection(this.source.slice(11));
1125
+ return {};
1126
+ }
1127
+ /**
1128
+ * The replicated fields of one entry.
1129
+ *
1130
+ * A USER state carries them under `sync` (that is what the owner node sends).
1131
+ * A COLLECTION entity is written by your SERVER, which has no reason to know
1132
+ * that convention — `addCollectionItem('coins', { position: [x, y] })` is the
1133
+ * obvious thing to write, and it used to apply nothing at all. Both work:
1134
+ * `sync` when present, otherwise the entity minus the platform's own keys.
1135
+ */
1136
+ patchOf(state) {
1137
+ if (state.sync !== void 0) return state.sync;
1138
+ if (this.source === "users") return void 0;
1139
+ const out = {};
1140
+ for (const [key, value] of Object.entries(state)) {
1141
+ if (RESERVED_KEYS.has(key) || key.startsWith("$")) continue;
1142
+ out[key] = value;
1143
+ }
1144
+ return Object.keys(out).length > 0 ? out : void 0;
1145
+ }
1146
+ /** Say it ONCE per message — a per-frame repeat would drown the log. */
1147
+ report(message) {
1148
+ if (this.reported.has(message)) return;
1149
+ this.reported.add(message);
1150
+ diagnose(this.tree?.engine ?? null, "error", `[incanto] NetworkSpawner '${this.name}': ${message} — that value replicates to nothing.`);
1151
+ }
1152
+ setProp(target, prop, value) {
1153
+ if (this.interpolate && prop === "position" && Array.isArray(value)) {
1154
+ this.positionTargets.set(target, value);
1155
+ return;
1156
+ }
1157
+ target[prop] = value;
1158
+ }
1159
+ stepInterpolation(dt) {
1160
+ const f = Math.min(1, dt * 8);
1161
+ for (const [target, goal] of this.positionTargets) {
1162
+ const pos = target.position;
1163
+ if (!Array.isArray(pos)) continue;
1164
+ target.position = pos.map((v, i) => v + ((goal[i] ?? v) - v) * f);
1165
+ }
1166
+ }
1167
+ };
1168
+ function sanitizeName(key) {
1169
+ return key.replace(/[/%]/g, "_") || "remote";
1170
+ }
1171
+ //#endregion
1172
+ //#region src/net/register.ts
1173
+ /**
1174
+ * Register the networking node types (and the core nodes). Call once in your
1175
+ * game entry before loading a multiplayer scene. Explicit — never a side effect.
1176
+ */
1177
+ function registerNodesNet() {
1178
+ registerCoreNodes();
1179
+ registerNode(NetworkSpawner);
1180
+ }
1181
+ //#endregion
1182
+ //#region src/net/split-screen.ts
1183
+ /**
1184
+ * N local clients on ONE page sharing ONE in-memory game server — the
1185
+ * split-screen preview harness three examples were hand-wiring (~40 lines
1186
+ * each). Wires: LocalGameServer + per-player Engine/scene/NetworkManager +
1187
+ * the server tick driven from the first panel's clock.
1188
+ *
1189
+ * const { players, server } = await createSplitScreen({
1190
+ * scene: gameJson,
1191
+ * server: MyServerClass,
1192
+ * scenes: { 'remote-player': remoteJson },
1193
+ * setup: ({ engine }, i) => {
1194
+ * new Renderer2D({ canvas: canvases[i], engine });
1195
+ * engine.input.attachKeyboard(window);
1196
+ * engine.start();
1197
+ * },
1198
+ * });
1199
+ *
1200
+ * Going live stays the one-line story: replace the transport with
1201
+ * `createAgent8Server()` and boot ONE client per browser.
1202
+ */
1203
+ async function createSplitScreen(opts) {
1204
+ const server = createLocalGameServer(opts);
1205
+ const accounts = opts.accounts ?? ["p1", "p2"];
1206
+ const players = [];
1207
+ for (let i = 0; i < accounts.length; i++) {
1208
+ const account = accounts[i];
1209
+ const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + i } : {});
1210
+ engine.setScene(loadScene(jsonClone(opts.scene)));
1211
+ const manager = await NetworkManager.create(engine, {
1212
+ transport: server.createClient(account),
1213
+ ...opts.room !== void 0 || i > 0 ? { room: opts.room ?? players[0].manager.roomId } : {}
1214
+ });
1215
+ for (const [name, json] of Object.entries(opts.scenes ?? {})) manager.registerScene(name, json);
1216
+ await enablePhysicsFor(engine, opts.physics);
1217
+ const player = {
1218
+ account,
1219
+ engine,
1220
+ manager
1221
+ };
1222
+ players.push(player);
1223
+ await opts.setup(player, i);
1224
+ }
1225
+ const disconnect = (players[0]?.engine)?.updated.connect((dt) => void server.tick(Math.min(dt, .05) * 1e3));
1226
+ return {
1227
+ server,
1228
+ players,
1229
+ dispose() {
1230
+ disconnect?.();
1231
+ for (const p of players) p.engine.dispose?.();
1232
+ }
1233
+ };
1234
+ }
1235
+ /**
1236
+ * Enable physics the way the createGame helpers do, without dragging three.js
1237
+ * into the net layer's static graph: the adapter is imported only when the
1238
+ * scene actually needs it.
1239
+ */
1240
+ async function enablePhysicsFor(engine, want) {
1241
+ if (want === false) return;
1242
+ const root = engine.scene?.tree.root;
1243
+ if (!root) return;
1244
+ const dim = engine.scene?.dimension === "3d" ? "3d" : "2d";
1245
+ if (want !== true) {
1246
+ if (!hasBodyNamed(root, dim === "3d" ? "PhysicsBody3D" : "PhysicsBody2D")) return;
1247
+ }
1248
+ if (dim === "3d") {
1249
+ const { enablePhysics3D } = await import("./3d.js");
1250
+ await enablePhysics3D(engine);
1251
+ } else {
1252
+ const { enablePhysics2D } = await import("./2d.js");
1253
+ await enablePhysics2D(engine);
1254
+ }
1255
+ }
1256
+ /** Any node whose prototype chain carries the adapter's body base class. */
1257
+ function hasBodyNamed(node, base) {
1258
+ let ctor = node.constructor;
1259
+ while (typeof ctor === "function") {
1260
+ if (ctor.name === base) return true;
1261
+ ctor = Object.getPrototypeOf(ctor);
1262
+ }
1263
+ for (const child of node.children) if (hasBodyNamed(child, base)) return true;
1264
+ return false;
1265
+ }
1266
+ //#endregion
1267
+ export { applySyncPatch as a, createLocalGameServer as c, NetworkManager as i, LoopbackHub as l, registerNodesNet as n, LocalGameServer as o, NetworkSpawner as r, LocalGameServerTransport as s, createSplitScreen as t, LoopbackTransport as u };