incanto 0.1.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 (138) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.md +88 -0
  4. package/assets/audio/attacked.mp3 +0 -0
  5. package/assets/audio/explosion.mp3 +0 -0
  6. package/assets/audio/gold_loot.mp3 +0 -0
  7. package/assets/audio/heal.mp3 +0 -0
  8. package/assets/audio/hit_metal_bang.mp3 +0 -0
  9. package/assets/audio/ice_spear.mp3 +0 -0
  10. package/assets/audio/monster_died.mp3 +0 -0
  11. package/assets/audio/slash.mp3 +0 -0
  12. package/assets/audio/smite.mp3 +0 -0
  13. package/assets/audio/spells_cast.mp3 +0 -0
  14. package/assets/audio/ui_click.wav +0 -0
  15. package/assets/audio/walk.mp3 +0 -0
  16. package/assets/catalog.json +390 -0
  17. package/assets/characters/2dbasic.json +41 -0
  18. package/assets/characters/2dbasic.png +0 -0
  19. package/assets/characters/ghost.json +46 -0
  20. package/assets/characters/ghost.png +0 -0
  21. package/assets/characters/goblin.json +40 -0
  22. package/assets/characters/goblin.png +0 -0
  23. package/assets/characters/medieval-knight.json +41 -0
  24. package/assets/characters/medieval-knight.png +0 -0
  25. package/assets/effects/swoosh.png +0 -0
  26. package/assets/items/box.png +0 -0
  27. package/assets/items/buff_potion.png +0 -0
  28. package/assets/items/coin.png +0 -0
  29. package/assets/items/gem.png +0 -0
  30. package/assets/items/gold.png +0 -0
  31. package/assets/items/hp_potion.png +0 -0
  32. package/assets/items/locked_item_box.png +0 -0
  33. package/assets/items/map.png +0 -0
  34. package/assets/items/resurrection_potion.png +0 -0
  35. package/assets/items/super_box.png +0 -0
  36. package/assets/items/trap.png +0 -0
  37. package/assets/tiles/floor00.jpg +0 -0
  38. package/assets/tiles/minecraft-tiles.png +0 -0
  39. package/assets/tiles/wall00.jpg +0 -0
  40. package/assets/vegetation/ash_color.png +0 -0
  41. package/assets/vegetation/aspen_color.png +0 -0
  42. package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
  43. package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
  44. package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
  45. package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
  46. package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
  47. package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
  48. package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
  49. package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
  50. package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
  51. package/assets/vegetation/ground/dirt_color.jpg +0 -0
  52. package/assets/vegetation/ground/dirt_normal.jpg +0 -0
  53. package/assets/vegetation/ground/grass.jpg +0 -0
  54. package/assets/vegetation/oak_color.png +0 -0
  55. package/assets/vegetation/pine_color.png +0 -0
  56. package/bin/incanto-assets.mjs +107 -0
  57. package/bin/incanto-check.mjs +107 -0
  58. package/bin/incanto-editor.mjs +343 -0
  59. package/bin/incanto-env.mjs +144 -0
  60. package/bin/incanto-model.mjs +296 -0
  61. package/bin/incanto-play.mjs +219 -0
  62. package/bin/incanto-skills.mjs +71 -0
  63. package/dist/2d.d.ts +642 -0
  64. package/dist/2d.js +44 -0
  65. package/dist/3d.d.ts +1860 -0
  66. package/dist/3d.js +5 -0
  67. package/dist/agent8-DzU2fFyH.js +129 -0
  68. package/dist/audio-player-DqUR3XFs.d.ts +110 -0
  69. package/dist/behavior-BAQq7HGM.d.ts +851 -0
  70. package/dist/create-game-BdjpTHrW.js +1725 -0
  71. package/dist/create-game-CZHROKcT.js +527 -0
  72. package/dist/debug-draw-CZmOYjL2.js +13 -0
  73. package/dist/debug.d.ts +66 -0
  74. package/dist/debug.js +658 -0
  75. package/dist/duplicate-DP2WPYom.js +22 -0
  76. package/dist/env.d.ts +430 -0
  77. package/dist/env.js +3152 -0
  78. package/dist/errors-BMFaY68Q.d.ts +33 -0
  79. package/dist/errors-BpWbnbb_.js +13 -0
  80. package/dist/gameplay-Ccruc3Wd.js +1501 -0
  81. package/dist/gameplay.d.ts +543 -0
  82. package/dist/gameplay.js +2 -0
  83. package/dist/heightmap-CroQPEER.js +185 -0
  84. package/dist/index.d.ts +305 -0
  85. package/dist/index.js +62 -0
  86. package/dist/json-BLk7H2Qa.js +30 -0
  87. package/dist/loader-CGs_G-r0.js +919 -0
  88. package/dist/loader-Mo0KghCv.d.ts +41 -0
  89. package/dist/net.d.ts +427 -0
  90. package/dist/net.js +772 -0
  91. package/dist/noise-CGUMx44x.js +82 -0
  92. package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
  93. package/dist/particle-sim-DYuSUxvK.js +1319 -0
  94. package/dist/physics-2d-KuMWPTf6.js +288 -0
  95. package/dist/physics-3d-Dl67vOLT.js +434 -0
  96. package/dist/react.d.ts +65 -0
  97. package/dist/react.js +209 -0
  98. package/dist/register-BuUV1_KB.js +561 -0
  99. package/dist/register-CNlYAS1_.js +10634 -0
  100. package/dist/register-DPEV9_9t.js +851 -0
  101. package/dist/register-Dasmnurl.js +374 -0
  102. package/dist/registry-BVJ2HbCn.js +132 -0
  103. package/dist/rng-DP-SR7eg.js +38 -0
  104. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  105. package/dist/schema-CcoWb32N.d.ts +104 -0
  106. package/dist/test.d.ts +158 -0
  107. package/dist/test.js +275 -0
  108. package/dist/touch-031PxtCR.js +208 -0
  109. package/dist/vite.d.ts +26 -0
  110. package/dist/vite.js +57 -0
  111. package/editor/assets/GameServer-C56iOUgF.js +1 -0
  112. package/editor/assets/agent8-Bp7QFI7v.js +1 -0
  113. package/editor/assets/index-DF3tMeKJ.css +1 -0
  114. package/editor/assets/index-Dl2pjA8e.js +7365 -0
  115. package/editor/assets/rapier-CEuLKeCu.js +1 -0
  116. package/editor/assets/rapier-DE6a0vmv.js +1 -0
  117. package/editor/index.html +169 -0
  118. package/package.json +97 -0
  119. package/schemas/scene.schema.json +4254 -0
  120. package/skills/README.md +9 -0
  121. package/skills/incanto-3d-character.md +229 -0
  122. package/skills/incanto-3d-models.md +151 -0
  123. package/skills/incanto-assets.md +118 -0
  124. package/skills/incanto-audio.md +309 -0
  125. package/skills/incanto-behaviors-and-scripts.md +169 -0
  126. package/skills/incanto-building-2d-games.md +242 -0
  127. package/skills/incanto-building-3d-games.md +245 -0
  128. package/skills/incanto-editor.md +163 -0
  129. package/skills/incanto-environment.md +743 -0
  130. package/skills/incanto-gameplay-behaviors.md +707 -0
  131. package/skills/incanto-multiplayer.md +264 -0
  132. package/skills/incanto-node-reference.md +797 -0
  133. package/skills/incanto-physics-and-input.md +164 -0
  134. package/skills/incanto-scene-json-authoring.md +325 -0
  135. package/skills/incanto-verifying-your-game.md +191 -0
  136. package/skills/incanto-web-integration.md +96 -0
  137. package/templates/agent8-server.js +84 -0
  138. package/templates/agent8-server.ts +138 -0
