incanto 0.69.0 → 0.71.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 (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
@@ -1,6 +1,7 @@
1
- import { Lt as Signal, b as Engine, jt as Node } from "./behavior-rm8vVzu_.js";
2
- import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
3
- import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
1
+ import { Lt as Signal, b as Engine, jt as Node } from "./behavior-Dcz0fr1S.js";
2
+ import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js";
3
+ import { a as SceneJson, c as JsonValue, s as JsonObject } from "./rng-Bb-IutXB.js";
4
+ import { t as LoadSceneOptions } from "./loader-8-IHvbKD.js";
4
5
 
5
6
  //#region src/net/types.d.ts
6
7
  type Unsubscribe = () => void;
@@ -117,6 +118,17 @@ interface LocalGameServerOptions {
117
118
  * raw `LoopbackHub`.
118
119
  */
119
120
  server?: ServerClass;
121
+ /**
122
+ * Where the preview server reports its own failures.
123
+ *
124
+ * It had nowhere to report them, and it is the server every multiplayer game
125
+ * boots against. A `$roomTick` that THREW went to absolutely nothing —
126
+ * `enqueue` marks the rejection handled and every shipped pump is
127
+ * `void local.tick(...)` — so the server-driven match clock stopped forever
128
+ * with `engine.log` empty and `stats().errors` 0. `createSplitScreen` passes
129
+ * the first player's engine.
130
+ */
131
+ diagnostics?: DiagnosticSink | null;
120
132
  }
121
133
  /**
122
134
  * Run a multiplayer game's REAL agent8-sdk-v2 `Server` class **locally**, in
@@ -165,7 +177,21 @@ declare class LocalGameServer {
165
177
  private readonly subColl;
166
178
  private readonly subAsset;
167
179
  private readonly subMsg;
168
- constructor(serverClass?: ServerClass);
180
+ private diagnostics;
181
+ /**
182
+ * Point the server's own failures at a log.
183
+ *
184
+ * `createSplitScreen` builds the server BEFORE the engines that will read it,
185
+ * so the sink arrives after construction. Without one, a `$roomTick` that
186
+ * throws and a wedged queue are console-only, which is exactly the silence
187
+ * this reports.
188
+ */
189
+ reportTo(sink: DiagnosticSink | null): void;
190
+ /** Said once per room, so a per-frame pump cannot fill the log. */
191
+ private readonly reportedTickFailures;
192
+ /** Said once per stuck call — see `enqueue`. */
193
+ private readonly reportedSlow;
194
+ constructor(serverClass?: ServerClass, diagnostics?: DiagnosticSink | null);
169
195
  /** A client on this server. Distinct `account` per player (default auto-assigned). */
170
196
  createClient(account?: string): LocalGameServerTransport;
171
197
  private collectionSnapshot;
@@ -192,6 +218,8 @@ declare class LocalGameServer {
192
218
  tick(deltaMS: number): Promise<void>;
193
219
  /** @internal Dispatch a remoteFunction (serialized). */
194
220
  call(account: string, fn: string, args: unknown[]): Promise<unknown>;
221
+ /** How long one serialized call may take before the server says it is stuck. */
222
+ private static readonly SLOW_CALL_MS;
195
223
  private enqueue;
196
224
  private dispatch;
197
225
  private runTick;
@@ -353,6 +381,10 @@ declare class NetworkManager {
353
381
  private detachReplication;
354
382
  private lastOwner;
355
383
  private boundScene;
384
+ /** The room the BOUND scene declared, so a swap can tell same-room from not. */
385
+ private declaredRoom;
386
+ /** Said once: a client that went mute must not also spam the console. */
387
+ private reportedUnbound;
356
388
  static get(engine: Engine): NetworkManager | null;
357
389
  static create(engine: Engine, opts?: NetworkManagerOptions): Promise<NetworkManager>;
358
390
  private constructor();
@@ -1,8 +1,8 @@
1
- import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-DZE7B07H.js";
2
- import { t as registerCoreNodes, v as Engine } from "./register-mmfugpaQ.js";
1
+ import { n as jsonEquals, t as jsonClone } from "./json-CfTjpvW8.js";
2
+ import { J as Node, Y as diagnose, ct as Signal, d as rebuildOptions, h as loadScene, l as markCloned, m as buildSceneSubtree, p as stripUids, u as markTemplateName } from "./save-slots-Bvuh2p_r.js";
3
+ import { t as registerCoreNodes, v as Engine } from "./register-DuVdzxbq.js";
3
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
5
- import { l as registerNode } from "./registry-CF70EArN.js";
5
+ import { p as registerNode } from "./touch-BnCyPA0G.js";
6
6
  //#region src/net/loopback.ts
7
7
  /**
8
8
  * In-memory multiplayer hub implementing the SAME kernel contract as
@@ -247,7 +247,7 @@ var LoopbackTransport = class {
247
247
  * NOT a substitute for the cloud: no isolated-vm sandboxing, no persistence, no
248
248
  * rate limits — a faithful FUNCTIONAL emulator for local play and tests.
249
249
  */
250
- var LocalGameServer = class {
250
+ var LocalGameServer = class LocalGameServer {
251
251
  hub = new LoopbackHub();
252
252
  serverClass;
253
253
  /** Each client's current room — binds `$sender.roomId` on later calls. */
@@ -268,8 +268,25 @@ var LocalGameServer = class {
268
268
  subColl = /* @__PURE__ */ new Map();
269
269
  subAsset = /* @__PURE__ */ new Map();
270
270
  subMsg = /* @__PURE__ */ new Map();
271
- constructor(serverClass) {
271
+ diagnostics;
272
+ /**
273
+ * Point the server's own failures at a log.
274
+ *
275
+ * `createSplitScreen` builds the server BEFORE the engines that will read it,
276
+ * so the sink arrives after construction. Without one, a `$roomTick` that
277
+ * throws and a wedged queue are console-only, which is exactly the silence
278
+ * this reports.
279
+ */
280
+ reportTo(sink) {
281
+ this.diagnostics = sink;
282
+ }
283
+ /** Said once per room, so a per-frame pump cannot fill the log. */
284
+ reportedTickFailures = /* @__PURE__ */ new Set();
285
+ /** Said once per stuck call — see `enqueue`. */
286
+ reportedSlow = /* @__PURE__ */ new Set();
287
+ constructor(serverClass, diagnostics) {
272
288
  this.serverClass = serverClass;
289
+ this.diagnostics = diagnostics ?? null;
273
290
  }
274
291
  /** A client on this server. Distinct `account` per player (default auto-assigned). */
275
292
  createClient(account) {
@@ -344,8 +361,18 @@ var LocalGameServer = class {
344
361
  call(account, fn, args) {
345
362
  return this.enqueue(() => this.dispatch(account, fn, args));
346
363
  }
364
+ /** How long one serialized call may take before the server says it is stuck. */
365
+ static SLOW_CALL_MS = 5e3;
347
366
  enqueue(run) {
348
367
  const result = this.queue.then(run, run);
368
+ const timer = setTimeout(() => {
369
+ const key = "slow";
370
+ if (this.reportedSlow.has(key)) return;
371
+ this.reportedSlow.add(key);
372
+ diagnose(this.diagnostics, "error", `[incanto] preview server: a call has been running for over ${LocalGameServer.SLOW_CALL_MS / 1e3}s and every other client is blocked behind it. LocalGameServer runs one call at a time — look for an await that never settles, or a nested \`\$lock\` on the same key (\$lock is NOT reentrant).`);
373
+ }, LocalGameServer.SLOW_CALL_MS);
374
+ timer.unref?.();
375
+ result.then(() => clearTimeout(timer), () => clearTimeout(timer));
349
376
  this.queue = result.then(() => void 0, () => void 0);
350
377
  return result;
351
378
  }
@@ -374,6 +401,11 @@ var LocalGameServer = class {
374
401
  const restore = this.bindGlobals("", roomId);
375
402
  try {
376
403
  await server.$roomTick.call(server, deltaMS, roomId);
404
+ } catch (error) {
405
+ if (!this.reportedTickFailures.has(roomId)) {
406
+ this.reportedTickFailures.add(roomId);
407
+ diagnose(this.diagnostics, "error", `[incanto] preview server: $roomTick threw for room '${roomId}' — this room's server-driven state has stopped advancing.`, error);
408
+ }
377
409
  } finally {
378
410
  restore();
379
411
  }
@@ -508,8 +540,8 @@ var LocalGameServer = class {
508
540
  broadcastToAll: (type, message) => {
509
541
  this.fireMsg(type, message);
510
542
  },
511
- sendMessageToUser: (type, acct, message) => {
512
- this.fireMsg(type, message, acct);
543
+ sendMessageToUser: (account, type, message) => {
544
+ this.fireMsg(type, message, account);
513
545
  }
514
546
  };
515
547
  }
@@ -621,7 +653,7 @@ var LocalGameServer = class {
621
653
  message
622
654
  ]);
623
655
  },
624
- sendMessageToUser: (_type, _acct, _message) => {}
656
+ sendMessageToUser: (_account, _type, _message) => {}
625
657
  };
626
658
  }
627
659
  withLock(key, fn) {
@@ -700,7 +732,7 @@ function queryItems(items, options) {
700
732
  }
701
733
  /** Construct a local preview server. With no `server`, it equals a raw LoopbackHub. */
702
734
  function createLocalGameServer(opts = {}) {
703
- return new LocalGameServer(opts.server);
735
+ return new LocalGameServer(opts.server, opts.diagnostics);
704
736
  }
705
737
  /**
706
738
  * A client of a {@link LocalGameServer}. Subscriptions read the shared in-memory
@@ -849,13 +881,17 @@ var NetworkManager = class NetworkManager {
849
881
  detachReplication = null;
850
882
  lastOwner = null;
851
883
  boundScene = null;
884
+ /** The room the BOUND scene declared, so a swap can tell same-room from not. */
885
+ declaredRoom = null;
886
+ /** Said once: a client that went mute must not also spam the console. */
887
+ reportedUnbound = false;
852
888
  static get(engine) {
853
889
  return managers.get(engine) ?? null;
854
890
  }
855
891
  static async create(engine, opts = {}) {
856
892
  await managers.get(engine)?.dispose();
857
- const server = opts.transport ?? await createDefaultTransport(opts.config);
858
- await server.connect();
893
+ const server = opts.transport ?? await createDefaultTransport(opts.config, engine);
894
+ if (await server.connect() === false) throw new IncantoError("NETWORK_UNAVAILABLE", "The multiplayer transport refused to connect, so this game cannot join a room. Check the server is running and reachable; pass `transport` to use a LocalGameServer or LoopbackTransport instead.");
859
895
  const requested = opts.room ?? engine.scene?.multiplayer?.room ?? "auto";
860
896
  const manager = new NetworkManager(engine, server, await server.remoteFunction("joinRoom", [requested === "auto" ? void 0 : requested], { needResponse: true }), opts.throttleMs ?? 50, opts.keyframeMs ?? DEFAULT_KEYFRAME_MS);
861
897
  managers.set(engine, manager);
@@ -869,6 +905,7 @@ var NetworkManager = class NetworkManager {
869
905
  this.throttleMs = Math.max(30, throttleMs);
870
906
  this.keyframeMs = Math.max(0, keyframeMs);
871
907
  this.boundScene = engine.scene;
908
+ this.declaredRoom = (engine.scene?.multiplayer)?.room;
872
909
  this.subs.push(server.subscribeRoomState(roomId, (state) => {
873
910
  this.latestRoomState = state;
874
911
  this.roomState.emit(state);
@@ -1017,11 +1054,25 @@ var NetworkManager = class NetworkManager {
1017
1054
  this.detachReplication?.();
1018
1055
  for (const off of this.subs) off();
1019
1056
  await this.server.remoteFunction("leaveRoom", [this.roomId]);
1020
- managers.delete(this.engine);
1057
+ if (managers.get(this.engine) === this) managers.delete(this.engine);
1021
1058
  }
1022
1059
  replicate(dt) {
1023
1060
  const scene = this.engine.scene;
1024
- if (!scene || scene !== this.boundScene) return;
1061
+ if (!scene) return;
1062
+ if (scene !== this.boundScene) {
1063
+ const room = scene.multiplayer?.room;
1064
+ if (room === this.declaredRoom) {
1065
+ this.boundScene = scene;
1066
+ this.lastOwner = null;
1067
+ this.lastSent.clear();
1068
+ } else {
1069
+ if (!this.reportedUnbound) {
1070
+ this.reportedUnbound = true;
1071
+ diagnose(this.engine, "error", `[incanto] multiplayer: this client stopped broadcasting — the scene changed to '${scene.name}', which declares room ${JSON.stringify(room ?? null)} and this manager is bound to ${JSON.stringify(this.declaredRoom ?? null)}. Create a new NetworkManager for the new room (incanto-multiplayer.md).`);
1072
+ }
1073
+ return;
1074
+ }
1075
+ }
1025
1076
  this.sendAccumulator += dt * 1e3;
1026
1077
  this.aliveMs += dt * 1e3;
1027
1078
  const owner = findOwnerNode(scene.root, true);
@@ -1052,7 +1103,9 @@ var NetworkManager = class NetworkManager {
1052
1103
  for (const [key, value] of Object.entries(full)) this.lastSent.set(key, jsonClone(value));
1053
1104
  this.keyframeAccumulator = 0;
1054
1105
  this.sendAccumulator = 0;
1055
- this.setMyState({ sync: full });
1106
+ this.setMyState({ sync: full }).catch((error) => {
1107
+ this.engine._asyncFailed(`${owner.getPath()}: replication to room '${this.roomId}' failed — this client is no longer visible to the others`, error);
1108
+ });
1056
1109
  }
1057
1110
  };
1058
1111
  /** The single `network.mode === 'owner'` node in a tree, or null. @internal */
@@ -1079,7 +1132,9 @@ function readSyncKey(node, key) {
1079
1132
  const target = lastDot === -1 ? node : node.getNodeOrNull(key.slice(0, lastDot)) ?? void 0;
1080
1133
  const prop = lastDot === -1 ? key : key.slice(lastDot + 1);
1081
1134
  if (!target) return void 0;
1082
- return target[prop];
1135
+ const value = target[prop];
1136
+ if (value instanceof Set) return [...value];
1137
+ return value;
1083
1138
  }
1084
1139
  /**
1085
1140
  * @internal Applies a replicated flat sync patch (`{'position': …,
@@ -1094,9 +1149,9 @@ function applySyncPatch(root, patch, setProp, onMissingNode) {
1094
1149
  else onMissingNode?.(key);
1095
1150
  }
1096
1151
  }
1097
- async function createDefaultTransport(config) {
1098
- const { createAgent8Server } = await import("./agent8-CvsfVskX.js").then((n) => n.t);
1099
- return createAgent8Server(config);
1152
+ async function createDefaultTransport(config, diagnostics) {
1153
+ const { createAgent8Server } = await import("./agent8-Cfmd3ar_.js").then((n) => n.t);
1154
+ return createAgent8Server(config, diagnostics);
1100
1155
  }
1101
1156
  //#endregion
1102
1157
  //#region src/net/nodes/network-spawner.ts
@@ -1125,7 +1180,19 @@ var NetworkSpawner = class extends Node {
1125
1180
  static signals = ["spawned", "despawned"];
1126
1181
  static props = {
1127
1182
  source: { default: "users" },
1128
- scene: { default: "" },
1183
+ /**
1184
+ * The key a remote's scene was registered under
1185
+ * (`manager.registerScene(key, json)`).
1186
+ *
1187
+ * REQUIRED, like `Chase.target` and `FollowCamera.target`, and for the same
1188
+ * reason: without one `update` returned every frame, no remote player ever
1189
+ * appeared, and the scene loaded, audited and played clean. A multiplayer
1190
+ * game with nobody in it is worth a load error, not a silent room.
1191
+ */
1192
+ scene: {
1193
+ default: "",
1194
+ required: true
1195
+ },
1129
1196
  interpolate: { default: true }
1130
1197
  };
1131
1198
  source = "users";
@@ -1136,18 +1203,30 @@ var NetworkSpawner = class extends Node {
1136
1203
  positionTargets = /* @__PURE__ */ new Map();
1137
1204
  failedKeys = /* @__PURE__ */ new Set();
1138
1205
  reported = /* @__PURE__ */ new Set();
1206
+ /** Seconds spent looking for a NetworkManager — see the report in `update`. */
1207
+ waited = 0;
1139
1208
  update(dt) {
1140
1209
  const engine = this.tree?.engine;
1141
1210
  if (!engine) return;
1142
1211
  const manager = NetworkManager.get(engine);
1143
- if (!manager || this.scene === "") return;
1212
+ if (!manager) {
1213
+ this.waited += dt;
1214
+ if (this.waited > 2) this.report("there is no multiplayer in this game");
1215
+ return;
1216
+ }
1144
1217
  const entries = this.currentEntries(manager);
1145
1218
  for (const [key, state] of Object.entries(entries)) {
1146
1219
  let instance = this.spawned.get(key);
1147
1220
  if (!instance) {
1148
1221
  if (this.failedKeys.has(key)) continue;
1149
1222
  try {
1150
- instance = buildNodeJson(manager.resolveScene(this.scene).root);
1223
+ const json = manager.resolveScene(this.scene);
1224
+ instance = buildSceneSubtree({
1225
+ ...json,
1226
+ root: stripUids(json.root)
1227
+ }, rebuildOptions(this));
1228
+ markCloned(instance);
1229
+ markTemplateName(instance);
1151
1230
  } catch (error) {
1152
1231
  this.failedKeys.add(key);
1153
1232
  throw error;
@@ -1207,13 +1286,57 @@ var NetworkSpawner = class extends Node {
1207
1286
  this.positionTargets.set(target, value);
1208
1287
  return;
1209
1288
  }
1210
- target[prop] = value;
1289
+ if (prop === "position" && Array.isArray(target.position)) {
1290
+ this.report(`'position' arrived as ${JSON.stringify(value)} — a node position is a number array`);
1291
+ return;
1292
+ }
1293
+ if (prop === "groups") {
1294
+ if (Array.isArray(value)) {
1295
+ for (const g of value) if (typeof g === "string") target.addToGroup(g);
1296
+ }
1297
+ return;
1298
+ }
1299
+ try {
1300
+ target[prop] = value;
1301
+ } catch (error) {
1302
+ if (!this.reported.has(`prop:${prop}`)) {
1303
+ this.reported.add(`prop:${prop}`);
1304
+ diagnose(this.tree?.engine, "error", `[incanto] NetworkSpawner on '${this.getPath()}': replicated key '${prop}' cannot be written onto a '${target.constructor.typeName ?? "Node"}' — it is a readonly accessor, not a prop. Sync a real prop, or a plain data key of your own.`, error);
1305
+ }
1306
+ }
1307
+ }
1308
+ /**
1309
+ * What the NETWORK last said this node's position is — not where it is drawn.
1310
+ *
1311
+ * A remote copy renders slightly in the past, chasing its target at
1312
+ * ~12.5%/frame. That is the right way to draw one and the wrong way to
1313
+ * COMPARE one: `incanto-multiplay`'s `agrees` rung read the rendered
1314
+ * position, so a late joiner whose interpolation had not converged was
1315
+ * reported as disagreeing with the owner —
1316
+ *
1317
+ * ✗ late has p1's `position` as [-3.3465…], not [-3.3989…]
1318
+ *
1319
+ * — on both SHIPPED 2D multiplayer examples, which exit 1 today on the plain
1320
+ * documented command. The size of the "disagreement" is the player's speed
1321
+ * times the chase lag, so a faster game fails harder while replicating
1322
+ * perfectly.
1323
+ *
1324
+ * `undefined` when this spawner has no target for that node: it is not one of
1325
+ * ours, or `interpolate` is off, in which case the drawn value IS the
1326
+ * received one.
1327
+ */
1328
+ replicatedPosition(node) {
1329
+ return this.positionTargets.get(node);
1211
1330
  }
1212
1331
  stepInterpolation(dt) {
1213
- const f = Math.min(1, dt * 8);
1332
+ const real = this.tree?.engine?.unscaledDelta || dt;
1333
+ const f = Math.min(1, real * 8);
1214
1334
  for (const [target, goal] of this.positionTargets) {
1215
1335
  const pos = target.position;
1216
- if (!Array.isArray(pos)) continue;
1336
+ if (!Array.isArray(pos)) {
1337
+ target.position = [...goal];
1338
+ continue;
1339
+ }
1217
1340
  target.position = pos.map((v, i) => v + ((goal[i] ?? v) - v) * f);
1218
1341
  }
1219
1342
  }
@@ -1276,7 +1399,17 @@ async function createSplitScreen(opts) {
1276
1399
  players.push(player);
1277
1400
  await opts.setup(player, i);
1278
1401
  }
1279
- const disconnect = (players[0]?.engine)?.updated.connect((dt) => void server.tick(Math.min(dt, .05) * 1e3));
1402
+ const first = players[0]?.engine;
1403
+ server.reportTo(first ? { log: {
1404
+ warn: (...p) => first.log.warn(...p),
1405
+ info: (...p) => first.log.info(...p),
1406
+ debug: (...p) => first.log.debug(...p),
1407
+ error: (...p) => {
1408
+ const [head, ...rest] = p;
1409
+ first._asyncFailed(String(head).replace(/^\[incanto\] /, ""), rest[0] ?? "");
1410
+ }
1411
+ } } : null);
1412
+ const disconnect = first?.updated.connect((dt) => void server.tick(Math.min(dt, .05) * 1e3));
1280
1413
  return {
1281
1414
  server,
1282
1415
  players,
@@ -1,5 +1,6 @@
1
+ import { n as jsonEquals } from "./json-CfTjpvW8.js";
1
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { n as jsonEquals } from "./json-CwwhxQgb.js";
3
+ import { h as didYouMean } from "./touch-BnCyPA0G.js";
3
4
  //#region src/core/particle-presets.ts
4
5
  const PARTICLE_PRESETS = {
5
6
  fire: {
@@ -319,16 +320,46 @@ function resolveAnimation(animations, name, owner) {
319
320
  key = entry;
320
321
  }
321
322
  }
323
+ /** The whole of a clip definition — anything else in one is a mistake. */
324
+ const DEF_KEYS = [
325
+ "frames",
326
+ "fps",
327
+ "loop"
328
+ ];
322
329
  /**
323
- * Every alias resolves — checked once, at ready, rather than on the frame the
324
- * character first jumps.
330
+ * Every clip and every alias — checked once, at ready, rather than on the frame
331
+ * the character first jumps.
325
332
  *
326
333
  * A cosmetic map that only fails the moment a state is first entered is the
327
- * shape of bug that reaches players: the game loads, plays, and dies on a
328
- * ledge twenty minutes in.
334
+ * shape of bug that reaches players: the game loads, plays, and dies on a ledge
335
+ * twenty minutes in.
336
+ *
337
+ * That was the argument for checking the aliases, and it was only ever applied
338
+ * to them: the clip DEFINITIONS beside them were first read by
339
+ * `resolveAnimation`, which `play()` calls. So a map like
340
+ * `{"walk": {"frame": [0,7], "fps": 10}}` — `frames` misspelled, the likeliest
341
+ * of the mistakes and the quietest, because `frame`, `frameRate`, `speed` and
342
+ * `repeat` are all real fields in the sheet formats people convert from —
343
+ * loaded clean, passed `incanto-check`, and was a hard error the first time the
344
+ * character walked. The one mistake caught at load was `autoplay` naming a clip
345
+ * that does not exist: the name you play was checked, the clip you play was not.
329
346
  */
330
- function validateAnimationAliases(animations, owner) {
331
- for (const [name, entry] of Object.entries(animations)) if (typeof entry === "string") resolveAnimation(animations, name, owner);
347
+ function validateAnimations(animations, owner) {
348
+ for (const [name, entry] of Object.entries(animations)) {
349
+ if (typeof entry === "string") {
350
+ resolveAnimation(animations, name, owner);
351
+ continue;
352
+ }
353
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new IncantoError("BAD_FORMAT", `Animation '${name}' on '${owner}' is ${Array.isArray(entry) ? "a list" : `a ${typeof entry}`} — a clip is {"frames": [start, end], "fps": 10} or the name of another clip.`);
354
+ const def = entry;
355
+ for (const key of Object.keys(def)) {
356
+ if (DEF_KEYS.includes(key)) continue;
357
+ throw new IncantoError("BAD_FORMAT", `Animation '${name}' on '${owner}' has an unknown field "${key}".` + didYouMean(key, DEF_KEYS) + " A clip is {frames, fps, loop} — the rate is \"fps\", not frameRate or speed, and looping is \"loop\": true, not a repeat count.");
358
+ }
359
+ if (def.loop !== void 0 && typeof def.loop !== "boolean") throw new IncantoError("BAD_FORMAT", `Animation '${name}' on '${owner}': "loop" is true or false, not ${JSON.stringify(def.loop)}.`);
360
+ if (def.fps !== void 0 && typeof def.fps !== "number") throw new IncantoError("BAD_FORMAT", `Animation '${name}' on '${owner}': "fps" is a number, not ${JSON.stringify(def.fps)}.`);
361
+ resolveAnimation(animations, name, owner);
362
+ }
332
363
  }
333
364
  /**
334
365
  * `[start, end]` is a RANGE; anything else is an explicit list.
@@ -349,4 +380,4 @@ function resolveFrames(frames, name) {
349
380
  return [...frames];
350
381
  }
351
382
  //#endregion
352
- export { PARTICLE_PRESETS as a, ParticleSim as i, resolveFrames as n, PARTICLE_PRESET_NAMES as o, validateAnimationAliases as r, applyParticlePreset as s, resolveAnimation as t };
383
+ export { PARTICLE_PRESETS as a, ParticleSim as i, resolveFrames as n, PARTICLE_PRESET_NAMES as o, validateAnimations as r, applyParticlePreset as s, resolveAnimation as t };
@@ -197,23 +197,8 @@ async function preloadSceneAssets(assets, title) {
197
197
  }
198
198
  }
199
199
  //#endregion
200
- //#region src/core/uid-gen.ts
201
- /**
202
- * The ONE way to mint a node uid: crypto-strength, `n_` + 16 base36 chars
203
- * (~82 bits). Hand-written uids are forbidden by convention — they break the
204
- * collision-strength contract and read as fakes next to generated ones.
205
- */
206
- function newUid() {
207
- const bytes = new Uint8Array(16);
208
- if (globalThis.crypto?.getRandomValues) globalThis.crypto.getRandomValues(bytes);
209
- else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
210
- let id = "";
211
- for (const b of bytes) id += (b % 36).toString(36);
212
- return `n_${id.slice(0, 16)}`;
213
- }
214
- //#endregion
215
200
  //#region src/index.ts
216
201
  /** Engine version. Kept in sync with package.json by the release pipeline. */
217
- const VERSION = "0.69.0";
202
+ const VERSION = "0.71.0";
218
203
  //#endregion
219
- export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
204
+ export { findPath as a, preloadUrls as i, assetUrls as n, gridFromRows as o, preloadSceneAssets as r, VERSION as t };