incanto 0.51.0 → 0.53.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 (67) hide show
  1. package/dist/2d.d.ts +86 -3
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +9 -3
  4. package/dist/3d.js +4 -4
  5. package/dist/{behavior-rZNfzVbH.d.ts → behavior-Do0Da56m.d.ts} +108 -0
  6. package/dist/{create-game-CkzKZ4v5.js → create-game-9D87XaiX.js} +112 -15
  7. package/dist/{create-game-BNaOC3Px.js → create-game-CDJ1lVqK.js} +21 -13
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/debug.js +1 -1
  10. package/dist/{duplicate-EybyYeyL.js → duplicate-MNLMAcbz.js} +1 -1
  11. package/dist/editor.js +1931 -1579
  12. package/dist/{environment-presets-C08pOC6H.js → environment-presets-SkGanr2s.js} +3 -3
  13. package/dist/{gameplay-bStgtZBV.js → gameplay-CRYw_Q-T.js} +6 -4
  14. package/dist/gameplay.d.ts +1 -1
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.d.ts +237 -5
  17. package/dist/index.js +9 -9
  18. package/dist/{loader-B2asghWa.js → loader-BTkHYrQn.js} +86 -50
  19. package/dist/{loader-Dkbn56KC.d.ts → loader-Cu_7kJDy.d.ts} +1 -1
  20. package/dist/{log-report-lxrQY9cH.js → log-report-CPFm4OXf.js} +0 -0
  21. package/dist/net.d.ts +15 -410
  22. package/dist/net.js +1 -822
  23. package/dist/{pathfinding-Bz34pvQD.d.ts → pathfinding-HNGFqGUZ.d.ts} +24 -1
  24. package/dist/{physics-2d-Ccq2L9R0.js → physics-2d-BhZZ-HAp.js} +2 -2
  25. package/dist/{physics-3d-B2c5KNYh.js → physics-3d-DmyO2oaN.js} +3 -3
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-CGq-Hee8.js → register-BTomIiYG.js} +305 -25
  29. package/dist/{register-B_BaaGx-.js → register-DROK2l7J.js} +3 -3
  30. package/dist/{registry-IyWCGe4q.js → registry-C7u42TID.js} +23 -1
  31. package/dist/{replay-DIP2_as4.d.ts → replay-9Fy6C10F.d.ts} +1 -1
  32. package/dist/{replay-BgKxGcXH.js → replay-DYNUL4BU.js} +195 -2
  33. package/dist/split-screen-CaF9hO7g.js +1267 -0
  34. package/dist/split-screen-DNmcX1Pz.d.ts +441 -0
  35. package/dist/{src-C3UwzYXl.js → src-C9xyZW7M.js} +1 -1
  36. package/dist/{teardown-BKTCzLek.js → teardown-Bw2aeyGI.js} +32 -2
  37. package/dist/{test-D2gRpk5V.js → test-CJRsciFk.js} +119 -22
  38. package/dist/test.d.ts +60 -5
  39. package/dist/test.js +3 -3
  40. package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
  41. package/dist/vite.js +2 -2
  42. package/editor/assets/{agent8-DbX_msaO.js → agent8-PlFHzJsh.js} +1 -1
  43. package/editor/assets/{debug-C-kMxdl1.js → debug-CebV7CDW.js} +1 -1
  44. package/editor/assets/index-CdbsY31G.js +10958 -0
  45. package/editor/index.html +1 -1
  46. package/package.json +1 -1
  47. package/schemas/scene.schema.json +213 -0
  48. package/skills/incanto-assets.md +4 -1
  49. package/skills/incanto-audio.md +97 -10
  50. package/skills/incanto-building-2d-games.md +20 -3
  51. package/skills/incanto-editor.md +48 -11
  52. package/skills/incanto-gameplay-behaviors.md +14 -5
  53. package/skills/incanto-hud.md +7 -0
  54. package/skills/incanto-multiplayer.md +39 -3
  55. package/skills/incanto-node-reference.md +34 -0
  56. package/skills/incanto-physics-and-input.md +38 -0
  57. package/skills/incanto-verifying-your-game.md +75 -1
  58. package/skills/incanto-web-integration.md +1 -0
  59. package/templates/agent8-server.ts +79 -2
  60. package/templates-app/beacon-isle-3d/index.html +3 -3
  61. package/templates-app/beacon-isle-3d/package.json +1 -1
  62. package/templates-app/tps-3d/index.html +3 -3
  63. package/templates-app/tps-3d/package.json +1 -1
  64. package/templates-app/village-quest-3d/index.html +3 -3
  65. package/templates-app/village-quest-3d/package.json +1 -1
  66. package/dist/register-CLVhzWcI.js +0 -374
  67. package/editor/assets/index-CUc1U7wm.js +0 -10951
