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.
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1205 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- 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-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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: (
|
|
512
|
-
this.fireMsg(type, message,
|
|
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: (
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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))
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
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
|
|
331
|
-
for (const [name, entry] of Object.entries(animations))
|
|
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,
|
|
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.
|
|
202
|
+
const VERSION = "0.71.0";
|
|
218
203
|
//#endregion
|
|
219
|
-
export {
|
|
204
|
+
export { findPath as a, preloadUrls as i, assetUrls as n, gridFromRows as o, preloadSceneAssets as r, VERSION as t };
|