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,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-physics-and-input
|
|
3
|
+
description: Incanto physics bodies (2D/3D Rapier), colliders-as-props, trigger signals, and the declarative InputMap. Use when adding movement, collisions, pickups, gravity, or keyboard controls.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Physics & Input in 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
|
+
## Enabling physics (async — Rapier wasm loads on demand)
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { enablePhysics2D } from 'incanto/2d'; // or enablePhysics3D from 'incanto/3d'
|
|
15
|
+
await enablePhysics2D(engine); // AFTER engine.setScene(scene)
|
|
16
|
+
engine.start();
|
|
17
|
+
```
|
|
18
|
+
Games that never call this never download Rapier (~600KB gz). 2D and 3D worlds are
|
|
19
|
+
independent; enable the one matching your scene.
|
|
20
|
+
|
|
21
|
+
## Units & gravity
|
|
22
|
+
|
|
23
|
+
| | 2D | 3D |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Units | pixels, y-DOWN | meters, y-UP |
|
|
26
|
+
| Default gravity | `[0, 980]` | `[0, -9.81, 0]` |
|
|
27
|
+
| Scene header | `"physics": { "gravity": [0, 1400] }` | `"physics": { "gravity": [0, -9.81, 0] }` |
|
|
28
|
+
|
|
29
|
+
## Body nodes (colliders are PROPS, never child nodes)
|
|
30
|
+
|
|
31
|
+
2D collider shapes: `{"shape":"rect","size":[w,h]}` · `{"shape":"circle","radius":r}` ·
|
|
32
|
+
`{"shape":"capsule","radius":r,"height":h}`.
|
|
33
|
+
3D: `{"shape":"box","size":[x,y,z]}` · `{"shape":"sphere","radius":r}` · `{"shape":"capsule",...}` ·
|
|
34
|
+
`{"shape":"trimesh","vertices":[...],"indices":[...]}` · `{"shape":"heightfield"}` (STATIC-only;
|
|
35
|
+
reads the grid from a `Terrain3D` child — see the incanto-environment skill).
|
|
36
|
+
Malformed colliders are hard `BAD_FORMAT` errors at enable time.
|
|
37
|
+
|
|
38
|
+
- **`StaticBody2D/3D`** — immovable (ground, walls). Props: `collider`.
|
|
39
|
+
- **`RigidBody2D/3D`** — simulated. Props: `collider`, `mass 1`, `gravityScale 1`,
|
|
40
|
+
`fixedRotation false`, `friction 0.5`, `restitution 0`, `linearVelocity` (write to launch,
|
|
41
|
+
read back every step).
|
|
42
|
+
- **`Area2D/3D`** — sensor. Emits `triggerEnter(other)` / `triggerExit(other)`. Never blocks
|
|
43
|
+
movement. Solid bodies emit the same signals on real contact (one mental model).
|
|
44
|
+
Areas overlapping OTHER Areas fire too (e.g. a weapon-hitbox Area over an
|
|
45
|
+
enemy-hitbox Area) — neither side needs to be a Body.
|
|
46
|
+
- **`CharacterBody2D/3D`** — kinematic character (Rapier KCC). Props: `collider`
|
|
47
|
+
(capsule recommended), `velocity`, `snapToGround true`, `slopeLimitDeg 45`.
|
|
48
|
+
API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`.
|
|
49
|
+
|
|
50
|
+
Physics simulates WORLD positions: bodies under offset parents work (offsets compose),
|
|
51
|
+
but ancestor ROTATION/SCALE are not supported for physics bodies — keep body ancestors
|
|
52
|
+
untransformed or translation-only.
|
|
53
|
+
|
|
54
|
+
**Gravity is NOT auto-applied to CharacterBody** (Godot semantics) — integrate it yourself:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
class Player extends CharacterBody2D {
|
|
58
|
+
static override readonly typeName = 'Player';
|
|
59
|
+
override fixedUpdate(dt: number): void {
|
|
60
|
+
const dir = input.getVector('move');
|
|
61
|
+
this.velocity[0] = dir.x * 260;
|
|
62
|
+
if (this.isOnFloor()) {
|
|
63
|
+
this.velocity[1] = input.justPressed('jump') ? -640 : 20; // small bias keeps ground snap
|
|
64
|
+
} else {
|
|
65
|
+
this.velocity[1] += 1400 * dt;
|
|
66
|
+
}
|
|
67
|
+
this.moveAndSlide();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
registerNode(Player); // then use "type": "Player" in scene JSON
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Declarative input (scene JSON `input{}` → `engine.input`)
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
"input": {
|
|
77
|
+
"move": { "type": "vector2", "keys": { "up": ["KeyW","ArrowUp"], "down": ["KeyS","ArrowDown"], "left": ["KeyA","ArrowLeft"], "right": ["KeyD","ArrowRight"] }, "touch": "joystick" },
|
|
78
|
+
"jump": { "type": "button", "keys": ["Space"], "touch": "button" }
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
engine.input.attachKeyboard(window); // explicit DOM wiring (browser only)
|
|
84
|
+
engine.input.isPressed('jump'); // held
|
|
85
|
+
engine.input.justPressed('jump'); // one-frame edge (settled per tick)
|
|
86
|
+
engine.input.getVector('move'); // normalized {x, y}, y-down (up = -y)
|
|
87
|
+
```
|
|
88
|
+
Keys are `KeyboardEvent.code` strings (`KeyW`, `Space`, `ArrowLeft`). Unknown actions and
|
|
89
|
+
wrong-kind queries (getVector on a button) are hard errors listing valid names.
|
|
90
|
+
Action declarations RESET on every `setScene` (no keybind bleed between scenes).
|
|
91
|
+
|
|
92
|
+
`attachKeyboard` calls `preventDefault()` ONLY for keys bound to a declared action
|
|
93
|
+
(default `{ preventDefault: 'bound' }` — embedded games no longer scroll the host page on
|
|
94
|
+
Space/arrows; pass `'none'` to opt out). Keys typed into INPUT/TEXTAREA/SELECT/
|
|
95
|
+
contenteditable are ignored entirely, so DOM UI overlays keep working.
|
|
96
|
+
`engine.input.dispose()` detaches every attached source (keyboard and pointer).
|
|
97
|
+
|
|
98
|
+
⚠️ `justPressed` edges settle once per RENDER tick: on a dropped frame with multiple fixed
|
|
99
|
+
steps, the edge is visible in every fixed step of that tick. Gate one-shot actions on a
|
|
100
|
+
state change too (the jump example works because `isOnFloor()` flips after the first step).
|
|
101
|
+
|
|
102
|
+
## Touch controls (mobile web)
|
|
103
|
+
|
|
104
|
+
Declare `"touch": "joystick"` on a vector2 action and/or `"touch": "button"` on button
|
|
105
|
+
actions (as above) and the game grows on-screen controls: a left-side virtual stick
|
|
106
|
+
feeding `setActionVector`, right-side buttons feeding `pressAction`/`releaseAction` —
|
|
107
|
+
the game logic never distinguishes touch from keyboard. `createGame2D/3D` shows them
|
|
108
|
+
automatically on coarse-pointer devices (`touch: 'auto'` default; `true` forces, `false`
|
|
109
|
+
disables; they overlay `touchContainer` — default the canvas's parent, give it
|
|
110
|
+
`position: relative`). Manual boots call `attachTouchControls(engine, container,
|
|
111
|
+
{ force? })` from `incanto`. Wrong touch kinds (`"joystick"` on a button) fail at load.
|
|
112
|
+
|
|
113
|
+
Collision layers/masks are not in v0 — everything collides with everything; use groups +
|
|
114
|
+
`triggerEnter` filtering for game logic.
|
|
115
|
+
|
|
116
|
+
## Action-level injection (scripted tests, touch overlays)
|
|
117
|
+
|
|
118
|
+
Drive the game BY INTENT — no key codes to reverse-engineer:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
engine.input.pressAction('jump'); // one justPressed frame, held until…
|
|
122
|
+
engine.input.releaseAction('jump'); // …released (one justReleased frame)
|
|
123
|
+
engine.input.setActionVector('move', 1, 0); // analog vector2; (0, 0) clears
|
|
124
|
+
```
|
|
125
|
+
Injected state combines with key state (vectors clamped to unit length).
|
|
126
|
+
|
|
127
|
+
## Patterns
|
|
128
|
+
|
|
129
|
+
- Pickups: `Area2D` in a group + `triggerEnter` → check `other.isInGroup('player')` →
|
|
130
|
+
`queueFree()` — wire it via JSON `connections` to a behavior method (the `Pickup`
|
|
131
|
+
pattern in `incanto-behaviors-and-scripts.md`), or imperatively with `node.on(...)`.
|
|
132
|
+
- Teleport: just write `node.position` — the body follows. Launch: write `linearVelocity`.
|
|
133
|
+
- Reference: [examples/2d-phaser-sprite-character-gravity](https://github.com/rareboe/Incanto/tree/main/examples/2d-phaser-sprite-character-gravity) — gravity, jump, attack lockout, custom
|
|
134
|
+
`Player` node type. Verified in Chromium end-to-end.
|
|
135
|
+
|
|
136
|
+
## Debug drawing
|
|
137
|
+
|
|
138
|
+
`physics.debugDraw = true` (the instance `enablePhysics2D/3D` returns) renders
|
|
139
|
+
every collider as light-green wireframe lines IN THE GAME VIEW — Rapier's own
|
|
140
|
+
debug geometry, so what you see is exactly what the solver sees. Default OFF;
|
|
141
|
+
toggle it live whenever physics feels wrong.
|
|
142
|
+
|
|
143
|
+
## Placement rules
|
|
144
|
+
|
|
145
|
+
`CharacterController2D` MUST be a direct child of a `CharacterBody2D` — the
|
|
146
|
+
loader hard-fails otherwise (it steers its parent body; nothing else has a
|
|
147
|
+
`moveAndSlide`). Bodies themselves can sit anywhere in the tree.
|
|
148
|
+
|
|
149
|
+
## Validation
|
|
150
|
+
|
|
151
|
+
Collider shapes are validated at LOAD TIME: a wrong shape (`box` on a 2D body,
|
|
152
|
+
`circle` on a 3D body) or missing dimensions is a hard `loadScene` error — you
|
|
153
|
+
find out when you author the JSON, not when physics starts. A body with NO
|
|
154
|
+
collider loads fine and physics skips it with a console warning;
|
|
155
|
+
`moveAndSlide()` on it throws a "has no collider" error.
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
## Pointer input (mouse look, buttons, wheel)
|
|
159
|
+
|
|
160
|
+
`engine.input.attachPointer(canvas, { lockOnClick: true })` wires the mouse:
|
|
161
|
+
buttons become codes `Mouse0/1/2` usable in any input-map action, look
|
|
162
|
+
movement accumulates into `input.pointerDelta()` (drain once per frame —
|
|
163
|
+
movementX/Y, so pointer lock just works), and `input.wheelDelta()` drains the
|
|
164
|
+
wheel. `lockOnClick` requests pointer lock on click — the FPS pattern.
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-scene-json-authoring
|
|
3
|
+
description: Read and write Incanto *.scene.json files — the JSON source of truth for every Incanto game. Use when creating, inspecting, or modifying scenes, nodes, signal wiring, or sub-scene instances.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Authoring Incanto Scene JSON
|
|
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
|
+
An Incanto game's structure lives entirely in scene JSON. You can understand and modify a
|
|
12
|
+
game without reading any TypeScript: nodes, their properties, group/tag identity, signal
|
|
13
|
+
wiring, and composition are all in the file. TS is for *behavior only* (attached via
|
|
14
|
+
`script` — see `incanto-behaviors-and-scripts.md`).
|
|
15
|
+
|
|
16
|
+
## File shape
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"format": 1,
|
|
21
|
+
"type": "scene",
|
|
22
|
+
"name": "Level1",
|
|
23
|
+
"dimension": "2d",
|
|
24
|
+
"viewport": { "design": [960, 540], "fit": "expand" },
|
|
25
|
+
"assets": { "<key>": { "type": "...", "url": "..." } },
|
|
26
|
+
"constants": { "UI": 1000, "Background": -100 },
|
|
27
|
+
"input": { "<action>": { "...": "...", "touch": "joystick|button (optional — mobile on-screen controls)" } },
|
|
28
|
+
"multiplayer": { "room": "auto" },
|
|
29
|
+
"root": { "name": "Level1", "type": "Node", "children": [] },
|
|
30
|
+
"connections": []
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `format` MUST be `1`. `type` MUST be `"scene"`. `name` non-empty.
|
|
35
|
+
- `dimension` is optional: `"2d"` or `"3d"` only.
|
|
36
|
+
- `viewport` (optional) makes scene JSON own responsive layout: author the world
|
|
37
|
+
in fixed `design` pixels (`[width, height]`, positive numbers) and the renderer
|
|
38
|
+
maps them onto any canvas size. `fit`: `"expand"` (design rect always fully
|
|
39
|
+
visible, extra world beyond — default) · `"letterbox"` (exactly the design
|
|
40
|
+
rect, centered bars) · `"integer"` (whole-number pixel-art scaling). Bad
|
|
41
|
+
values are hard `BAD_FORMAT` errors at load. Place world geometry in design px
|
|
42
|
+
in the JSON — not `window.innerWidth` math in TS. 2D HUDs pair this with
|
|
43
|
+
`UILayer`'s `anchor` prop, which pins the layer origin to a screen
|
|
44
|
+
corner/edge/center (see `incanto-building-2d-games.md`).
|
|
45
|
+
- `assets` keys are stable human-readable handles; node props reference them as `"$key"`.
|
|
46
|
+
- `constants` are named, reusable TYPED values; a node prop references one as
|
|
47
|
+
`{ "@const": "NAME" }` and the loader replaces it with the literal AT LOAD (see
|
|
48
|
+
"Named constants" below). Distinct from `$key` assets (which stay references).
|
|
49
|
+
- `environment.rendering` carries renderer settings IN the scene:
|
|
50
|
+
`{ "antialias": false, "pixelRatio": "device" }` (pixelRatio: a number or
|
|
51
|
+
`"device"`; defaults antialias true / 2D pixelRatio 1 (Phaser parity) /
|
|
52
|
+
3D device-capped-2). Explicit `Renderer2D/3D` constructor options outrank it.
|
|
53
|
+
- 3D `environment` also carries the whole atmosphere — `sky` (physical
|
|
54
|
+
atmosphere: `type: "atmosphere"`, sun via `elevationDeg`/`azimuthDeg` or
|
|
55
|
+
`sunPosition`, `turbidity`, `rayleigh`), `fog` (`{color?, near?, far?}`
|
|
56
|
+
linear fog), `clouds` (volumetric raymarched cloud deck —
|
|
57
|
+
`{coverage?, density?, base?, top?, color?, shadeColor?, speed?, scale?}`;
|
|
58
|
+
see `incanto-building-3d-games.md`), `shadows`
|
|
59
|
+
(`true`/`false`/`{mapSize: 1024|2048, radius}`), `exposure` (ACES
|
|
60
|
+
tone-mapping dial, default 1). All renderer-interpreted, hard-validated with
|
|
61
|
+
errors listing valid keys/options; scenes without them render unchanged.
|
|
62
|
+
Full recipe: `incanto-building-3d-games.md`.
|
|
63
|
+
- `assets` / `input` / `multiplayer` / `environment` / `viewport` are preserved verbatim by load → export
|
|
64
|
+
round-trips (`environment` carries renderer settings — see `incanto-building-3d-games.md`).
|
|
65
|
+
- Fixed-length numeric array props (`position`, `rotation`, `scale`, `size`…) are validated
|
|
66
|
+
element-wise: wrong length, non-number elements, or NaN/Infinity → `PROP_TYPE_MISMATCH`.
|
|
67
|
+
|
|
68
|
+
## Nodes
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"name": "Player",
|
|
73
|
+
"type": "Node",
|
|
74
|
+
"groups": ["player"],
|
|
75
|
+
"tags": { "kind": "LOCAL_PLAYER" },
|
|
76
|
+
"props": { },
|
|
77
|
+
"script": { "name": "PlayerController", "props": { "maxSpeed": 220 } },
|
|
78
|
+
"network": { "mode": "owner", "sync": ["position"] },
|
|
79
|
+
"children": []
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Rules the engine enforces with **hard errors** (it never warns silently):
|
|
84
|
+
|
|
85
|
+
1. Every node needs `type` (a registered node type) **or** `instance` — never both, never neither.
|
|
86
|
+
2. `props` are **delta-only**: write a prop only when it differs from the type's default.
|
|
87
|
+
On export, default-equal values are omitted automatically.
|
|
88
|
+
3. Prop keys and value kinds are validated against the type's schema:
|
|
89
|
+
unknown key → `UNKNOWN_PROP` (message lists valid keys); wrong JSON kind
|
|
90
|
+
(e.g. string where the default is boolean) → `PROP_TYPE_MISMATCH`.
|
|
91
|
+
4. Sibling names must be unique. Colliding names are auto-renamed by incrementing a
|
|
92
|
+
trailing number (`Enemy` → `Enemy2` → `Enemy3`) — **write unique names yourself** so your
|
|
93
|
+
connection paths stay valid (connections resolve after renaming).
|
|
94
|
+
5. Node names must not contain `/` or `%` and must be non-empty.
|
|
95
|
+
6. `groups` are string tags for queries (`getNodesInGroup`, connection filters).
|
|
96
|
+
`tags` is free-form JSON for game-logic identity (`{"kind": "ITEM", "value": 10}`).
|
|
97
|
+
7. `script` resolves to a registered Behavior at load (`incanto-behaviors-and-scripts.md`);
|
|
98
|
+
`network` drives multiplayer replication (`incanto-multiplayer.md`). Both round-trip
|
|
99
|
+
losslessly through export.
|
|
100
|
+
|
|
101
|
+
## Why a NESTED tree (not flat nodes+parent)
|
|
102
|
+
|
|
103
|
+
The file you are editing is the primary surface agents read: nesting makes
|
|
104
|
+
orphans/cycles/dangling-parent errors UNREPRESENTABLE, keeps a node's context
|
|
105
|
+
physically adjacent, and makes subtree copy/move one-object operations. Flat
|
|
106
|
+
addressing needs are covered by uid (`getNodeByUid`). Full reasoning:
|
|
107
|
+
docs/adr/0001 in the repo.
|
|
108
|
+
|
|
109
|
+
## Parents and children
|
|
110
|
+
|
|
111
|
+
EVERY node type can hold children — `children` is universal, so any node works
|
|
112
|
+
as a grouping container. The inverse is not true: some types demand a SPECIFIC
|
|
113
|
+
parent and are therefore also invalid as the root. Today's rule:
|
|
114
|
+
|
|
115
|
+
- `CharacterController2D` must be a direct child of a `CharacterBody2D` —
|
|
116
|
+
anywhere else (including root) is a hard `loadScene` error naming both nodes.
|
|
117
|
+
|
|
118
|
+
These constraints are loader-enforced, so you discover them at authoring time;
|
|
119
|
+
the editor's add-node list greys out types its engine probe rejects at the
|
|
120
|
+
current position.
|
|
121
|
+
|
|
122
|
+
## Stable identity: `uid`
|
|
123
|
+
|
|
124
|
+
Sibling names must be unique, but the SAME name may repeat across the tree — so name
|
|
125
|
+
lookups return lists. For a stable, scene-wide-unique handle give a node a `uid`:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{ "name": "Player", "type": "CharacterBody2D", "uid": "n_sqb84lj9q8pvemmx", … }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- **NEVER hand-write a uid.** Generate every uid with the engine's `newUid()`
|
|
132
|
+
(crypto-strength `n_` + 16 base36 chars) — hand-made readable strings break
|
|
133
|
+
the collision contract and stand out as fakes:
|
|
134
|
+
- in game/tool code: `import { newUid } from 'incanto'` (e.g. when spawning
|
|
135
|
+
nodes at runtime that you'll serialize)
|
|
136
|
+
- while authoring JSON by hand:
|
|
137
|
+
`node -e "import('incanto').then(m => console.log(m.newUid()))"`
|
|
138
|
+
- the editor assigns uids automatically to everything it creates
|
|
139
|
+
- `root.getNodeByUid("n_p1gakz144ktj7eta")` → the node (or null) — survives moves and renames
|
|
140
|
+
- `root.getNodesByName("Enemy")` → EVERY node named Enemy, in document order
|
|
141
|
+
- Duplicate uids are a hard load error (`DUPLICATE_UID`)
|
|
142
|
+
- The editor treats uid as IDENTITY: every node gets one at creation (legacy scenes
|
|
143
|
+
are backfilled on open), it is shown read-only, and deleting a node whose uid is
|
|
144
|
+
still referenced anywhere opens an unlink-or-cancel confirmation
|
|
145
|
+
|
|
146
|
+
## Assets
|
|
147
|
+
|
|
148
|
+
Every entry needs **`type` and `url` — both required**. Everything else on an
|
|
149
|
+
entry is FREE METADATA: arbitrary key-values the engine ignores but preserves,
|
|
150
|
+
readable from `scene.assets` — use it to carry information for yourself or
|
|
151
|
+
other agents (license, palette, source prompt, frame counts…).
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
"assets": {
|
|
155
|
+
"characters/knight": {
|
|
156
|
+
"type": "texture", "url": "/sprites/knight.png",
|
|
157
|
+
"filter": "nearest", "license": "CC0", "palette": ["#2d3250", "#ff6b6b"]
|
|
158
|
+
},
|
|
159
|
+
"avatar": { "type": "model", "url": "/models/hero.vrm" },
|
|
160
|
+
"run": { "type": "animation", "url": "/anims/run.glb" }
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Supported `type` values:
|
|
165
|
+
|
|
166
|
+
| type | what it loads | consumed by |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `texture` | an image | `Sprite2D.texture` |
|
|
169
|
+
| `spritesheet` | a spritesheet image (needs numeric `frameWidth`/`frameHeight`) | `AnimatedSprite2D.sheet` |
|
|
170
|
+
| `model` | a **GLB / glTF / VRM** 3D model | `ModelInstance3D.model` |
|
|
171
|
+
| `animation` | a GLB's animation clips (memory only — drawn nowhere) | `ModelInstance3D.animation` |
|
|
172
|
+
|
|
173
|
+
Nodes reference entries as `"$key"` (`"$characters/knight"`). `model`/`animation`
|
|
174
|
+
details — sizing, VRM retargeting, the `incanto-model` CLI — live in
|
|
175
|
+
`incanto-3d-models.md`.
|
|
176
|
+
|
|
177
|
+
## Asset groups
|
|
178
|
+
|
|
179
|
+
Asset keys may carry a `group/` prefix — pure naming convention, zero engine
|
|
180
|
+
machinery: `"characters/knight"` is one flat key, referenced as
|
|
181
|
+
`"$characters/knight"`. The editor renders groups as collapsible folders in
|
|
182
|
+
its ASSETS explorer section. Group related assets (`characters/`, `ui/`,
|
|
183
|
+
`fx/`) so scenes stay navigable as they grow.
|
|
184
|
+
|
|
185
|
+
## Named constants
|
|
186
|
+
|
|
187
|
+
A scene-level table of reusable TYPED values. Define once, reference from any
|
|
188
|
+
node prop — change the value in one place and every reference updates.
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"constants": { "UI": 1000, "Background": -100, "Brand": "#ff3366", "Spawn": [0, 0, 0] },
|
|
193
|
+
"root": { "name": "World", "type": "Node3D", "children": [
|
|
194
|
+
{ "name": "Hud", "type": "Sprite2D", "props": { "renderOrder": { "@const": "UI" } } },
|
|
195
|
+
{ "name": "Pond", "type": "Water3D", "props": { "renderOrder": { "@const": "Background" } } },
|
|
196
|
+
{ "name": "Logo", "type": "MeshInstance3D", "props": { "material": { "color": { "@const": "Brand" } } } }
|
|
197
|
+
] }
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- A prop value of `{ "@const": "NAME" }` is replaced with the constant's literal
|
|
202
|
+
value AT LOAD — the node only ever sees the resolved value, so the constant's
|
|
203
|
+
type MUST match the prop's type (a number constant for a number prop, etc.) or
|
|
204
|
+
you get a `PROP_TYPE_MISMATCH`. Works at any depth (inside `material`, arrays…).
|
|
205
|
+
- Unknown name → `UNKNOWN_CONSTANT` (lists the declared names). Constants are
|
|
206
|
+
per-scene and FLAT — a constant's value is a final literal, never another `@const`.
|
|
207
|
+
- The editor manages these in a CONSTANTS panel (like ASSETS); the Inspector lets
|
|
208
|
+
you pick a matching-typed constant or type a raw value for any prop.
|
|
209
|
+
- THE use case: render-order / sorting tiers. Define `Background`/`World`/`UI`
|
|
210
|
+
once, then point every node's `renderOrder` at them.
|
|
211
|
+
|
|
212
|
+
## Render order (draw priority)
|
|
213
|
+
|
|
214
|
+
ONE prop name across 2D and 3D: **`renderOrder`** (default 0, higher = drawn
|
|
215
|
+
later / on top), on every `Node2D` and `Node3D`. (Legacy 2D scenes that used
|
|
216
|
+
`zIndex` still load — it's accepted as an alias — but author new scenes with
|
|
217
|
+
`renderOrder`.)
|
|
218
|
+
|
|
219
|
+
- **2D**: every drawable (Sprite2D/ColorRect2D/Label/Particles2D) honors it —
|
|
220
|
+
see `incanto-building-2d-games.md`.
|
|
221
|
+
- **3D**: only affects TRANSPARENT materials (three sorts the transparent pass
|
|
222
|
+
by `renderOrder`, then depth; opaque meshes always sort by depth). Built-ins set
|
|
223
|
+
sensible defaults — `Water3D` 1, `Foliage3D` blades 2 — which you can override.
|
|
224
|
+
- Name the tiers with constants (above) instead of scattering magic numbers.
|
|
225
|
+
|
|
226
|
+
## Node paths
|
|
227
|
+
|
|
228
|
+
Used by `connections[].from/to` and APIs like `getNode`:
|
|
229
|
+
|
|
230
|
+
| Path | Meaning |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `"Child/Grand"` | descend from the current node |
|
|
233
|
+
| `"."` | the current node (in connections: the scene root) |
|
|
234
|
+
| `".."`, `"../Sibling"` | parent hops (relative paths only) |
|
|
235
|
+
| `"/Level1/Player"` | absolute — first segment must equal the root node's name |
|
|
236
|
+
| `"/root/Player"` | `/root/` is an alias for the tree root regardless of its actual name |
|
|
237
|
+
| `"%Player"` | unique-name lookup across the whole tree (error if 0 or ≥2 matches) |
|
|
238
|
+
|
|
239
|
+
Bad grammar → `BAD_NODE_PATH`. Unresolvable → `NODE_NOT_FOUND` (message lists the children
|
|
240
|
+
that do exist at the failing spot).
|
|
241
|
+
|
|
242
|
+
## Connections (serialized signal wiring)
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{ "signal": "triggerEnter", "from": "Coin", "to": ".", "handler": "onCoinCollected",
|
|
246
|
+
"once": true, "filter": { "group": "player" } }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- `from`/`to` are node paths **relative to the scene root** (`"."` = the root).
|
|
250
|
+
- `signal` must be DECLARED on the from node (the class's or its behavior's
|
|
251
|
+
`static signals`) — validated at load → `UNKNOWN_SIGNAL` otherwise.
|
|
252
|
+
- `handler` must be a method on the target node OR its behavior (script) — both are
|
|
253
|
+
validated hard at load. Otherwise → `UNKNOWN_HANDLER`. Script names themselves must be
|
|
254
|
+
registered (`registerBehavior`) before `loadScene` → `UNKNOWN_BEHAVIOR` otherwise.
|
|
255
|
+
- Unresolvable `from`/`to` → `DANGLING_CONNECTION` at load. Renaming a node breaks its
|
|
256
|
+
connections **loudly** — update paths in the same edit.
|
|
257
|
+
- `filter` gates firing on the first emitted argument: it must be a node in `filter.group`
|
|
258
|
+
and/or match every `filter.tag` entry. With `once: true`, the connection is consumed only
|
|
259
|
+
when the filter matches.
|
|
260
|
+
|
|
261
|
+
## Sub-scene composition
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{ "name": "Ruby", "instance": "scenes/item.scene.json", "overrides": { "value": 99 } }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- `instance` embeds another scene's tree; `overrides` deep-merge onto the sub-scene root's
|
|
268
|
+
props. At the instancing site, `groups` and `children` COMPOSE (union/append), while
|
|
269
|
+
`tags`/`script`/`network` REPLACE the sub-scene root's values when declared (omitted = kept).
|
|
270
|
+
- The sub-scene's own `connections` are wired inside its subtree automatically.
|
|
271
|
+
- There is **no** scene inheritance — composition only.
|
|
272
|
+
- Current limitation: exporting expands instances into full trees (the `instance` reference is
|
|
273
|
+
not preserved on export yet).
|
|
274
|
+
- Cycles (`a` instances `b` instances `a`) and missing resolvers → `UNRESOLVED_INSTANCE`.
|
|
275
|
+
|
|
276
|
+
## Error codes you will see (all hard failures at load)
|
|
277
|
+
|
|
278
|
+
| Code | Cause | Fix |
|
|
279
|
+
|---|---|---|
|
|
280
|
+
| `BAD_FORMAT` | wrong `format`/`type`/`name`/`dimension`, node without `type`/`instance`, or both | match the file shape above |
|
|
281
|
+
| `UNKNOWN_NODE_TYPE` | `type` not registered | use a type from the message's registered list |
|
|
282
|
+
| `UNKNOWN_PROP` | prop key not in the type schema | use a key from the message's list |
|
|
283
|
+
| `PROP_TYPE_MISMATCH` | prop JSON kind differs from the default's kind (incl. a `@const` whose value is the wrong type) | match the default's kind |
|
|
284
|
+
| `UNKNOWN_CONSTANT` | `{"@const":"X"}` names a constant not in the scene's `constants` | declare it, or use a listed name |
|
|
285
|
+
| `BAD_NODE_PATH` | malformed path grammar | see path table |
|
|
286
|
+
| `NODE_NOT_FOUND` | path resolves nowhere | message lists existing children |
|
|
287
|
+
| `DUPLICATE_UNIQUE_NAME` | `%Name` matches ≥2 nodes | rename one, or use an explicit path |
|
|
288
|
+
| `DANGLING_CONNECTION` | connection `from`/`to` unresolvable | fix the path after renames |
|
|
289
|
+
| `UNKNOWN_HANDLER` | handler missing on the node AND its behavior | fix the method name |
|
|
290
|
+
| `UNKNOWN_SIGNAL` | connection (or emit/on) names a signal the from node never declares | use a declared signal, or declare it (`static signals` / `declareSignal`) |
|
|
291
|
+
| `UNKNOWN_BEHAVIOR` | `script.name` not registered | `registerBehavior(name, Class)` before `loadScene` |
|
|
292
|
+
| `UNRESOLVED_INSTANCE` | no resolver, unknown path, or instance cycle | provide/fix `resolveScene`, break the cycle |
|
|
293
|
+
| `DUPLICATE_NODE_TYPE` | two classes registered under one type name | rename one type, or `{ replace: true }` for hot reload |
|
|
294
|
+
| `DUPLICATE_BEHAVIOR` | two classes registered under one behavior name | rename one, or `{ replace: true }` for hot reload |
|
|
295
|
+
| `TREE_VIOLATION` | invalid name, re-parenting without detach, cycles, double root | follow the rule in the message |
|
|
296
|
+
|
|
297
|
+
Scene-load and registry errors carry structured `details` (`path`, `uid`, `nodeType`,
|
|
298
|
+
`prop`, `signal`, `validOptions`) mirroring the prose — prefer those over regexing the
|
|
299
|
+
message (a few runtime errors still carry prose only). Scene-load
|
|
300
|
+
errors append the offending node's path: `… (at '/Level/Enemies/Slime3')`.
|
|
301
|
+
|
|
302
|
+
## Programmatic API (current milestone)
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { loadScene, registerCoreNodes, duplicateNode } from 'incanto';
|
|
306
|
+
|
|
307
|
+
registerCoreNodes(); // explicit — never an import side effect
|
|
308
|
+
const scene = loadScene(json, { resolveScene }); // throws IncantoError on any problem
|
|
309
|
+
scene.root.getNode('Player').emit('hit', 10);
|
|
310
|
+
const copy = duplicateNode(scene.root.getNode('Coin'));
|
|
311
|
+
const exported = scene.toJSON(); // lossless (delta-only props)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Registration semantics: `registerNodes2D()` / `registerNodes3D()` (from
|
|
315
|
+
`incanto/2d` / `incanto/3d`) and `registerNodesNet()` (from `incanto/net`)
|
|
316
|
+
each INCLUDE `registerCoreNodes()` — you never call it separately alongside
|
|
317
|
+
them, and calling several registrars together (e.g. 2D + Net for a
|
|
318
|
+
multiplayer game) is safe: re-registering the SAME class under a name is
|
|
319
|
+
idempotent (only a DIFFERENT class under an existing name is a
|
|
320
|
+
`DUPLICATE_NODE_TYPE` error, or needs `{ replace: true }` for hot reload).
|
|
321
|
+
`createGame2D`/`createGame3D` call the right registrar for you.
|
|
322
|
+
|
|
323
|
+
Lifecycle once loaded: `onEnterTree` parent-first → `onReady` children-first (once per
|
|
324
|
+
instance) → per-frame `update(dt)` / fixed-rate `fixedUpdate(dt)` parent-first →
|
|
325
|
+
`queueFree()` defers destruction to end of the update pass.
|