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.
- package/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- 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
|
+
}
|