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,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.