@@ -58,13 +58,22 @@ Authority rules:
58
58
  applies onto the spawned scene's root; `position` lerps when `interpolate: true`
59
59
  (remote entities render slightly in the past — that's correct). Emits
60
60
  `spawned(node, key)` / `despawned(node, key)`. `source: "collection:<id>"` mirrors a
61
- room collection by `__id`.
61
+ room collection by `__id` — a collection ENTITY applies as your server wrote it
62
+ (`addCollectionItem('coins', { position: [x, y] })` lands on the spawned node;
63
+ the `{sync: {…}}` envelope an owner state uses is accepted too). A key whose
64
+ node PATH resolves to nothing (`Skin.animation` on a scene with no `Skin`) is
65
+ reported once rather than dropped — that value replicates to nowhere.
62
66
 
63
67
  ## Boot
64
68
 
65
69
  ```ts
70
+ import { registerNodes2D } from 'incanto/2d'; // or registerNodes3D for a 3D game
66
71
  import { NetworkManager, registerNodesNet, LoopbackHub } from 'incanto/net';
67
72
 
73
+ // REGISTRARS COMPOSE and you need BOTH: `registerNodesNet()` adds NetworkSpawner
74
+ // and nothing else, so a 2D scene loaded after it alone dies on its own root
75
+ // node (`Unknown node type 'Node2D'`).
76
+ registerNodes2D();
68
77
  registerNodesNet();
69
78
  const scene = loadScene(json);
70
79
  engine.setScene(scene);
@@ -147,8 +156,12 @@ the whole N-panel harness — one LocalGameServer, one engine + NetworkManager
147
156
  per player — so you only supply the per-panel renderer/input:
148
157
 
149
158
  ```ts
159
+ import { registerNodes2D } from 'incanto/2d'; // the dimension your scene uses
150
160
  import { createSplitScreen, registerNodesNet } from 'incanto/net';
151
161
 
162
+ registerNodes2D();
163
+ registerNodesNet();
164
+
152
165
  const canvases = [document.getElementById('p1'), document.getElementById('p2')];
153
166
  const { players, server, dispose } = await createSplitScreen({
154
167
  scene: gameJson, // shared scene (each panel gets its own copy)
@@ -163,8 +176,13 @@ const { players, server, dispose } = await createSplitScreen({
163
176
  });
164
177
  ```
165
178
 
166
- The first panel's clock pumps `server.tick` (so `$roomTick` runs) — don't add
167
- your own. `dispose()` tears every panel down. Going live is unchanged: ONE
179
+ Each panel gets physics on the same terms `createGame2D` gives it (`'auto'`:
180
+ Rapier when the scene has bodies; `physics: false` opts out) — without it a
181
+ `CharacterBody2D` never moves. The first panel's clock pumps `server.tick` (so
182
+ `$roomTick` runs) — don't add your own. Every panel joins ONE room: a shared scene usually says
183
+ `multiplayer: { room: "auto" }`, and "auto" means a server-ASSIGNED room, so the
184
+ harness pins panels 1..N to the room panel 0 got (`room: 'lobby'` overrides).
185
+ `dispose()` tears every panel down. Going live is unchanged: ONE
168
186
  client per browser with `createAgent8Server()` as the transport.
169
187
 
170
188
  It runs the SAME class body the cloud runs: the v2 globals (`$sender`/`$global`/
@@ -172,6 +190,24 @@ It runs the SAME class body the cloud runs: the v2 globals (`$sender`/`$global`/
172
190
  (so `this.*` never persists), and calls are serialized (no global leaks across
173
191
  `await`s). `$roomTick(deltaMS, roomId)` runs only while a room has users.
174
192
 
193
+ **Verifying a whole match**: `playMultiplayer` from `incanto/test` runs N clients
194
+ against one in-memory server for a fixed number of simulated seconds and reports
195
+ what they ended up sharing — rooms, who saw whom, what each `NetworkSpawner`
196
+ materialised, per-client frame errors, the final room state. See
197
+ `incanto-verifying-your-game.md`.
198
+
199
+ **Driving one by hand**: server calls are QUEUED and only run when the event loop
200
+ turns, so a synchronous frame loop enqueues a thousand ticks that never execute —
201
+ the match clock stands still and every `call()` result arrives after your
202
+ assertions.
203
+
204
+ ```ts
205
+ for (let f = 0; f * 16.7 < ms; f++) {
206
+ for (const p of players) p.engine.tick(f * 16.7);
207
+ if (f % 6 === 0) await new Promise((r) => setTimeout(r, 0)); // let the server run
208
+ }
209
+ ```
210
+
175
211
  It is a FUNCTIONAL emulator, NOT the platform: no isolated-vm sandbox, no rate
176
212
  limits, and no DURABLE persistence (global state lives only for the preview process
177
213
  — it is not saved across runs, and rooms still clear when empty). It proves your
@@ -1039,6 +1039,20 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
1039
1039
 
1040
1040
  Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1041
1041
 
1042
+ ## `UiMuteToggle` — `incanto`
1043
+
1044
+ | Prop | Default | Kind |
1045
+ |---|---|---|
1046
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1047
+ | `visible` | `true` | boolean |
1048
+ | `focusable` | `true` | boolean |
1049
+ | `draggable` | `false` | boolean |
1050
+ | `dropTarget` | `false` | boolean |
1051
+ | `label` | `""` | string |
1052
+ | `value` | `false` | boolean |
1053
+
1054
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1055
+
1042
1056
  ## `UiPanel` — `incanto`
1043
1057
 
1044
1058
  | Prop | Default | Kind |
@@ -1155,6 +1169,26 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
1155
1169
 
1156
1170
  Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1157
1171
 
1172
+ ## `UiVolumeSlider` — `incanto`
1173
+
1174
+ | Prop | Default | Kind |
1175
+ |---|---|---|
1176
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1177
+ | `visible` | `true` | boolean |
1178
+ | `focusable` | `true` | boolean |
1179
+ | `draggable` | `false` | boolean |
1180
+ | `dropTarget` | `false` | boolean |
1181
+ | `label` | `""` | string |
1182
+ | `value` | `0.5` | number |
1183
+ | `min` | `0` | number |
1184
+ | `max` | `1` | number |
1185
+ | `step` | `0.01` | number |
1186
+ | `width` | `180` | number |
1187
+ | `color` | `"#6ee7dc"` | string |
1188
+ | `bus` | `"master"` | one of: `master` `sfx` `music` |
1189
+
1190
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1191
+
1158
1192
  ## `VoxelGrid3D` — `incanto/3d`
1159
1193
 
1160
1194
  | Prop | Default | Kind |
@@ -142,6 +142,44 @@ disables; they overlay `touchContainer` — default the canvas's parent, give it
142
142
  `position: relative`). Manual boots call `attachTouchControls(engine, container,
143
143
  { force? })` from `incanto`. Wrong touch kinds (`"joystick"` on a button) fail at load.
144
144
 
145
+ **Three things a phone needs that a desktop never shows you**, two of which the
146
+ engine now does for you:
147
+
148
+ - **The canvas owns its touch gestures.** `createGame2D/3D` set
149
+ `touch-action: none` (plus no selection/callout) on the canvas, or the browser
150
+ keeps pan, pinch-zoom and pull-to-refresh over the play surface — a drag meant
151
+ for aim or a virtual stick SCROLLS THE PAGE instead. `pageGestures: true`
152
+ gives them back to the browser (a small canvas inside a scrolling article).
153
+ - **The controls clear the phone's furniture.** They sit
154
+ `calc(24px + env(safe-area-inset-*))` from the edges, off the iPhone home
155
+ indicator — whose band is also the OS's "leave the app" swipe.
156
+ - **Your PAGE has to opt in**, and this part is yours: `env()` is 0 unless the
157
+ document says so, and `100vh` is the toolbar-hidden height, so the bottom strip
158
+ (where the controls are) hides under the browser chrome.
159
+
160
+ ```html
161
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
162
+ <style>
163
+ html, body { margin: 0; height: 100%; overflow: hidden; }
164
+ canvas { display: block; width: 100%; height: 100%; } /* NOT 100vw/100vh */
165
+ </style>
166
+ ```
167
+
168
+ Every `incanto-new` template and every runnable example ships exactly this.
169
+
170
+ Two more phone facts worth knowing:
171
+
172
+ - **Audio unlocks on ANY first gesture** — the canvas, a key, or the on-screen
173
+ stick and buttons. (Those controls are siblings of the canvas, so their taps
174
+ never reached it; a game with on-screen controls used to stay silent until the
175
+ player happened to touch the play area.)
176
+ - **2D renders at `pixelRatio: 1` by default** (Phaser parity — the browser
177
+ upscales, edges read soft). Phones are 2–3× denser, so a text-heavy or
178
+ vector-heavy 2D game wants
179
+ `createGame2D({ pixelRatio: Math.min(devicePixelRatio, 2) })`, or
180
+ `"environment": { "rendering": { "pixelRatio": "device" } }` in the scene. 3D
181
+ already defaults to `min(dpr, 2)`; pixel art is usually happier left at 1.
182
+
145
183
  Collision layers/masks are not in v0 — everything collides with everything; use groups +
146
184
  `triggerEnter` filtering for game logic.
147
185
 
@@ -314,7 +314,9 @@ They now go to **`engine.log` as well as the console**, so:
314
314
  - `runScript().logs` contains them, and `describe()` prints them under the
315
315
  failures (`! warn: …`).
316
316
  - The debug overlay's **Logs** panel shows them live.
317
- - `game.assetErrors()` lists every asset that failed **with its reason and ref**
317
+ - `game.assetErrors()` lists every asset that failed **with its reason and ref** —
318
+ textures, models, AND sounds (an `AudioPlayer` whose `src` the browser cannot
319
+ play), in 2D and 3D alike
318
320
  — the direct answer to "why is my model not there". The store itself is
319
321
  reachable as `renderer.assets` in both dimensions.
320
322
 
@@ -619,6 +621,23 @@ rows until the orange box lands on the thing you're hunting.
619
621
  inside a `static` subtree, HUD widgets outside a HudLayer. Programmatic:
620
622
  `auditScene(json)` from `incanto` or `incanto/test` returns the warnings.
621
623
 
624
+ **Including a node path that points at nothing.** `Chase.target`,
625
+ `Camera2D.follow`, `Spawner.prefab`, `Joint3D.target`, `skinPath`, `terrain` —
626
+ these hold a path to another node, and a wrong one is SILENT: a connection that
627
+ dangles is a hard load error, but a prop resolves with `getNodeOrNull`, so the
628
+ scene opens and the enemy simply never chases.
629
+
630
+ ```
631
+ warn: World/Enemy: Chase.target — '%Playerr' matches no node in this scene.
632
+ The scene loads and the prop does nothing.
633
+ warn: World/Cam: follow — '/Level/Player' starts at 'Level', but the scene
634
+ root is 'World' (or write '/root/…'). The scene loads and the prop does nothing.
635
+ ```
636
+
637
+ An empty value is never reported — `""` is the default of most of these and
638
+ means "not set". A behavior of YOUR OWN is not reported either: the checker
639
+ never loads your TypeScript, so it cannot know which of its props are paths.
640
+
622
641
  ## Deterministic replay (record once, regression-test forever)
623
642
 
624
643
  The engine is fully deterministic under a seed + injected clock, so a
@@ -644,6 +663,61 @@ never Math.random, dt/`engine.time` never Date.now. Gamepads replay through
644
663
  the ACTIONS they were bound to, not raw pad state.
645
664
 
646
665
 
666
+ ## Sound: did the right thing sound?
667
+
668
+ Audio makes no sound in the VM, but the calls still happen — so the wiring is
669
+ checkable where the game can be run a thousand times:
670
+
671
+ ```ts
672
+ session.engine.audio.clearLog();
673
+ hitTheEnemy();
674
+ session.step(200);
675
+ expect(session.engine.audio.countOf('hit')).toBe(1);
676
+ ```
677
+
678
+ `engine.audio.recent()` is the last 200 sounds with their node path, bus and
679
+ time; `countOf(name)` counts one. A clip whose FILE is broken shows up in
680
+ `assetErrors()` instead. Full guide: `incanto-audio.md`.
681
+
682
+ ## Multiplayer: do the players end up in the SAME world?
683
+
684
+ Every rung above asks about one player. A multiplayer game's first question is
685
+ whether there are two of them in the same room at all — and when there are not,
686
+ nothing throws, nothing logs, and each browser looks perfectly fine on its own.
687
+
688
+ ```ts
689
+ import { playMultiplayer, multiplayText } from 'incanto/test';
690
+ import { Server } from '../server/src/server'; // your real server class
691
+
692
+ const report = await playMultiplayer({
693
+ scene: gameJson,
694
+ server: Server, // optional: runs YOUR rules
695
+ scenes: { 'remote-player': remoteJson },
696
+ players: 2,
697
+ seconds: 5,
698
+ drive: ({ engine, manager, account }, frame) => {
699
+ engine.input.setActionVector('move', 1, 0); // steer each client
700
+ },
701
+ });
702
+ console.log(multiplayText(report));
703
+ ```
704
+
705
+ ```
706
+ multiplayer: 2 clients, 5s, room room1
707
+ p1: sees [p2] spawned Remotes=1
708
+ p2: sees [p1] spawned Remotes=1
709
+ ✓ every client in one room, seeing the others
710
+ ```
711
+
712
+ `report.problems` names the silent ones: clients in different rooms, a client
713
+ that never saw another account's state, a `NetworkSpawner` that materialised
714
+ nothing, frame errors per client. `report.roomState` is the shared state at the
715
+ end, so a `$roomTick` match clock is checkable too.
716
+
717
+ It runs the whole match on ONE in-memory server (`LocalGameServer`), so it also
718
+ pays the trap a hand-rolled harness has to know about: those server calls are
719
+ QUEUED, and a synchronous frame loop enqueues a thousand ticks that never run.
720
+
647
721
  ## The fifth question: is it a GAME?
648
722
 
649
723
  The four signals above answer *will it load*, *did something throw*, *did the art
@@ -59,6 +59,7 @@ Rule of thumb: score/menus/dialogs that look like WEB UI → DOM overlay;
59
59
  anything the game itself must own (and runScript must verify) → UILayer.
60
60
 
61
61
  Pin a DOM element to a world position: `game.renderer.screenFromWorld(wx, wy)`
62
+ (and `worldFromScreen(sx, sy)` the other way, for taps and clicks)
62
63
  each frame (subscribe `engine.updated`), then `transform: translate(x, y)`.
63
64
 
64
65
  Perf note: `useNodeProp` deep-compares per frame — subscribe to LEAF values
@@ -32,27 +32,104 @@
32
32
  */
33
33
 
34
34
  // These globals are injected by the agent8 isolated-vm runtime (see gameserver-sdk-v2).
35
- declare const $sender: { account: string; roomId: string };
35
+ declare const $sender: { account: string; roomId: string; isGuest?: boolean };
36
+
37
+ /**
38
+ * Room membership, PERSISTENT global state, global collections, room management
39
+ * and global messaging. Room data is ephemeral — anything that must outlive an
40
+ * empty room is written here.
41
+ */
36
42
  declare const $global: {
37
43
  joinRoom(roomId?: string): Promise<string>;
38
44
  leaveRoom(): Promise<string>;
45
+ getGlobalState(): Promise<Record<string, unknown>>;
46
+ updateGlobalState(patch: Record<string, unknown>): Promise<Record<string, unknown>>;
47
+ getMyState(): Promise<Record<string, unknown>>;
48
+ updateMyState(patch: Record<string, unknown>): Promise<Record<string, unknown>>;
49
+ getUserState(account: string): Promise<Record<string, unknown>>;
50
+ updateUserState(account: string, patch: Record<string, unknown>): Promise<Record<string, unknown>>;
51
+ addCollectionItem(collectionId: string, item: Record<string, unknown>): Promise<{ __id: string }>;
52
+ updateCollectionItem(
53
+ collectionId: string,
54
+ item: Record<string, unknown>,
55
+ ): Promise<{ __id: string }>;
56
+ deleteCollectionItem(collectionId: string, itemId: string): Promise<{ __id: string }>;
57
+ deleteCollection(collectionId: string): Promise<string>;
58
+ getCollectionItem(collectionId: string, itemId: string): Promise<Record<string, unknown>>;
59
+ getCollectionItems(
60
+ collectionId: string,
61
+ options?: CollectionQuery,
62
+ ): Promise<Record<string, unknown>[]>;
63
+ countCollectionItems(collectionId: string, options?: CollectionQuery): Promise<number>;
64
+ countRooms(): Promise<number>;
65
+ getAllRoomIds(): Promise<string[]>;
66
+ getAllRoomStates(): Promise<Record<string, unknown>[]>;
67
+ getRoomUserAccounts(roomId: string): Promise<string[]>;
68
+ countRoomUsers(roomId: string): Promise<number>;
69
+ getRoomState(roomId: string): Promise<Record<string, unknown>>;
70
+ updateRoomState(roomId: string, patch: Record<string, unknown>): Promise<Record<string, unknown>>;
71
+ getRoomUserState(roomId: string, account: string): Promise<Record<string, unknown>>;
72
+ updateRoomUserState(
73
+ roomId: string,
74
+ account: string,
75
+ patch: Record<string, unknown>,
76
+ ): Promise<Record<string, unknown>>;
77
+ broadcastToAll(type: string, message: unknown): void;
78
+ sendMessageToUser(account: string, type: string, message: unknown): void;
39
79
  };
80
+
81
+ /** Query options for a collection read (`filters` / `orderBy` / `limit`). */
82
+ interface CollectionQuery {
83
+ filters?: Record<string, unknown>;
84
+ orderBy?: { field: string; direction?: 'asc' | 'desc' };
85
+ limit?: number;
86
+ }
87
+
88
+ /** The CURRENT room: shared state, per-user state, collections, messaging. */
40
89
  declare const $room: {
90
+ getMyState(): Promise<Record<string, unknown>>;
41
91
  updateMyState(patch: Record<string, unknown>): Promise<Record<string, unknown>>;
92
+ getRoomState(): Promise<Record<string, unknown>>;
42
93
  updateRoomState(patch: Record<string, unknown>): Promise<Record<string, unknown>>;
43
94
  getUserState(account: string): Promise<Record<string, unknown>>;
44
95
  updateUserState(account: string, patch: Record<string, unknown>): Promise<Record<string, unknown>>;
96
+ getAllUserStates(): Promise<Record<string, unknown>[]>;
97
+ countUsers(): Promise<number>;
45
98
  addCollectionItem(collectionId: string, item: Record<string, unknown>): Promise<{ __id: string }>;
46
99
  updateCollectionItem(
47
100
  collectionId: string,
48
101
  item: Record<string, unknown>,
49
102
  ): Promise<{ __id: string }>;
50
103
  deleteCollectionItem(collectionId: string, itemId: string): Promise<{ __id: string }>;
104
+ deleteCollection(collectionId: string): Promise<string>;
105
+ getCollectionItem(collectionId: string, itemId: string): Promise<Record<string, unknown>>;
106
+ getCollectionItems(
107
+ collectionId: string,
108
+ options?: CollectionQuery,
109
+ ): Promise<Record<string, unknown>[]>;
110
+ countCollectionItems(collectionId: string, options?: CollectionQuery): Promise<number>;
51
111
  broadcastToRoom(type: string, message: unknown): void;
52
- countUsers(): Promise<number>;
112
+ sendMessageToUser(account: string, type: string, message: unknown): void;
53
113
  };
114
+
115
+ /**
116
+ * Serialize a read-modify-write against concurrent requests.
117
+ *
118
+ * The preview runs calls one at a time, so a FORGOTTEN lock still passes
119
+ * locally — live, parallel requests race (double-award, last-write-wins).
120
+ */
54
121
  declare function $lock<T>(key: string, fn: () => T | Promise<T>): Promise<T>;
55
122
 
123
+ /** Per-account currency ledger. `burn`/`transfer` throw on an insufficient balance. */
124
+ declare const $asset: {
125
+ mint(assetId: string, amount: number): Promise<Record<string, unknown>>;
126
+ burn(assetId: string, amount: number): Promise<Record<string, unknown>>;
127
+ has(assetId: string, amount: number): Promise<boolean>;
128
+ get(assetId: string): Promise<number>;
129
+ getAll(): Promise<Record<string, number>>;
130
+ transfer(toAccount: string, assetId: string, amount: number): Promise<Record<string, unknown>>;
131
+ };
132
+
56
133
  export class Server {
57
134
  // ---- rooms -----------------------------------------------------------------
58
135
 
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%232a4a6b%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%23ffd166%22>bi</text></svg>" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
7
7
  <title>Beacon Isle — Incanto 3D template</title>
8
8
  <style>
9
9
  html,
@@ -17,8 +17,8 @@
17
17
  }
18
18
  canvas {
19
19
  display: block;
20
- width: 100vw;
21
- height: 100vh;
20
+ width: 100%;
21
+ height: 100%;
22
22
  }
23
23
  .loading-container {
24
24
  position: fixed;
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.51.0",
17
+ "incanto": "^0.53.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%233b6ea5%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%236ee7dc%22>vg</text></svg>" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
7
7
  <title>Vanguard — Incanto TPS</title>
8
8
  <style>
9
9
  html,
@@ -17,8 +17,8 @@
17
17
  }
18
18
  canvas {
19
19
  display: block;
20
- width: 100vw;
21
- height: 100vh;
20
+ width: 100%;
21
+ height: 100%;
22
22
  }
23
23
  .loading-container {
24
24
  position: fixed;
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.51.0",
16
+ "incanto": "^0.53.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 rx=%2220%22 fill=%22%234a6b3a%22/><text x=%2250%22 y=%2268%22 font-size=%2255%22 text-anchor=%22middle%22 font-family=%22Arial%22 font-weight=%22bold%22 fill=%22%23ffd166%22>eb</text></svg>" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
7
7
  <title>Emberwood — Incanto quest</title>
8
8
  <style>
9
9
  html,
@@ -17,8 +17,8 @@
17
17
  }
18
18
  canvas {
19
19
  display: block;
20
- width: 100vw;
21
- height: 100vh;
20
+ width: 100%;
21
+ height: 100%;
22
22
  }
23
23
  .loading-container {
24
24
  position: fixed;
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.51.0",
16
+ "incanto": "^0.53.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {