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.
- package/dist/2d.d.ts +86 -3
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +9 -3
- package/dist/3d.js +4 -4
- package/dist/{behavior-rZNfzVbH.d.ts → behavior-Do0Da56m.d.ts} +108 -0
- package/dist/{create-game-CkzKZ4v5.js → create-game-9D87XaiX.js} +112 -15
- package/dist/{create-game-BNaOC3Px.js → create-game-CDJ1lVqK.js} +21 -13
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +1 -1
- package/dist/{duplicate-EybyYeyL.js → duplicate-MNLMAcbz.js} +1 -1
- package/dist/editor.js +1931 -1579
- package/dist/{environment-presets-C08pOC6H.js → environment-presets-SkGanr2s.js} +3 -3
- package/dist/{gameplay-bStgtZBV.js → gameplay-CRYw_Q-T.js} +6 -4
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +237 -5
- package/dist/index.js +9 -9
- package/dist/{loader-B2asghWa.js → loader-BTkHYrQn.js} +86 -50
- package/dist/{loader-Dkbn56KC.d.ts → loader-Cu_7kJDy.d.ts} +1 -1
- package/dist/{log-report-lxrQY9cH.js → log-report-CPFm4OXf.js} +0 -0
- package/dist/net.d.ts +15 -410
- package/dist/net.js +1 -822
- package/dist/{pathfinding-Bz34pvQD.d.ts → pathfinding-HNGFqGUZ.d.ts} +24 -1
- package/dist/{physics-2d-Ccq2L9R0.js → physics-2d-BhZZ-HAp.js} +2 -2
- package/dist/{physics-3d-B2c5KNYh.js → physics-3d-DmyO2oaN.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-CGq-Hee8.js → register-BTomIiYG.js} +305 -25
- package/dist/{register-B_BaaGx-.js → register-DROK2l7J.js} +3 -3
- package/dist/{registry-IyWCGe4q.js → registry-C7u42TID.js} +23 -1
- package/dist/{replay-DIP2_as4.d.ts → replay-9Fy6C10F.d.ts} +1 -1
- package/dist/{replay-BgKxGcXH.js → replay-DYNUL4BU.js} +195 -2
- package/dist/split-screen-CaF9hO7g.js +1267 -0
- package/dist/split-screen-DNmcX1Pz.d.ts +441 -0
- package/dist/{src-C3UwzYXl.js → src-C9xyZW7M.js} +1 -1
- package/dist/{teardown-BKTCzLek.js → teardown-Bw2aeyGI.js} +32 -2
- package/dist/{test-D2gRpk5V.js → test-CJRsciFk.js} +119 -22
- package/dist/test.d.ts +60 -5
- package/dist/test.js +3 -3
- package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-DbX_msaO.js → agent8-PlFHzJsh.js} +1 -1
- package/editor/assets/{debug-C-kMxdl1.js → debug-CebV7CDW.js} +1 -1
- package/editor/assets/index-CdbsY31G.js +10958 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +213 -0
- package/skills/incanto-assets.md +4 -1
- package/skills/incanto-audio.md +97 -10
- package/skills/incanto-building-2d-games.md +20 -3
- package/skills/incanto-editor.md +48 -11
- package/skills/incanto-gameplay-behaviors.md +14 -5
- package/skills/incanto-hud.md +7 -0
- package/skills/incanto-multiplayer.md +39 -3
- package/skills/incanto-node-reference.md +34 -0
- package/skills/incanto-physics-and-input.md +38 -0
- package/skills/incanto-verifying-your-game.md +75 -1
- package/skills/incanto-web-integration.md +1 -0
- package/templates/agent8-server.ts +79 -2
- package/templates-app/beacon-isle-3d/index.html +3 -3
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/index.html +3 -3
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/index.html +3 -3
- package/templates-app/village-quest-3d/package.json +1 -1
- package/dist/register-CLVhzWcI.js +0 -374
- 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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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:
|
|
21
|
-
height:
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
22
|
}
|
|
23
23
|
.loading-container {
|
|
24
24
|
position: fixed;
|
|
@@ -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:
|
|
21
|
-
height:
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
22
|
}
|
|
23
23
|
.loading-container {
|
|
24
24
|
position: fixed;
|
|
@@ -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:
|
|
21
|
-
height:
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
22
|
}
|
|
23
23
|
.loading-container {
|
|
24
24
|
position: fixed;
|