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,191 @@
1
+ ---
2
+ name: incanto-verifying-your-game
3
+ description: Verify an Incanto game WITHOUT a browser — headless scene validation (incanto-check), scripted gameplay runs with assertions (runScript), scene-state captures as text screenshots, and engine logs. Use after every scene/behavior edit, and whenever you need to prove a gameplay change actually works.
4
+ ---
5
+
6
+ # Verifying your game (browserless)
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
+ You usually cannot SEE the game (no browser in most agent environments). The
12
+ scene state itself is your screenshot. The loop is: **edit → check → scripted
13
+ run → read the capture → fix**. Never hand a game back without steps 2–3.
14
+
15
+ ## 1. After EVERY scene edit: `incanto-check`
16
+
17
+ ```bash
18
+ bunx incanto-check # validates every *.scene.json under cwd
19
+ bunx incanto-check --json # machine-readable (code + details.path/prop)
20
+ ```
21
+
22
+ Runs every hard load-time check (unknown types/props, bad colliders, dangling
23
+ connections, undeclared signals, duplicate uids…) and names the offending node
24
+ path. Unregistered behaviors are stubbed — no TypeScript needed.
25
+ (`--strict-behaviors` fails on behavior names the ENGINE doesn't know;
26
+ incanto-check can't load your TS, so with custom behaviors skip the flag and
27
+ get code-aware validation from `incanto-play --behaviors` / `runScript`
28
+ instead.) Wire incanto-check as a `"check"` script in package.json and run
29
+ it before declaring anything done.
30
+
31
+ ## 2. Play it from bash (incanto-play)
32
+
33
+ The interactive headless loop — inputs in, the screen out AS SCENE JSON:
34
+
35
+ ```bash
36
+ bunx incanto-play src/game.scene.json --behaviors src/behaviors.ts <<'EOF'
37
+ vector move 1 0
38
+ step 1000
39
+ press jump
40
+ step 500
41
+ capture
42
+ logs
43
+ quit
44
+ EOF
45
+ ```
46
+
47
+ Every response is one JSON line. `capture` returns the CURRENT state in the
48
+ exact *.scene.json format (positions, text, everything non-default) — read
49
+ it like a screenshot; `capture snap.json` writes it to disk for diffing.
50
+ Other commands: `release <action>`, `key KeyW down|up`, `mouse 0 down|up`,
51
+ `pointer <dx> <dy>`, `wheel <dy>`, `describe` (one line per node), `logs`
52
+ (your `this.log` output), `--commands file` to replay a script. Without
53
+ `--behaviors` scripts are stubbed (structure-only play); `--behaviors`
54
+ accepts your .ts directly. `--seed` makes runs reproducible. `--behaviors` registers every EXPORTED
55
+ `Behavior` subclass under its export name.
56
+
57
+ Script steps' `do:`/`assert:` callbacks receive the full RunContext —
58
+ `{ engine, scene, timeMs, getNode, capture }` — so closed-loop autopilots
59
+ work inside runScript: `do: (ctx) => ctx.engine.input.setActionVector('move',
60
+ chase(ctx), 0)`.
61
+
62
+ **Reaching nodes & state from a step (the gotchas a sim hit):**
63
+ - `ctx.getNode(path)` and `ctx.scene.root.getNode(path)` resolve from the ROOT.
64
+ When the root node IS named `Game`, the root is `ctx.scene.root` (or
65
+ `ctx.getNode('.')`) — `ctx.getNode('Game')` looks for a CHILD named `Game` and
66
+ throws. Reach a child as `ctx.getNode('Player')`, a grandchild as
67
+ `ctx.getNode('Player/Weapon')`.
68
+ - **Behavior state lives on `node.behavior`.** Read a Health's HP via
69
+ `ctx.getNode('Player').behavior.current`; a ScoreKeeper's score via
70
+ `ctx.scene.root.behavior.score`. The behavior's own methods (`addScore`,
71
+ `damage`) are on `node.behavior` too.
72
+ - **Signals are events to CONNECT, not fields to read.** `allCleared`, `won`,
73
+ `died` are emitted — listen with `ctx.scene.root.on('won', …)` inside a `do:`
74
+ step (or assert the resulting STATE, e.g. `behavior.score`). There is no
75
+ `behavior.allCleared` boolean.
76
+
77
+ Rapier may print a deprecated-parameters warning on stderr when physics
78
+ initializes headlessly — library-internal noise, safe to ignore.
79
+
80
+ Headless gotchas: `engine.start()` needs requestAnimationFrame (a browser) —
81
+ headless code always advances time with `engine.step()` (one fixed tick;
82
+ default 60/s, so 60 steps ≈ 1s). A `vector2` input action must declare ALL
83
+ FOUR direction arrays — use `[]` for directions you don't bind.
84
+
85
+ ## 3. Prove gameplay with a scripted run (runScript)
86
+
87
+ `incanto/test` plays the game headlessly at a fixed timestep, driving input by
88
+ ACTION intent (the same actions your scene's `input{}` declares):
89
+
90
+ ```ts
91
+ import { runScript } from 'incanto/test';
92
+ import { PlayerControl } from './src/behaviors';
93
+ import sceneJson from './src/game.scene.json';
94
+
95
+ const result = await runScript(sceneJson, {
96
+ behaviors: { PlayerControl }, // real classes — props validate too
97
+ durationMs: 3000,
98
+ seed: 42, // same seed → identical run
99
+ steps: [
100
+ { atMs: 0, vector: ['move', 1, 0] }, // hold right
101
+ { atMs: 1000, press: 'jump' },
102
+ { atMs: 1100, release: 'jump' },
103
+ { atMs: 1500, label: 'jump apex reached',
104
+ assert: (ctx) => (ctx.getNode('Player') as never as { position: number[] }).position[1]! < 300 },
105
+ ],
106
+ snapshotEveryMs: 500,
107
+ });
108
+ console.log(result.describe()); // run report + final scene, one line per node
109
+ if (!result.ok) process.exit(1); // failures carry atMs + label + message
110
+ ```
111
+
112
+ Run it with `bunx tsx verify.ts` (or inside a vitest test). Physics enables
113
+ automatically from the scene's `dimension`. `result.snapshots` holds periodic
114
+ captures; `result.logs` holds everything behaviors logged via `this.log`.
115
+ Steps that never execute (atMs beyond the run) are reported as failures —
116
+ an assert cannot "pass" by not running.
117
+
118
+ Note: `validateScene`/`runScript` register node types and your `behaviors`
119
+ into the process-global registries (hot-replacing same names). In a test
120
+ suite, isolate with `clearRegistry()`/`clearBehaviors()` between files if
121
+ you register conflicting classes.
122
+
123
+ ## 4. Read the scene like a screenshot
124
+
125
+ ```ts
126
+ import { captureScene, describeCapture } from 'incanto/test';
127
+ // `scene` is a loaded Scene (e.g. `ctx.scene` in a step, or `result.finalCapture`
128
+ // for the end state — runScript also returns `result.snapshots`).
129
+ console.log(describeCapture(captureScene(scene)));
130
+ // scene MyGame (2d)
131
+ // /Root Node2D
132
+ // /Root/Player CharacterBody2D script=PlayerControl position=[312,180] velocity=[180,0]
133
+ // /Root/UI/Score Label text="Score: 3"
134
+ ```
135
+
136
+ One grep-able line per node, showing only NON-default props — position, text,
137
+ visibility, velocity. Assert against these instead of pixels.
138
+
139
+ ## 5. Validate programmatically
140
+
141
+ ```ts
142
+ import { validateScene } from 'incanto/test';
143
+ const res = validateScene(sceneJson); // { ok } | { ok: false, error }
144
+ if (!res.ok) console.error(res.error.code, res.error.details); // details.path/prop/signal/validOptions
145
+ ```
146
+
147
+ ## 6. Catch broken edits at save time (vite)
148
+
149
+ ```ts
150
+ // vite.config.ts
151
+ import { incantoScenes } from 'incanto/vite';
152
+ export default { plugins: [incantoScenes()] };
153
+ ```
154
+
155
+ Invalid `*.scene.json` saves keep the last good scene running and surface the
156
+ IncantoError in the terminal AND as a browser overlay.
157
+
158
+ ## 7. See inside the RUNNING game (debug overlay)
159
+
160
+ ```ts
161
+ // Default is OFF. Gate it to your dev server with an env var — never ship it on.
162
+ createGame2D({ ..., debug: import.meta.env.VITE_INCANTO_DEBUG === '1' });
163
+ ```
164
+ ```bash
165
+ VITE_INCANTO_DEBUG=1 bun run dev # overlay on; plain `bun run dev` / prod build = off
166
+ ```
167
+
168
+ A ☰ debug menu (top-left) toggles three draggable/resizable panels over the
169
+ live game: **Explorer** (the scene tree — click any node), **Inspector** (the
170
+ selected node's current state; numbers/strings/booleans/vectors editable
171
+ in-place), **Logs** (an `engine.log` tail with level filters and an optional
172
+ browser-console capture toggle) — plus **Stats**, an always-on-top chip pinned
173
+ top-right (`60 fps · 12.1 ms` / `nodes 34 · tris 18.2k · calls 21`, refreshed
174
+ ~2×/s; the same numbers any code can read via `game.stats()`). Manual boots:
175
+ `(await import('incanto/debug')).attachDebugOverlay(engine)`.
176
+
177
+ **Security — `debug` defaults to `false` and there is NO URL/query toggle**, so
178
+ a DEPLOYED build can never be switched on by an end user (no more `?incanto_debug=1`).
179
+ Enable it only from your own dev environment via the env-var gate above. The
180
+ overlay is a lazily-imported chunk — it stays out of the main bundle (no size
181
+ cost), but it still ships in the build, so that runtime `debug` flag is the only
182
+ thing gating it: keep it `false` (or dev-gated) in anything you publish.
183
+
184
+ ## 8. Debugging signals
185
+
186
+ - `this.log.info(...)` in behaviors — shows up in `runScript().logs` (and the
187
+ browser console never has to be scraped).
188
+ - Determinism: draw randomness from `this.rng`, never `Math.random()`, and any
189
+ seeded run reproduces exactly.
190
+ - A run that "does nothing": remember the first engine tick only primes the
191
+ clock — `runScript` already handles this for you.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: incanto-web-integration
3
+ description: Embed an Incanto game in a React/Next/any web app — the IncantoCanvas component, useGame/useNodeProp/useSignal hooks, DOM HUD overlays vs UILayer, SSR safety, responsive sizing. Use when putting a game inside an existing website, building a React HUD, or wiring game state to web UI.
4
+ ---
5
+
6
+ # Web integration (React and beyond)
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
+ Incanto's core is framework-free and every entry imports cleanly in Node (SSR
12
+ safe). `incanto/react` is the official last mile — React is an OPTIONAL peer:
13
+ `bun add react` only if you use it.
14
+
15
+ ## React: the component
16
+
17
+ ```tsx
18
+ import { IncantoCanvas, useNodeProp, useSignal } from 'incanto/react';
19
+ import { PlayerControl } from './behaviors';
20
+ import sceneJson from './game.scene.json';
21
+
22
+ function Hud() {
23
+ const score = useNodeProp<string>('UI/Score', 'text'); // re-renders only on change
24
+ useSignal('Coin', 'triggerEnter', () => confetti()); // auto-disconnects
25
+ return <div style={{ pointerEvents: 'auto' }}>{score}</div>;
26
+ }
27
+
28
+ export function GamePage() {
29
+ return (
30
+ <div style={{ width: '100%', height: '100vh' }}>
31
+ <IncantoCanvas scene={sceneJson} behaviors={{ PlayerControl }}>
32
+ <Hud /> {/* children = DOM overlay over the canvas, game context inside */}
33
+ </IncantoCanvas>
34
+ </div>
35
+ );
36
+ }
37
+ ```
38
+
39
+ - The component boots `createGame2D/3D` in an effect (lazy-imported by the
40
+ scene's `dimension` — a 2D game never bundles the 3D stack), and `dispose()`s
41
+ everything on unmount. **StrictMode-safe**: double-invoked boots are disposed.
42
+ - All createGame options pass through: `physics`, `touch`, `debug`, `seed`,
43
+ `pixelRatio`, `pointer` (3D), `keyboard: 'window' | 'canvas' | false`,
44
+ `fallback` (loading UI), `onReady(game)`.
45
+ - Pass a STABLE `scene` reference — a new object identity re-boots the game.
46
+ - Hooks need the `<IncantoCanvas>` context: put HUD components in `children`.
47
+ The overlay wrapper is `pointer-events: none`; re-enable per element.
48
+
49
+ ## DOM HUD vs UILayer — pick one per piece of UI
50
+
51
+ | | DOM overlay (React/HTML) | `UILayer` nodes (in-canvas) |
52
+ |---|---|---|
53
+ | Styling | full CSS, fonts, a11y | engine Label/ColorRect only |
54
+ | State | `useNodeProp`/`useSignal` | behaviors mutate props |
55
+ | Serialized in scene JSON | no | yes (agents can edit it) |
56
+ | Works in headless capture | no | yes (`describeCapture` sees it) |
57
+
58
+ Rule of thumb: score/menus/dialogs that look like WEB UI → DOM overlay;
59
+ anything the game itself must own (and runScript must verify) → UILayer.
60
+
61
+ Pin a DOM element to a world position: `game.renderer.screenFromWorld(wx, wy)`
62
+ each frame (subscribe `engine.updated`), then `transform: translate(x, y)`.
63
+
64
+ Perf note: `useNodeProp` deep-compares per frame — subscribe to LEAF values
65
+ (`'UI/Score', 'text'`), not big objects like a whole `animations` map.
66
+
67
+ Perf HUDs: `game.stats()` returns `{ fps, frameMs, nodes, running, triangles,
68
+ drawCalls, geometries, textures }` at any time — poll it on a `setInterval`
69
+ (2×/s is plenty) into React state; never subscribe it per frame.
70
+
71
+ ## Next.js / SSR
72
+
73
+ Every incanto entry imports in Node — no `dynamic()` tricks needed. The game
74
+ itself boots inside `useEffect`, so server rendering emits the container +
75
+ fallback only. In the App Router mark the page `'use client'`.
76
+
77
+ ## No React? Vanilla embed
78
+
79
+ ```ts
80
+ const game = await createGame2D({ canvas, scene: sceneJson, behaviors: { … } });
81
+ // later, on teardown (SPA route change):
82
+ game.dispose(); // one call: renderer, physics, loop, scene, listeners
83
+ ```
84
+
85
+ The canvas follows its CSS size every frame — size the CONTAINER with normal
86
+ CSS/flex/grid and give the canvas `width:100%; height:100%; display:block`.
87
+ Use the scene `viewport` header (see incanto-building-2d-games) so world
88
+ coordinates stay in design pixels no matter the container.
89
+
90
+ ## Bridging game ↔ web state
91
+
92
+ - Web → game: call behavior methods (`game.engine.scene.root.getNode('Player')
93
+ .behavior`), or `engine.input.pressAction('jump')` (same pipeline as keys).
94
+ - Game → web: `useNodeProp` (polling, change-detected) or `useSignal`
95
+ (event-driven). For non-React, `engine.updated.connect(read)` +
96
+ `node.on('signal', cb)` are the same primitives.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * LEGACY agent8 netcode kernel (root `server.js`, gameserver-sdk v1).
3
+ *
4
+ * PREFER THE STRUCTURED v2 TEMPLATE: `agent8-server.ts` (sibling file) is the
5
+ * current standard — run `npx -y @agent8/gameserver-node init` and drop that body
6
+ * into `server/src/server.ts`. Use THIS file only for an existing project that
7
+ * already ships a root `server.js`. See the service's `gameserver-sdk-v2` skill
8
+ * for the migration guide.
9
+ *
10
+ * Implements EXACTLY the room protocol incanto's NetworkManager (and the
11
+ * in-memory LoopbackHub) speak, so a game proven on Loopback runs live by
12
+ * swapping the transport.
13
+ *
14
+ * Platform facts baked into this design (do not fight them):
15
+ * - A NEW Server instance runs per request in isolated-vm: `this.*` never persists.
16
+ * Use $global/$room for state — NEVER setTimeout/setInterval (no $roomTick in legacy).
17
+ * - $room.updateMyState / $room.updateRoomState are SHALLOW merges — keep state maps flat.
18
+ * - Room data is ephemeral; persist long-term data to $global before rooms empty.
19
+ * - Trust $sender.account, never trust args. Guard economy/score writes with $lock.
20
+ *
21
+ * LEGACY server.js MUST NOT use `export` — just define the class (the platform
22
+ * picks up a root `server.js` over `server/dist/server.js`).
23
+ */
24
+ class Server {
25
+ // ---- rooms -----------------------------------------------------------------
26
+
27
+ async joinRoom(roomId) {
28
+ return await $global.joinRoom(roomId); // undefined → server-assigned room
29
+ }
30
+
31
+ async leaveRoom(_roomId) {
32
+ return await $global.leaveRoom(); // acts on $sender's current room
33
+ }
34
+
35
+ // ---- per-player state (owner-authoritative: movement, cosmetics) ------------
36
+
37
+ async setMyState(_roomId, patch) {
38
+ await $room.updateMyState(patch); // shallow merge
39
+ return true;
40
+ }
41
+
42
+ // ---- shared room state (match phase, timers) --------------------------------
43
+
44
+ async patchRoomState(_roomId, patch) {
45
+ await $room.updateRoomState(patch); // shallow merge
46
+ return true;
47
+ }
48
+
49
+ // ---- room collections (spawned entities: bullets, pickups) -------------------
50
+
51
+ async addEntity(_roomId, collectionId, entity) {
52
+ const doc = await $room.addCollectionItem(collectionId, entity);
53
+ return doc.__id;
54
+ }
55
+
56
+ async updateEntity(_roomId, collectionId, id, patch) {
57
+ await $room.updateCollectionItem(collectionId, { __id: id, ...patch });
58
+ return true;
59
+ }
60
+
61
+ async removeEntity(_roomId, collectionId, id) {
62
+ await $room.deleteCollectionItem(collectionId, id);
63
+ return true;
64
+ }
65
+
66
+ // ---- transient events (explosions, chat) -------------------------------------
67
+
68
+ async sendEvent(_roomId, type, payload) {
69
+ $room.broadcastToRoom(type, payload);
70
+ return true;
71
+ }
72
+
73
+ // ---- EXTEND BELOW: server-authoritative game rules ----------------------------
74
+ // Example — guarded score (never let clients write scores directly):
75
+ //
76
+ // async awardPoint(_roomId) {
77
+ // const account = $sender.account;
78
+ // await $lock(`score:${account}`, async () => {
79
+ // const state = (await $room.getUserState(account)) ?? {};
80
+ // await $room.updateUserState(account, { score: (state.score ?? 0) + 1 });
81
+ // });
82
+ // return true;
83
+ // }
84
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Incanto netcode kernel for the agent8 GameServer SDK v2 (STRUCTURED project).
3
+ *
4
+ * HOW TO USE — DO NOT hand-create the server project. Run the init command first
5
+ * (it generates server/package.json, server/tsconfig.json, server/src/server.ts,
6
+ * server/test/), THEN drop this class body into server/src/server.ts:
7
+ *
8
+ * npx -y @agent8/gameserver-node init # only if server/ does not exist yet
9
+ * # then replace server/src/server.ts with the body below
10
+ * npx -y @agent8/gameserver-node test # write + run server tests
11
+ * npx -y @agent8/gameserver-node build # generates server/dist/server.js
12
+ * # then DEPLOY = push to the repository (platform auto-builds + deploys).
13
+ *
14
+ * Server-code specifics (the $global/$room/$sender/$asset/$lock contexts, the
15
+ * isolated-vm limits, build/deploy) AND the live client's connection + auth/identity
16
+ * (useGameServer / server.connect / $sender.isGuest etc.) are owned by the service's
17
+ * `gameserver-sdk-v2` skill — this file is ONLY the incanto-flavored room protocol body.
18
+ *
19
+ * This class implements EXACTLY the room protocol incanto's NetworkManager (and
20
+ * the in-memory LoopbackHub) speak, so a game proven on Loopback runs live by
21
+ * swapping the transport — nothing else changes.
22
+ *
23
+ * Platform facts baked into this design (do not fight them):
24
+ * - A NEW Server instance runs per request in isolated-vm: `this.*` never persists.
25
+ * Use $global/$room for state, $roomTick for periodic logic — NEVER setTimeout/setInterval.
26
+ * - $room.updateMyState / $room.updateRoomState are SHALLOW merges — keep state maps flat.
27
+ * - Room data is ephemeral (cleared when the last user leaves); persist long-term
28
+ * data to $global before rooms empty.
29
+ * - Trust $sender.account; never trust args. Guard economy/score writes with $lock.
30
+ *
31
+ * STRUCTURED v2 requires the `export` keyword (legacy root server.js must NOT export).
32
+ */
33
+
34
+ // These globals are injected by the agent8 isolated-vm runtime (see gameserver-sdk-v2).
35
+ declare const $sender: { account: string; roomId: string };
36
+ declare const $global: {
37
+ joinRoom(roomId?: string): Promise<string>;
38
+ leaveRoom(): Promise<string>;
39
+ };
40
+ declare const $room: {
41
+ updateMyState(patch: Record<string, unknown>): Promise<Record<string, unknown>>;
42
+ updateRoomState(patch: Record<string, unknown>): Promise<Record<string, unknown>>;
43
+ getUserState(account: string): Promise<Record<string, unknown>>;
44
+ updateUserState(account: string, patch: Record<string, unknown>): Promise<Record<string, unknown>>;
45
+ addCollectionItem(collectionId: string, item: Record<string, unknown>): Promise<{ __id: string }>;
46
+ updateCollectionItem(
47
+ collectionId: string,
48
+ item: Record<string, unknown>,
49
+ ): Promise<{ __id: string }>;
50
+ deleteCollectionItem(collectionId: string, itemId: string): Promise<{ __id: string }>;
51
+ broadcastToRoom(type: string, message: unknown): void;
52
+ countUsers(): Promise<number>;
53
+ };
54
+ declare function $lock<T>(key: string, fn: () => T | Promise<T>): Promise<T>;
55
+
56
+ export class Server {
57
+ // ---- rooms -----------------------------------------------------------------
58
+
59
+ /** undefined roomId → server-assigned random room; explicit id → shared room. */
60
+ async joinRoom(roomId?: string): Promise<string> {
61
+ return $global.joinRoom(roomId);
62
+ }
63
+
64
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: roomId kept for protocol symmetry with LoopbackHub.
65
+ async leaveRoom(roomId?: string): Promise<string> {
66
+ return $global.leaveRoom(); // acts on $sender's current room
67
+ }
68
+
69
+ // ---- per-player state (owner-authoritative: movement, cosmetics) ------------
70
+ // NetworkManager throttles owner sync into one setMyState({sync:{…}}) per window.
71
+
72
+ async setMyState(_roomId: string, patch: Record<string, unknown>): Promise<boolean> {
73
+ await $room.updateMyState(patch); // shallow merge; surfaces via subscribeRoomAllUserStates
74
+ return true;
75
+ }
76
+
77
+ // ---- shared room state (match phase, timers) --------------------------------
78
+
79
+ async patchRoomState(_roomId: string, patch: Record<string, unknown>): Promise<boolean> {
80
+ await $room.updateRoomState(patch); // shallow merge
81
+ return true;
82
+ }
83
+
84
+ // ---- room collections (spawned entities: bullets, pickups) -------------------
85
+
86
+ async addEntity(
87
+ _roomId: string,
88
+ collectionId: string,
89
+ entity: Record<string, unknown>,
90
+ ): Promise<string> {
91
+ const doc = await $room.addCollectionItem(collectionId, entity);
92
+ return doc.__id;
93
+ }
94
+
95
+ async updateEntity(
96
+ _roomId: string,
97
+ collectionId: string,
98
+ id: string,
99
+ patch: Record<string, unknown>,
100
+ ): Promise<boolean> {
101
+ await $room.updateCollectionItem(collectionId, { __id: id, ...patch });
102
+ return true;
103
+ }
104
+
105
+ async removeEntity(_roomId: string, collectionId: string, id: string): Promise<boolean> {
106
+ await $room.deleteCollectionItem(collectionId, id);
107
+ return true;
108
+ }
109
+
110
+ // ---- transient events (explosions, chat) -------------------------------------
111
+
112
+ async sendEvent(_roomId: string, type: string, payload: unknown): Promise<boolean> {
113
+ $room.broadcastToRoom(type, payload);
114
+ return true;
115
+ }
116
+
117
+ // ---- server-driven periodic logic (OPTIONAL) ---------------------------------
118
+ // Runs every 100–1000ms while the room has users — the ONLY way to do timed
119
+ // server logic (no setTimeout/setInterval). Drive match clocks, AI waves, etc.
120
+ // Delete if your game is purely client-driven.
121
+ //
122
+ // $roomTick(deltaMS: number, roomId: string): void {
123
+ // // e.g. advance a shared match timer in room state
124
+ // }
125
+
126
+ // ---- EXTEND BELOW: server-authoritative game rules ----------------------------
127
+ // Example — guarded score (never let clients write scores directly). $lock makes
128
+ // the read-modify-write atomic against concurrent requests:
129
+ //
130
+ // async awardPoint(_roomId: string): Promise<boolean> {
131
+ // const account = $sender.account;
132
+ // await $lock(`score:${account}`, async () => {
133
+ // const state = (await $room.getUserState(account)) ?? {};
134
+ // await $room.updateUserState(account, { score: ((state.score as number) ?? 0) + 1 });
135
+ // });
136
+ // return true;
137
+ // }
138
+ }