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,441 @@
1
+ import { Dt as Node, Nt as Signal, b as Engine } from "./behavior-uPEuZrUB.js";
2
+ import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
3
+
4
+ //#region src/net/types.d.ts
5
+ type Unsubscribe = () => void;
6
+ /**
7
+ * Incanto's pluggable multiplayer transport contract. The engine speaks ONLY
8
+ * this interface — any backend works by implementing it:
9
+ *
10
+ * - `LoopbackTransport` (built in): in-memory — offline dev, tests, split-screen
11
+ * - `createAgent8Server()` (built in): adapter for `@agent8/gameserver`
12
+ * - your own: Socket.IO, Colyseus, Supabase Realtime, a custom WebSocket server…
13
+ *
14
+ * The room protocol a transport's backend must answer via `remoteFunction`:
15
+ * `joinRoom(roomId?) → roomId` · `leaveRoom(roomId)` · `setMyState(roomId, patch)`
16
+ * (SHALLOW merge) · `patchRoomState(roomId, patch)` · `addEntity(roomId, coll,
17
+ * entity) → id` · `updateEntity(roomId, coll, id, patch)` · `removeEntity(roomId,
18
+ * coll, id)` · `sendEvent(roomId, type, payload)`.
19
+ * (The shape happens to align with @agent8/gameserver's GameServer class, so that
20
+ * adapter is thin — but nothing in the engine depends on that backend.)
21
+ */
22
+ interface NetworkTransport {
23
+ readonly account: string;
24
+ readonly connected: boolean;
25
+ connect(): Promise<boolean>;
26
+ disconnect(): Promise<void>;
27
+ /** Calls a function defined in the game's server.js (see templates/agent8-server.js). */
28
+ remoteFunction(fn: string, args?: unknown[], opts?: {
29
+ needResponse?: boolean;
30
+ throttle?: number;
31
+ throttleKey?: string;
32
+ }): Promise<unknown>;
33
+ subscribeRoomState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe;
34
+ subscribeRoomMyState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe;
35
+ subscribeRoomAllUserStates(roomId: string, cb: (states: Record<string, JsonObject>) => void): Unsubscribe;
36
+ subscribeRoomCollection(roomId: string, collectionId: string, cb: (entities: Record<string, JsonObject>) => void): Unsubscribe;
37
+ onRoomMessage(roomId: string, type: string, cb: (message: JsonValue) => void): Unsubscribe;
38
+ onRoomUserJoin(roomId: string, cb: (account: string) => void): Unsubscribe;
39
+ onRoomUserLeave(roomId: string, cb: (account: string) => void): Unsubscribe;
40
+ /**
41
+ * GLOBAL (cross-room, persistent) channels — OPTIONAL. A backend that has no
42
+ * global tier (e.g. the fixed `LoopbackHub`) simply omits them; NetworkManager
43
+ * degrades gracefully (its global signals never fire). `LocalGameServer` and the
44
+ * agent8 adapter implement them. Each fires the current value once on subscribe.
45
+ */
46
+ subscribeGlobalState?(cb: (state: JsonObject) => void): Unsubscribe;
47
+ /** The caller's own global user state. */
48
+ subscribeGlobalMyState?(cb: (state: JsonObject) => void): Unsubscribe;
49
+ subscribeGlobalUserState?(account: string, cb: (state: JsonObject) => void): Unsubscribe;
50
+ subscribeGlobalCollection?(collectionId: string, cb: (entities: Record<string, JsonObject>) => void): Unsubscribe;
51
+ /** An account's currency ledger (`$asset`). */
52
+ subscribeAsset?(account: string, cb: (assets: Record<string, number>) => void): Unsubscribe;
53
+ onGlobalMessage?(type: string, cb: (message: JsonValue) => void): Unsubscribe;
54
+ }
55
+ //#endregion
56
+ //#region src/net/loopback.d.ts
57
+ interface Room {
58
+ users: Map<string, JsonObject>;
59
+ state: JsonObject;
60
+ collections: Map<string, Map<string, JsonObject>>;
61
+ }
62
+ /**
63
+ * In-memory multiplayer hub implementing the SAME kernel contract as
64
+ * `templates/agent8-server.js` — local split-screen demos, offline development,
65
+ * and headless tests run real multi-client flows without any infrastructure.
66
+ */
67
+ declare class LoopbackHub {
68
+ private readonly rooms;
69
+ private accountSeq;
70
+ private roomSeq;
71
+ private entitySeq;
72
+ /** Per-room listener registries, keyed by roomId. */
73
+ private readonly listeners;
74
+ createClient(account?: string): LoopbackTransport;
75
+ /** @internal */
76
+ _room(roomId: string): Room;
77
+ /** @internal The server.js kernel, in memory. */
78
+ _call(account: string, fn: string, args: unknown[]): unknown;
79
+ /** @internal */
80
+ _subscribe<K extends keyof LoopbackHub["listeners"]>(kind: K, roomId: string, entry: LoopbackHub["listeners"][K] extends Map<string, Set<infer E>> ? E : never): Unsubscribe;
81
+ /** @internal */
82
+ _snapshotUsers(roomId: string): Record<string, JsonObject>;
83
+ /** @internal */
84
+ _snapshotCollection(roomId: string, collectionId: string): Record<string, JsonObject>;
85
+ /** @internal */
86
+ _snapshotRoomState(roomId: string): JsonObject;
87
+ private fire;
88
+ private fireAllUsers;
89
+ private fireCollection;
90
+ }
91
+ /** A client on a LoopbackHub. Subscriptions fire immediately with a snapshot. */
92
+ declare class LoopbackTransport implements NetworkTransport {
93
+ private readonly hub;
94
+ readonly account: string;
95
+ connected: boolean;
96
+ constructor(hub: LoopbackHub, account: string);
97
+ connect(): Promise<boolean>;
98
+ disconnect(): Promise<void>;
99
+ remoteFunction(fn: string, args?: unknown[]): Promise<unknown>;
100
+ subscribeRoomState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe;
101
+ subscribeRoomMyState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe;
102
+ subscribeRoomAllUserStates(roomId: string, cb: (states: Record<string, JsonObject>) => void): Unsubscribe;
103
+ subscribeRoomCollection(roomId: string, collectionId: string, cb: (entities: Record<string, JsonObject>) => void): Unsubscribe;
104
+ onRoomMessage(roomId: string, type: string, cb: (message: JsonValue) => void): Unsubscribe;
105
+ onRoomUserJoin(roomId: string, cb: (account: string) => void): Unsubscribe;
106
+ onRoomUserLeave(roomId: string, cb: (account: string) => void): Unsubscribe;
107
+ }
108
+ //#endregion
109
+ //#region src/net/local-game-server.d.ts
110
+ /** A user's `server/src/server.ts` `Server` class (or any class with room methods). */
111
+ type ServerClass = new () => object;
112
+ interface LocalGameServerOptions {
113
+ /**
114
+ * The game's `Server` class from `server/src/server.ts` (the v2 structured
115
+ * project). Omit it to fall back to the built-in kernel only — identical to a
116
+ * raw `LoopbackHub`.
117
+ */
118
+ server?: ServerClass;
119
+ }
120
+ /**
121
+ * Run a multiplayer game's REAL agent8-sdk-v2 `Server` class **locally**, in
122
+ * memory, with NO cloud and NO auth — the preview/dev path.
123
+ *
124
+ * `LoopbackHub` answers only the fixed built-in room protocol (joinRoom,
125
+ * setMyState, collections, events). Real games add server-AUTHORITATIVE rules
126
+ * (`awardPoint`, `castSpell`, `$roomTick` match clocks) in `server/src/server.ts`
127
+ * against the v2 contexts `$sender`/`$global`/`$room`/`$lock`. LocalGameServer
128
+ * runs that exact class body so the whole game — client + server logic — is
129
+ * playable in dev before it ever touches the Agent8 platform. Swap
130
+ * `local.createClient(account)` for `await createAgent8Server()` to go live;
131
+ * nothing else changes.
132
+ *
133
+ * How it stays faithful to the isolated-vm platform:
134
+ * - A FRESH `Server` instance runs per request, so `this.*` never persists.
135
+ * - The v2 globals are injected on `globalThis` for the duration of each call
136
+ * (the SAME `server/src/server.ts` body that reads bare `$room`/`$sender`
137
+ * runs unmodified).
138
+ * - Calls are SERIALIZED through one queue, so a global binding can't leak across
139
+ * another request's `await`s (the platform isolates per request; we serialize).
140
+ * - `$roomTick(deltaMS, roomId)` runs only while a room has users — drive it from
141
+ * the engine: `engine.updated.connect((dt) => local.tick(dt * 1000))`.
142
+ *
143
+ * NOT a substitute for the cloud: no isolated-vm sandboxing, no persistence, no
144
+ * rate limits — a faithful FUNCTIONAL emulator for local play and tests.
145
+ */
146
+ declare class LocalGameServer {
147
+ private readonly hub;
148
+ private readonly serverClass?;
149
+ /** Each client's current room — binds `$sender.roomId` on later calls. */
150
+ private readonly clientRooms;
151
+ /** All remoteFunction/tick calls run one-at-a-time so injected globals never overlap. */
152
+ private queue;
153
+ /** Per-key mutex backing `$lock`. */
154
+ private readonly locks;
155
+ /** Persistent (process-lifetime) store backing the `$global` + `$asset` contexts. */
156
+ private readonly g_state;
157
+ private readonly g_userStates;
158
+ private readonly g_collections;
159
+ private readonly g_assets;
160
+ private g_seq;
161
+ /** Client-side global/asset subscription registries (fanned out on mutation). */
162
+ private readonly subState;
163
+ private readonly subUser;
164
+ private readonly subColl;
165
+ private readonly subAsset;
166
+ private readonly subMsg;
167
+ constructor(serverClass?: ServerClass);
168
+ /** A client on this server. Distinct `account` per player (default auto-assigned). */
169
+ createClient(account?: string): LocalGameServerTransport;
170
+ private collectionSnapshot;
171
+ private assetSnapshot;
172
+ /** @internal */
173
+ subscribeGlobalState(cb: (s: JsonObject) => void): Unsubscribe;
174
+ /** @internal */
175
+ subscribeGlobalUserState(account: string, cb: (s: JsonObject) => void): Unsubscribe;
176
+ /** @internal */
177
+ subscribeGlobalCollection(id: string, cb: (r: Record<string, JsonObject>) => void): Unsubscribe;
178
+ /** @internal */
179
+ subscribeAsset(account: string, cb: (a: Record<string, number>) => void): Unsubscribe;
180
+ /** @internal Messages are events — no immediate snapshot. */
181
+ onGlobalMessage(account: string, type: string, cb: (m: JsonValue) => void): Unsubscribe;
182
+ private fireState;
183
+ private fireUser;
184
+ private fireColl;
185
+ private fireAsset;
186
+ private fireMsg;
187
+ /**
188
+ * Advance server-driven periodic logic. Calls the `Server`'s `$roomTick(deltaMS,
189
+ * roomId)` once per room that currently has users. No-op without a `$roomTick`.
190
+ */
191
+ tick(deltaMS: number): Promise<void>;
192
+ /** @internal Dispatch a remoteFunction (serialized). */
193
+ call(account: string, fn: string, args: unknown[]): Promise<unknown>;
194
+ private enqueue;
195
+ private dispatch;
196
+ private runTick;
197
+ /** Install the v2 globals for one dispatch; returns a restorer. */
198
+ private bindGlobals;
199
+ /**
200
+ * The `$global` context — room membership, persistent GLOBAL state/user-state/
201
+ * collections, room management, and global messaging. Backed by an in-memory
202
+ * store that survives between requests (unlike rooms, which the hub clears when
203
+ * empty) — matching the platform's persistent-global / ephemeral-room split.
204
+ */
205
+ private globalContext;
206
+ /**
207
+ * The `$asset` context — a per-account currency ledger. `burn`/`transfer` throw
208
+ * on an insufficient balance (the documented pattern always checks `has` first),
209
+ * surfacing economy bugs in preview just as the platform would.
210
+ */
211
+ private assetContext;
212
+ /** The `$room` context, bound to one room — maps onto the hub's kernel primitives. */
213
+ private roomContext;
214
+ private withLock;
215
+ }
216
+ /** Firebase-style query options for `$global`/`$room` collections (preview subset). */
217
+ interface CollectionOptions {
218
+ filters?: {
219
+ field: string;
220
+ operator: string;
221
+ value: JsonValue;
222
+ }[];
223
+ orderBy?: {
224
+ field: string;
225
+ direction?: "asc" | "desc";
226
+ }[];
227
+ limit?: number;
228
+ startAfter?: JsonValue;
229
+ endBefore?: JsonValue;
230
+ }
231
+ /** Construct a local preview server. With no `server`, it equals a raw LoopbackHub. */
232
+ declare function createLocalGameServer(opts?: LocalGameServerOptions): LocalGameServer;
233
+ /**
234
+ * A client of a {@link LocalGameServer}. Subscriptions read the shared in-memory
235
+ * store directly (same fan-out as Loopback); `remoteFunction` routes through the
236
+ * server's serialized dispatch so the game's `Server` class actually runs.
237
+ */
238
+ declare class LocalGameServerTransport implements NetworkTransport {
239
+ private readonly server;
240
+ private readonly loop;
241
+ readonly account: string;
242
+ connected: boolean;
243
+ constructor(server: LocalGameServer, loop: LoopbackTransport);
244
+ connect(): Promise<boolean>;
245
+ disconnect(): Promise<void>;
246
+ remoteFunction(fn: string, args?: unknown[]): Promise<unknown>;
247
+ subscribeRoomState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe;
248
+ subscribeRoomMyState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe;
249
+ subscribeRoomAllUserStates(roomId: string, cb: (states: Record<string, JsonObject>) => void): Unsubscribe;
250
+ subscribeRoomCollection(roomId: string, collectionId: string, cb: (entities: Record<string, JsonObject>) => void): Unsubscribe;
251
+ onRoomMessage(roomId: string, type: string, cb: (message: JsonValue) => void): Unsubscribe;
252
+ onRoomUserJoin(roomId: string, cb: (account: string) => void): Unsubscribe;
253
+ onRoomUserLeave(roomId: string, cb: (account: string) => void): Unsubscribe;
254
+ subscribeGlobalState(cb: (state: JsonObject) => void): Unsubscribe;
255
+ subscribeGlobalMyState(cb: (state: JsonObject) => void): Unsubscribe;
256
+ subscribeGlobalUserState(account: string, cb: (state: JsonObject) => void): Unsubscribe;
257
+ subscribeGlobalCollection(collectionId: string, cb: (entities: Record<string, JsonObject>) => void): Unsubscribe;
258
+ subscribeAsset(account: string, cb: (assets: Record<string, number>) => void): Unsubscribe;
259
+ onGlobalMessage(type: string, cb: (message: JsonValue) => void): Unsubscribe;
260
+ }
261
+ //#endregion
262
+ //#region src/net/network-manager.d.ts
263
+ interface NetworkManagerOptions {
264
+ /**
265
+ * The transport to speak through — `LoopbackTransport` (offline/dev/tests),
266
+ * `await createAgent8Server()` (agent8 platform), or any custom
267
+ * NetworkTransport implementation. Default: the agent8 adapter.
268
+ */
269
+ transport?: NetworkTransport;
270
+ /** Room id; default: scene `multiplayer.room` ('auto' = server-assigned). */
271
+ room?: string;
272
+ /** Config for the default agent8 adapter (ignored when `transport` is injected). */
273
+ config?: {
274
+ verse?: string;
275
+ account?: string;
276
+ auth?: string;
277
+ };
278
+ /** Replication send window in ms (default 50; node `network.throttleMs` can lower it). */
279
+ throttleMs?: number;
280
+ }
281
+ /**
282
+ * Per-engine multiplayer hub: joins a room, exposes the room channels as core
283
+ * Signals, replicates `network: {mode:'owner'}` node props on a throttle
284
+ * window, and resolves registered scenes for NetworkSpawner.
285
+ */
286
+ declare class NetworkManager {
287
+ readonly account: string;
288
+ readonly roomId: string;
289
+ readonly roomState: Signal<[JsonObject]>;
290
+ readonly allUserStates: Signal<[Record<string, JsonObject>]>;
291
+ readonly userJoined: Signal<[string]>;
292
+ readonly userLeft: Signal<[string]>;
293
+ /**
294
+ * GLOBAL (cross-room, persistent) channels — populated only when the transport
295
+ * supports them (`LocalGameServer`, the agent8 adapter). On a transport without a
296
+ * global tier they stay empty / never fire.
297
+ */
298
+ readonly globalState: Signal<[JsonObject]>;
299
+ readonly globalMyState: Signal<[JsonObject]>;
300
+ /** The local account's `$asset` ledger. */
301
+ readonly asset: Signal<[Record<string, number>]>;
302
+ /** Latest snapshots for polling consumers (NetworkSpawner). */
303
+ latestUserStates: Record<string, JsonObject>;
304
+ latestRoomState: JsonObject;
305
+ latestGlobalState: JsonObject;
306
+ latestGlobalMyState: JsonObject;
307
+ latestAsset: Record<string, number>;
308
+ private readonly server;
309
+ private readonly engine;
310
+ private readonly throttleMs;
311
+ private readonly subs;
312
+ private readonly messageSignals;
313
+ private readonly collectionSignals;
314
+ private readonly latestCollections;
315
+ private readonly globalMessageSignals;
316
+ private readonly globalCollectionSignals;
317
+ private readonly latestGlobalCollections;
318
+ private readonly scenes;
319
+ private readonly lastSent;
320
+ private sendAccumulator;
321
+ private detachReplication;
322
+ private lastOwner;
323
+ private boundScene;
324
+ static get(engine: Engine): NetworkManager | null;
325
+ static create(engine: Engine, opts?: NetworkManagerOptions): Promise<NetworkManager>;
326
+ private constructor();
327
+ /** Signal for a typed room message (`sendEvent` on any client). */
328
+ message(type: string): Signal<[JsonValue]>;
329
+ /** Signal + snapshot for a room collection (spawned entities). */
330
+ collection(id: string): Signal<[Record<string, JsonObject>]>;
331
+ latestCollection(id: string): Record<string, JsonObject>;
332
+ /** Signal for a typed GLOBAL message (`$global.broadcastToAll`/`sendMessageToUser`). */
333
+ globalMessage(type: string): Signal<[JsonValue]>;
334
+ /** Signal + snapshot for a GLOBAL collection (persistent, cross-room). */
335
+ globalCollection(id: string): Signal<[Record<string, JsonObject>]>;
336
+ latestGlobalCollection(id: string): Record<string, JsonObject>;
337
+ setMyState(patch: JsonObject): Promise<unknown>;
338
+ patchRoomState(patch: JsonObject): Promise<unknown>;
339
+ addEntity(collectionId: string, entity: JsonObject): Promise<unknown>;
340
+ updateEntity(collectionId: string, id: string, patch: JsonObject): Promise<unknown>;
341
+ removeEntity(collectionId: string, id: string): Promise<unknown>;
342
+ sendEvent(type: string, payload: JsonValue): Promise<unknown>;
343
+ /**
344
+ * Invoke a CUSTOM server-authoritative function — a method you added to your
345
+ * `server/src/server.ts` `Server` class (run live on the Agent8 platform, or
346
+ * locally via `createLocalGameServer`). The room id is prepended automatically,
347
+ * matching the `(roomId, …)` shape every server method takes:
348
+ *
349
+ * `manager.call('claimCoin', coinId)` → server `claimCoin(roomId, coinId)`
350
+ *
351
+ * Returns the function's result (use it for server-validated outcomes — a
352
+ * rejected claim, a rolled value). This is THE entry point for game rules the
353
+ * client must not be trusted to compute.
354
+ */
355
+ call(fn: string, ...args: JsonValue[]): Promise<unknown>;
356
+ /** Register a scene for NetworkSpawner (`"scene": "<key>"`). */
357
+ /**
358
+ * `json` is `unknown` for the reason `createGame2D.scene` is: an imported
359
+ * `.scene.json` does not satisfy `SceneJson` (`format: number` vs `1`), and
360
+ * the loader validates it properly anyway.
361
+ */
362
+ registerScene(key: string, json: unknown): void;
363
+ resolveScene(key: string): SceneJson;
364
+ dispose(): Promise<void>;
365
+ private replicate;
366
+ }
367
+ /**
368
+ * @internal Applies a replicated flat sync patch (`{'position': …,
369
+ * 'Sprite.animation': …}`) onto a spawned subtree (NetworkSpawner).
370
+ */
371
+ declare function applySyncPatch(root: Node, patch: JsonObject, setProp: (target: Node, prop: string, value: JsonValue) => void, onMissingNode?: (key: string) => void): void;
372
+ //#endregion
373
+ //#region src/net/split-screen.d.ts
374
+ interface SplitScreenPlayer {
375
+ account: string;
376
+ engine: Engine;
377
+ manager: NetworkManager;
378
+ }
379
+ interface SplitScreenOptions extends LocalGameServerOptions {
380
+ /**
381
+ * The shared scene JSON every panel loads (cloned per panel).
382
+ *
383
+ * `unknown`, like `createGame2D`'s: an imported `.scene.json` is typed
384
+ * `{format: number}` and would not satisfy `SceneJson` (`format: 1`), so a
385
+ * declared type here buys nothing but a cast in every consumer's boot file.
386
+ * `loadScene` hard-validates the real thing a line later.
387
+ */
388
+ scene: unknown;
389
+ /** Player accounts, one panel each (default ['p1', 'p2']). */
390
+ accounts?: string[];
391
+ /** Called per player to finish wiring (renderer, input, HUD). */
392
+ setup: (player: SplitScreenPlayer, index: number) => void | Promise<void>;
393
+ /** name → scene JSON registered on every panel's NetworkManager. */
394
+ scenes?: Record<string, unknown>;
395
+ /** Engine seed base (each panel gets seed + index). */
396
+ seed?: number;
397
+ /**
398
+ * The room every panel joins. Default: whatever the FIRST panel got.
399
+ *
400
+ * A shared scene usually says `multiplayer: { room: "auto" }`, and "auto"
401
+ * means a server-ASSIGNED room — so each panel used to land in a room of its
402
+ * own and the players never saw each other. Split screen means one game.
403
+ */
404
+ room?: string;
405
+ /**
406
+ * Rapier per panel. `'auto'` (default) enables it when the scene has bodies,
407
+ * exactly as `createGame2D`/`createGame3D` do.
408
+ *
409
+ * Left out, the one thing that makes a CharacterBody move was the caller's
410
+ * job — in a helper whose whole purpose is booting the engines for you. A
411
+ * two-player game came up with both players frozen at the origin and no error.
412
+ */
413
+ physics?: "auto" | boolean;
414
+ }
415
+ /**
416
+ * N local clients on ONE page sharing ONE in-memory game server — the
417
+ * split-screen preview harness three examples were hand-wiring (~40 lines
418
+ * each). Wires: LocalGameServer + per-player Engine/scene/NetworkManager +
419
+ * the server tick driven from the first panel's clock.
420
+ *
421
+ * const { players, server } = await createSplitScreen({
422
+ * scene: gameJson,
423
+ * server: MyServerClass,
424
+ * scenes: { 'remote-player': remoteJson },
425
+ * setup: ({ engine }, i) => {
426
+ * new Renderer2D({ canvas: canvases[i], engine });
427
+ * engine.input.attachKeyboard(window);
428
+ * engine.start();
429
+ * },
430
+ * });
431
+ *
432
+ * Going live stays the one-line story: replace the transport with
433
+ * `createAgent8Server()` and boot ONE client per browser.
434
+ */
435
+ declare function createSplitScreen(opts: SplitScreenOptions): Promise<{
436
+ server: ReturnType<typeof createLocalGameServer>;
437
+ players: SplitScreenPlayer[];
438
+ dispose(): void;
439
+ }>;
440
+ //#endregion
441
+ export { NetworkManagerOptions as a, LocalGameServer as c, ServerClass as d, createLocalGameServer as f, Unsubscribe as g, NetworkTransport as h, NetworkManager as i, LocalGameServerOptions as l, LoopbackTransport as m, SplitScreenPlayer as n, applySyncPatch as o, LoopbackHub as p, createSplitScreen as r, CollectionOptions as s, SplitScreenOptions as t, LocalGameServerTransport as u };
@@ -161,6 +161,6 @@ function newUid() {
161
161
  //#endregion
162
162
  //#region src/index.ts
163
163
  /** Engine version. Kept in sync with package.json by the release pipeline. */
164
- const VERSION = "0.51.0";
164
+ const VERSION = "0.52.0";
165
165
  //#endregion
166
166
  export { findPath as a, preloadUrls as i, newUid as n, gridFromRows as o, assetUrls as r, VERSION as t };
@@ -152,7 +152,8 @@ async function openBundledEditor(opts) {
152
152
  mode: "project",
153
153
  root: "",
154
154
  input: null,
155
- output: null
155
+ output: null,
156
+ hasScene: true
156
157
  }),
157
158
  loadScene: async (file) => file ? await readProjectScene(file) : opts.scene,
158
159
  ...opts.save || project ? { saveScene: async (json, file) => {