@@ -0,0 +1,264 @@
1
+ ---
2
+ name: incanto-multiplayer
3
+ description: Incanto multiplayer over a pluggable NetworkTransport — NetworkManager, owner replication via scene-JSON network blobs, NetworkSpawner, the built-in Loopback transport, the @agent8/gameserver adapter, and custom transports for any backend. Use when adding multiplayer to a game.
4
+ ---
5
+
6
+ # Multiplayer with Incanto
7
+
8
+ > Shipped inside the `incanto` npm package — this document always matches the
9
+ > installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
10
+
11
+ ## Mental model
12
+
13
+ Incanto multiplayer is **transport-agnostic**: the engine speaks one small
14
+ `NetworkTransport` interface, and ANY backend plugs in behind it —
15
+
16
+ | Transport | When |
17
+ |---|---|
18
+ | `LoopbackTransport` (built in) | offline dev, tests, local split-screen — built-in protocol only, zero infra |
19
+ | `LocalGameServer` (built in) | **PREVIEW**: run your REAL `server/src/server.ts` (custom rules + `$roomTick`) in memory — no cloud, no auth |
20
+ | `createAgent8Server()` (built in) | the agent8/Verse8 platform (`@agent8/gameserver`) — production |
21
+ | your own implementation | any server: Socket.IO, Colyseus, Supabase Realtime, custom WebSocket… |
22
+
23
+ Three rungs, same game code: **Loopback** (movement/co-presence, no server logic) →
24
+ **LocalGameServer** (your actual server-authoritative `Server` class, run locally) →
25
+ **Agent8** (that same `Server` deployed live). You climb rungs by swapping ONE line —
26
+ the transport — never the game or the server file.
27
+
28
+ The replication model is **data-authoritative, not simulation-authoritative**: clients own
29
+ their state; the backend stores/relays it (cadence ≥ tens of ms). Design for **casual
30
+ sync** — co-presence, turn-based, racing-with-tolerance. Fast-paced fairness (fighting
31
+ games, physics duels) needs a simulation-authoritative server, which is out of scope.
32
+
33
+ Authority rules:
34
+ - movement/cosmetics → **owner-authoritative** (the replication below)
35
+ - score/economy/inventory → **server-function-authoritative** (extend `server.js`;
36
+ `$sender.account` is trustworthy, args are NOT; guard with `$lock`)
37
+ - physics/AI run client-side; one-simulator needs (NPC waves) use the host-client pattern
38
+
39
+ ## Scene JSON
40
+
41
+ ```json
42
+ "multiplayer": { "room": "auto" },
43
+ "root": { ..., "children": [
44
+ { "name": "Player", "type": "CharacterBody2D",
45
+ "network": { "mode": "owner", "sync": ["position", "Skin.animation"], "throttleMs": 50 },
46
+ "children": [ ... ] },
47
+ { "name": "Remotes", "type": "NetworkSpawner",
48
+ "props": { "source": "users", "scene": "remote-player" } }
49
+ ] }
50
+ ```
51
+
52
+ - **ONE owner node per player.** Its `sync` keys are relative to ITSELF
53
+ (`position` = own prop, `Skin.animation` = child path + prop). Changed values coalesce
54
+ into one throttled `setMyState({sync: {...}})` per window. Spawned entities (bullets,
55
+ pickups) go through **collections**, never extra owner nodes.
56
+ - **`NetworkSpawner`** (register with `registerNodesNet()`): `source: "users"` spawns one
57
+ instance of the registered scene per OTHER account (self skipped); the flat `sync` patch
58
+ applies onto the spawned scene's root; `position` lerps when `interpolate: true`
59
+ (remote entities render slightly in the past — that's correct). Emits
60
+ `spawned(node, key)` / `despawned(node, key)`. `source: "collection:<id>"` mirrors a
61
+ room collection by `__id`.
62
+
63
+ ## Boot
64
+
65
+ ```ts
66
+ import { NetworkManager, registerNodesNet, LoopbackHub } from 'incanto/net';
67
+
68
+ registerNodesNet();
69
+ const scene = loadScene(json);
70
+ engine.setScene(scene);
71
+
72
+ // DEV / offline / split-screen: zero infrastructure, built-in protocol only
73
+ const hub = new LoopbackHub();
74
+ const manager = await NetworkManager.create(engine, { transport: hub.createClient('p1') });
75
+
76
+ // PREVIEW with YOUR server logic (custom rules + $roomTick), still no cloud:
77
+ // const local = createLocalGameServer({ server: Server }); // see "Preview" below
78
+ // const manager = await NetworkManager.create(engine, { transport: local.createClient('p1') });
79
+
80
+ // LIVE on agent8 (install the optional peer dep `@agent8/gameserver`):
81
+ // const manager = await NetworkManager.create(engine); // verse/account/auth auto-resolve
82
+ // …and run the server/ init flow below ("Going live on agent8 v2").
83
+
84
+ // ANY OTHER BACKEND: implement the NetworkTransport interface (see below) and
85
+ // pass it the same way: NetworkManager.create(engine, { transport: myTransport }).
86
+
87
+ manager.registerScene('remote-player', remotePlayerSceneJson);
88
+ engine.start();
89
+ ```
90
+
91
+ `NetworkManager` API:
92
+ - ROOM signals `roomState`, `allUserStates`, `userJoined/Left`, `message(type)`,
93
+ `collection(id)`; snapshots `latestUserStates`/`latestRoomState`/`latestCollection(id)`.
94
+ - GLOBAL signals `globalState`, `globalMyState`, `asset` (the local account's `$asset`
95
+ ledger — wire a HUD straight to it, no polling), `globalCollection(id)`,
96
+ `globalMessage(type)`; snapshots `latestGlobalState`/`latestGlobalMyState`/
97
+ `latestAsset`/`latestGlobalCollection(id)`. These fire only on a transport with a
98
+ global tier (LocalGameServer, the agent8 adapter); they stay quiet on raw Loopback.
99
+ - Outbound `setMyState`, `patchRoomState`, `addEntity/updateEntity/removeEntity`,
100
+ `sendEvent(type, payload)`; **`call(fn, ...args)`** to invoke a CUSTOM server function
101
+ (your `server/src/server.ts` method — roomId is prepended automatically, e.g.
102
+ `manager.call('claimCoin', id)` → server `claimCoin(roomId, id)`, returns the server's
103
+ result). Global state is server-authoritative — the client READS it via the signals
104
+ above and WRITES it only through `call(...)`. `dispose()` leaves the room.
105
+
106
+ The agent8 adapter (`createAgent8Server`) deep-imports the framework-free
107
+ `GameServer` class from `@agent8/gameserver` (skipping its React/zustand index) and
108
+ converts the v2 wire shapes to incanto's: `subscribeRoomAllUserStates` arrives as a
109
+ delta-merged ARRAY `[{...state, account, __updated}]` → an account-keyed Record;
110
+ `subscribeRoomCollection` arrives as `{items:[{__id,…}], changes}` → a `__id`-keyed
111
+ Record. It also re-issues every subscription after a reconnect.
112
+
113
+ ## Writing a custom transport (any backend)
114
+
115
+ Implement `NetworkTransport` (exported from `incanto/net`): `connect`/`disconnect`,
116
+ `remoteFunction(fn, args)` answering the room protocol
117
+ (`joinRoom`/`leaveRoom`/`setMyState` shallow-merge/`patchRoomState`/
118
+ `addEntity`/`updateEntity`/`removeEntity`/`sendEvent`), and the subscription methods
119
+ (`subscribeRoomState`, `subscribeRoomMyState`, `subscribeRoomAllUserStates`,
120
+ `subscribeRoomCollection`, `onRoomMessage`, `onRoomUserJoin/Leave`). `LoopbackTransport`'s source is the reference
121
+ implementation — anything that behaves like it works with the whole engine.
122
+
123
+ ## Preview: run your REAL server locally (no cloud, no auth)
124
+
125
+ `LoopbackHub` answers only the FIXED built-in protocol — it cannot run your
126
+ server-authoritative rules (`claimCoin`, `castSpell`) or your `$roomTick`. To
127
+ PLAY the whole game — client + the actual `server/src/server.ts` `Server` class —
128
+ in dev, use `LocalGameServer`:
129
+
130
+ ```ts
131
+ import { createLocalGameServer, NetworkManager, registerNodesNet } from 'incanto/net';
132
+ import { Server } from '../server/src/server'; // your deployable v2 Server class
133
+
134
+ const local = createLocalGameServer({ server: Server }); // runs Server in memory
135
+ const manager = await NetworkManager.create(engine, { transport: local.createClient('p1') });
136
+ // …a second client: local.createClient('p2') — split-screen on one page.
137
+
138
+ // $roomTick is server-DRIVEN: pump it from the engine ONCE (clamp dt so a
139
+ // backgrounded tab can't fast-forward the match):
140
+ engine.updated.connect((dt) => void local.tick(Math.min(dt, 0.05) * 1000));
141
+ ```
142
+
143
+ It runs the SAME class body the cloud runs: the v2 globals (`$sender`/`$global`/
144
+ `$room`/`$lock`) are injected per call, a FRESH `Server` instance runs per request
145
+ (so `this.*` never persists), and calls are serialized (no global leaks across
146
+ `await`s). `$roomTick(deltaMS, roomId)` runs only while a room has users.
147
+
148
+ It is a FUNCTIONAL emulator, NOT the platform: no isolated-vm sandbox, no rate
149
+ limits, and no DURABLE persistence (global state lives only for the preview process
150
+ — it is not saved across runs, and rooms still clear when empty). It proves your
151
+ game logic; deploy for the rest. Omitting `server` (`createLocalGameServer()`) gives
152
+ the built-in kernel only — identical to a raw `LoopbackHub`.
153
+
154
+ The preview injects `$sender`, `$room`, `$lock`, `$global`, and `$asset`, so
155
+ SERVER-side economy and persistence logic runs locally:
156
+ - `$room` — full room state/user-state/collections: `getMyState`/`updateMyState`,
157
+ `getRoomState` (always carries the `roomId` + `$users` defaults) / `updateRoomState`,
158
+ `getUserState`/`updateUserState`, `getAllUserStates` (array, each with `account`),
159
+ `countUsers`, room collections (`add`/`get`/`getCollectionItems`/`countCollectionItems`/
160
+ `delete`/`deleteCollection`), and `broadcastToRoom`.
161
+ - `$global` — `joinRoom`/`leaveRoom`, process-lifetime global state (`getGlobalState`/
162
+ `updateGlobalState`), per-account global user state (`getMyState`/`updateMyState`/
163
+ `getUserState`/`updateUserState`), global collections (`add`/`update`/`delete`/
164
+ `get`/`getCollectionItems`/`countCollectionItems` with `filters`/`orderBy`/`limit`
165
+ query options), and room management (`countRoomUsers`/`getAllRoomIds`/
166
+ `getRoomUserAccounts`/`getRoomState`/`updateRoomState`/…).
167
+ - `$asset` — a per-account currency ledger: `mint`/`burn`/`has`/`get`/`getAll`/
168
+ `transfer` (`burn`/`transfer` throw on an insufficient balance, and all amounts
169
+ must be non-negative & finite — surfacing economy bugs just as live would).
170
+
171
+ Client-side GLOBAL subscriptions ARE emulated: the preview client reacts to
172
+ `globalState`/`globalMyState`/`asset`/`globalCollection`/`globalMessage` (surfaced as
173
+ NetworkManager signals above), and `$global.broadcastToAll`/`sendMessageToUser` deliver
174
+ to `onGlobalMessage` (account-targeted for the latter).
175
+
176
+ Still NOT emulated (use these only LIVE): `$room.sendMessageToUser` targeted ROOM
177
+ delivery (the preview room message bus isn't account-scoped — a callable no-op;
178
+ `$room.broadcastToRoom` DOES deliver) and system handlers like `$onItemPurchased`
179
+ (VX shop). And the platform's hard constraints (isolated-vm, real concurrency, rate
180
+ limits, durable persistence) are still not enforced — a game green in preview must be
181
+ re-tested live.
182
+
183
+ Runnable reference: `examples/arena-preview` (Coin Dash — server-authoritative
184
+ `$lock`'d coin claim + `$roomTick` match clock, two clients split-screen). Going
185
+ live is the one-line transport swap below.
186
+
187
+ ## Going live on agent8 (gameserver-sdk v2)
188
+
189
+ The agent8 platform exposes NOTHING client-callable by default — every function
190
+ NetworkManager calls (`joinRoom`/`leaveRoom`/`setMyState`/`patchRoomState`/
191
+ `addEntity`/`updateEntity`/`removeEntity`/`sendEvent`) must be defined in YOUR
192
+ server code. incanto ships that body for both server styles:
193
+
194
+ **Structured project (v2, recommended).** Server code lives in `server/src/server.ts`.
195
+ NEVER hand-create `server/package.json`, `server/tsconfig.json`, or `server/src/server.ts`
196
+ — the init command generates them:
197
+
198
+ ```bash
199
+ npx -y @agent8/gameserver-node init # only if server/ does not exist yet
200
+ # then replace server/src/server.ts with the incanto kernel body:
201
+ # node_modules/incanto/templates/agent8-server.ts (export class Server { … })
202
+ npx -y @agent8/gameserver-node test # write + run server tests
203
+ npx -y @agent8/gameserver-node build # generates server/dist/server.js
204
+ # then DEPLOY = push to the repository — the platform auto-builds + deploys.
205
+ ```
206
+
207
+ **Legacy (root `server.js`).** Only for projects that already ship one. Body:
208
+ `node_modules/incanto/templates/agent8-server.js` (`class Server`, NO `export`).
209
+
210
+ The client side is unchanged — `await NetworkManager.create(engine)` uses the
211
+ built-in `createAgent8Server()` transport (verse/account/auth auto-resolve via the
212
+ SDK). Develop against `LoopbackHub`, then drop the `{ transport }` option to go live.
213
+
214
+ > **Cross-links — defer server-code specifics to the service skills, don't duplicate them here:**
215
+ > - **`gameserver-sdk-v2`** — the authoritative server AND client reference: the
216
+ > `$global`/`$room`/`$sender`/`$asset`/`$lock` contexts, `$roomTick`, collection/state
217
+ > APIs, the init/test/build/deploy flow, the isolated-vm limits, AND client connection +
218
+ > auth/identity (`useGameServer`, `server.connect`, `$sender.isGuest`/`isFollower`/
219
+ > `isSubscriber`, Verse8 identity). Read it before editing `server/src/server.ts` and for
220
+ > how the live client connects.
221
+ > - **`gameserver-sdk`** — legacy v1 single-file `server.js` reference.
222
+
223
+ How incanto's model maps onto v2 rooms:
224
+ - **`network: {mode:'owner', sync:[...]}`** → throttled `setMyState(roomId, {sync:{…}})`
225
+ → server `$room.updateMyState(patch)` (shallow merge) → surfaces to every other
226
+ client through v2's `subscribeRoomAllUserStates` (account-keyed after the adapter).
227
+ - **`NetworkSpawner` `source:'users'`** consumes `manager.latestUserStates` (the
228
+ account-keyed snapshot), spawning one instance per OTHER account; `onRoomUserJoin/Leave`
229
+ drive `userJoined`/`userLeft`. A user leaving simply disappears from the next
230
+ all-user-states array, so the spawner despawns them.
231
+ - **`source:'collection:<id>'`** consumes `addEntity`→`$room.addCollectionItem` etc.,
232
+ mirrored by v2's `subscribeRoomCollection` (keyed by `__id` after the adapter).
233
+
234
+ Kernel constraints (agent8 platform facts — don't fight them):
235
+ - a NEW Server instance per request: `this.*` never persists; use `$global`/`$room`,
236
+ and `$roomTick` (100–1000ms) for timed logic — NO setTimeout/setInterval/Node builtins/fetch
237
+ - `updateMyState`/`updateRoomState` are SHALLOW merges — keep state maps flat; you can't delete by omission
238
+ - room data is ephemeral — persist to `$global` before rooms empty
239
+ - never expose unthrottled per-frame remoteFunction (rapid calls are rejected); guard score/economy with `$lock`
240
+
241
+ ## State rules
242
+
243
+ - JSON-safe by construction: vectors as arrays, no `undefined`/`NaN`/typed arrays
244
+ - Reserved names (never use as keys): `$users`, `roomId`, `account`, `__id`, `__roomId`,
245
+ `__updated`, `__leaved`, anything `$`-prefixed
246
+ - `LoopbackHub` implements the SAME room protocol as the agent8 server templates,
247
+ so passing locally proves your game LOGIC + data flow are right — necessary, but
248
+ NOT sufficient. The preview does NOT enforce the platform's hard constraints, so a
249
+ game that's green locally can still misbehave live; you MUST re-test live for:
250
+ - **isolated-vm limits** — `setTimeout`/`setInterval`/`fetch`/Node builtins run
251
+ fine in preview but are REJECTED live (timed logic must be `$roomTick`);
252
+ - **concurrency** — the preview serializes every call, so a FORGOTTEN `$lock`
253
+ still passes locally; live, parallel requests race (double-award, last-write-wins);
254
+ - **rate limits** — unthrottled per-frame `remoteFunction` passes locally, throttled live;
255
+ - **persistence/ephemerality** — preview state lives forever in a Map; live, room
256
+ data is cleared when the last user leaves (persist to `$global` before then);
257
+ - **wire shapes & auth** — array/delta conversions and `$sender.account` trust
258
+ exist only on the live adapter.
259
+ - Runnable references: `examples/arena-loopback` (two clients on one `LoopbackHub`)
260
+ and `examples/arena-preview` (the real `server/src/server.ts` run locally via
261
+ `LocalGameServer` — server-authoritative scoring + `$roomTick`). Going live = the
262
+ client transport swap (for a normal single-client game, just omit `{transport}`;
263
+ a split-screen demo like arena-preview also drops its local-only scaffolding) PLUS
264
+ deploying `server/` (the "Going live on agent8" section above) — not literally one line.