incanto 0.31.0 → 0.33.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 (54) hide show
  1. package/bin/incanto-play.mjs +7 -2
  2. package/dist/2d.d.ts +59 -2
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +28 -3
  5. package/dist/3d.js +4 -4
  6. package/dist/{behavior-D_jMpFh8.d.ts → behavior-CKwTCjfR.d.ts} +140 -1
  7. package/dist/{create-game-z5XaB1p5.js → create-game-D2QU5x7S.js} +16 -8
  8. package/dist/{create-game-DqOjMBUS.js → create-game-sFuTLqjD.js} +22 -10
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-KRPtUtzl.js → duplicate-BgnG1Lqz.js} +1 -1
  11. package/dist/editor.js +35 -0
  12. package/dist/{environment-presets--DigHNg4.js → environment-presets-CQtEGogB.js} +2 -2
  13. package/dist/{gameplay-BftxM_It.js → gameplay-BpQCbABv.js} +90 -2
  14. package/dist/gameplay.d.ts +1 -1
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.d.ts +135 -23
  17. package/dist/index.js +6 -59
  18. package/dist/{loader-BlaRQGaA.js → loader-Buk8Bu1h.js} +53 -0
  19. package/dist/{loader-BYBrqTxP.d.ts → loader-COn5fS0o.d.ts} +1 -1
  20. package/dist/net.d.ts +1 -1
  21. package/dist/net.js +3 -3
  22. package/dist/{pathfinding-BwhqPD3i.d.ts → pathfinding-DUw9mir9.d.ts} +1 -1
  23. package/dist/{physics-2d-D9wquBvK.js → physics-2d-DjXR5DMu.js} +12 -2
  24. package/dist/{physics-3d-CnPygVGo.js → physics-3d-DF8npb1O.js} +55 -9
  25. package/dist/react.d.ts +1 -1
  26. package/dist/react.js +1 -1
  27. package/dist/{register-Dzkd6-os.js → register-CscIzJEO.js} +551 -13
  28. package/dist/{register-CUY284Is.js → register-CtI-itec.js} +2 -2
  29. package/dist/{register-tkR_8tWg.js → register-FIJtNbub.js} +145 -8
  30. package/dist/test-BRxLd2jH.js +642 -0
  31. package/dist/test.d.ts +55 -3
  32. package/dist/test.js +2 -392
  33. package/dist/{touch-031PxtCR.js → touch-BoNg_MnF.js} +2 -1
  34. package/dist/vite.js +1 -1
  35. package/editor/assets/{agent8-o27_Y1xN.js → agent8-BdDP3xKW.js} +1 -1
  36. package/editor/assets/{debug-DfcWX3uW.js → debug-DbjTyTlC.js} +1 -1
  37. package/editor/assets/{index-C9fb5QcT.js → index-BWCudoz1.js} +90 -90
  38. package/editor/index.html +1 -1
  39. package/package.json +1 -1
  40. package/schemas/scene.schema.json +520 -0
  41. package/skills/README.md +14 -2
  42. package/skills/incanto-audio.md +43 -0
  43. package/skills/incanto-building-2d-games.md +52 -0
  44. package/skills/incanto-building-3d-games.md +16 -0
  45. package/skills/incanto-hud.md +41 -0
  46. package/skills/incanto-node-reference.md +89 -0
  47. package/skills/incanto-physics-and-input.md +50 -0
  48. package/skills/incanto-verifying-your-game.md +86 -30
  49. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +40 -19
  50. package/templates-app/beacon-isle-3d/package.json +1 -1
  51. package/templates-app/tps-3d/docs/project-3d-rules.md +41 -19
  52. package/templates-app/tps-3d/package.json +1 -1
  53. package/templates-app/village-quest-3d/docs/project-3d-rules.md +40 -19
  54. package/templates-app/village-quest-3d/package.json +1 -1
@@ -562,3 +562,19 @@ if (cells) follow.setPath(cells.map(([cx, cy]) => nav.toWorld(cx, cy)));
562
562
  dense-forest frame). Use when shadow CASTERS don't move — pair moving
563
563
  characters with sprite/blob shadows. After a one-off world edit call
564
564
  `renderer.refreshShadows()`.
565
+
566
+ ## Before you hand it back
567
+
568
+ You cannot see this game. `incanto-verifying-your-game.md` is how you find out
569
+ whether it works — run it after every edit, not once at the end:
570
+
571
+ ```bash
572
+ bunx incanto-check # will it load? is anything lit?
573
+ printf 'step 500\nframing\nquit\n' | bunx incanto-play src/game.scene.json
574
+ ```
575
+
576
+ `framing` tells you what the camera can actually SEE (on screen / off screen /
577
+ behind it), what lights the scene, and which colliders intersect. Then read
578
+ `stats().errors` (something threw and got skipped) and `assetErrors()` (a model
579
+ 404'd). All four are silent failures otherwise — the screen just looks wrong,
580
+ or empty, and nothing throws.
@@ -80,3 +80,44 @@ Typewriter reveal at `charsPerSecond` (0 = instant); clicking the box
80
80
  reveals the line then advances; choice lines render buttons and wait for
81
81
  `choose(i)`. Buttons opt into pointer events — the rest of the HUD stays
82
82
  click-through.
83
+
84
+ ## Menus, options and inventories (UiPanel + the value widgets)
85
+
86
+ The HUD widgets can SAY things. These arrange them and take a value back — which
87
+ is what a title screen, pause menu, options panel, shop and inventory grid are.
88
+ Before them every one of those was hand-rolled DOM, which drops the whole screen
89
+ out of scene JSON, out of the editor, out of `incanto-check` and out of every
90
+ headless check you have.
91
+
92
+ **`UiPanel` is the box.** Widgets normally mount into the HudLayer's anchor slot
93
+ no matter how the tree is shaped; anything under a panel mounts into the PANEL,
94
+ so structure in the tree becomes structure on screen. Panels nest.
95
+
96
+ ```json
97
+ { "name": "Pause", "type": "UiPanel",
98
+ "props": { "anchor": "center", "layout": "column", "gap": 12, "padding": 20 },
99
+ "children": [
100
+ { "name": "Title", "type": "UiText", "props": { "text": "PAUSED", "size": 28 } },
101
+ { "name": "Volume", "type": "UiSlider", "props": { "label": "volume", "value": 0.8 } },
102
+ { "name": "Invert", "type": "UiToggle", "props": { "label": "invert Y" } },
103
+ { "name": "Quality", "type": "UiSelect", "props": { "options": "low,medium,high", "value": "high" } },
104
+ { "name": "Resume", "type": "UiButton", "props": { "label": "Resume" } }
105
+ ] }
106
+ ```
107
+
108
+ | node | props | signal |
109
+ | --- | --- | --- |
110
+ | `UiPanel` | `layout` (column/row/**grid**), `columns`, `gap`, `padding`, `background`, `radius`, `width`, `height`, `border` | — |
111
+ | `UiImage` | `src` (url or `$assetKey`), `width`, `height`, `fit`, `tint`, `opacity` | — |
112
+ | `UiSlider` | `label`, `value`, `min`, `max`, `step`, `width`, `color` | `changed(value)` |
113
+ | `UiToggle` | `label`, `value` | `changed(bool)` |
114
+ | `UiSelect` | `label`, `options` (`"low,medium,high"`), `value` | `changed(value)` |
115
+
116
+ **An inventory is a grid panel**: `"layout": "grid", "columns": 5`, one child per
117
+ slot, each a small `UiPanel` holding a `UiImage` (`tint` greys out what you
118
+ cannot afford) — and `Clickable` is on the world node, not the widget; for a
119
+ widget use `UiButton`'s `pressed`.
120
+
121
+ Setting `.value` from a behavior updates the control and does **NOT** re-emit
122
+ `changed` — restoring a saved setting must not fire the handler that saved it.
123
+
@@ -246,6 +246,17 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
246
246
  | `jumpHeight` | `64` | number |
247
247
  | `moveAction` | `"move"` | string |
248
248
  | `jumpAction` | `"jump"` | string |
249
+ | `coyoteSeconds` | `0` | number |
250
+ | `jumpBufferSeconds` | `0` | number |
251
+ | `jumpCutMultiplier` | `1` | number |
252
+ | `maxJumps` | `1` | number |
253
+ | `dashSpeed` | `0` | number |
254
+ | `dashSeconds` | `0.15` | number |
255
+ | `dashAction` | `"dash"` | string |
256
+ | `wallSlideSpeed` | `0` | number |
257
+ | `wallJumpImpulse` | `[0,0]` | array |
258
+
259
+ Signals: `movementStateChanged(state)`
249
260
 
250
261
  ## `CharacterController3D` — `incanto/3d`
251
262
 
@@ -958,6 +969,63 @@ Signals: `pressed`
958
969
 
959
970
  Signals: `lineShown` · `choiceMade` · `dialogueFinished`
960
971
 
972
+ ## `UiImage` — `incanto`
973
+
974
+ | Prop | Default | Kind |
975
+ |---|---|---|
976
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
977
+ | `visible` | `true` | boolean |
978
+ | `src` | `""` | string |
979
+ | `width` | `48` | number |
980
+ | `height` | `48` | number |
981
+ | `fit` | `"contain"` | one of: `contain` `cover` `fill` |
982
+ | `tint` | `""` | string |
983
+ | `opacity` | `1` | number |
984
+
985
+ ## `UiPanel` — `incanto`
986
+
987
+ | Prop | Default | Kind |
988
+ |---|---|---|
989
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
990
+ | `visible` | `true` | boolean |
991
+ | `layout` | `"column"` | one of: `column` `row` `grid` |
992
+ | `columns` | `4` | number |
993
+ | `gap` | `8` | number |
994
+ | `padding` | `12` | number |
995
+ | `background` | `"rgba(0,0,0,0.55)"` | string |
996
+ | `radius` | `10` | number |
997
+ | `width` | `0` | number |
998
+ | `height` | `0` | number |
999
+ | `border` | `""` | string |
1000
+
1001
+ ## `UiSelect` — `incanto`
1002
+
1003
+ | Prop | Default | Kind |
1004
+ |---|---|---|
1005
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1006
+ | `visible` | `true` | boolean |
1007
+ | `label` | `""` | string |
1008
+ | `options` | `""` | string |
1009
+ | `value` | `""` | string |
1010
+
1011
+ Signals: `changed`
1012
+
1013
+ ## `UiSlider` — `incanto`
1014
+
1015
+ | Prop | Default | Kind |
1016
+ |---|---|---|
1017
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1018
+ | `visible` | `true` | boolean |
1019
+ | `label` | `""` | string |
1020
+ | `value` | `0.5` | number |
1021
+ | `min` | `0` | number |
1022
+ | `max` | `1` | number |
1023
+ | `step` | `0.01` | number |
1024
+ | `width` | `180` | number |
1025
+ | `color` | `"#6ee7dc"` | string |
1026
+
1027
+ Signals: `changed`
1028
+
961
1029
  ## `UiText` — `incanto`
962
1030
 
963
1031
  | Prop | Default | Kind |
@@ -969,6 +1037,17 @@ Signals: `lineShown` · `choiceMade` · `dialogueFinished`
969
1037
  | `color` | `"#ffffff"` | string |
970
1038
  | `shadow` | `true` | boolean |
971
1039
 
1040
+ ## `UiToggle` — `incanto`
1041
+
1042
+ | Prop | Default | Kind |
1043
+ |---|---|---|
1044
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1045
+ | `visible` | `true` | boolean |
1046
+ | `label` | `""` | string |
1047
+ | `value` | `false` | boolean |
1048
+
1049
+ Signals: `changed`
1050
+
972
1051
  ## `VoxelGrid3D` — `incanto/3d`
973
1052
 
974
1053
  | Prop | Default | Kind |
@@ -1084,6 +1163,16 @@ Signals: `submerged` · `surfaced`
1084
1163
 
1085
1164
  Signals: `reachedTarget` · `lostTarget`
1086
1165
 
1166
+ ### `Clickable`
1167
+
1168
+ | Prop | Default | Kind |
1169
+ |---|---|---|
1170
+ | `button` | `0` | number |
1171
+ | `maxDistance` | `0` | number |
1172
+ | `enabled` | `true` | boolean |
1173
+
1174
+ Signals: `clicked` · `hovered` · `unhovered`
1175
+
1087
1176
  ### `Collector`
1088
1177
 
1089
1178
  | Prop | Default | Kind |
@@ -258,6 +258,56 @@ the tree says, without your scene falling over while you look at it. Use it for
258
258
  any "show me the collision geometry" tool. Do NOT use it to freeze a running
259
259
  game — that is `engine.timeScale = 0`, which keeps the world coherent.
260
260
 
261
+ ## Moving platforms carry their riders (2D **and** 3D)
262
+
263
+ A `CharacterBody2D`/`CharacterBody3D` standing on a body that moves is dragged
264
+ along by however far that body moved — elevators, patrol platforms, conveyors,
265
+ rotating discs. It is what makes a floating-island game work.
266
+
267
+ Author it the obvious way: move the platform's `position` (a behavior, a
268
+ `PathFollow`, an `Oscillate`). Nothing else to declare.
269
+
270
+ The carry stops the moment the character is no longer grounded on it, so walking
271
+ off an edge or jumping is not "sticky", and it is vertical as well as horizontal
272
+ — an elevator lifts you.
273
+
274
+ ## The mouse as gameplay input (click, hover)
275
+
276
+ `pointerDelta()` answers "how far did the mouse move" — the mouse-look question.
277
+ For match-3, tower defense, card games, point-and-click and RTS that is the
278
+ wrong question, and it used to be the only one the engine could answer.
279
+
280
+ ```ts
281
+ engine.input.pointerPosition(); // { x, y } in CANVAS pixels, or null
282
+ engine.input.mousePressed(0); // 0 left · 1 middle · 2 right
283
+ engine.input.mouseJustPressed(0); // and mouseJustReleased(0)
284
+ game.pick(x, y); // the node under that pixel, or null
285
+ ```
286
+
287
+ A stretched canvas is handled: `pointerPosition()` scales client → canvas pixels,
288
+ so the coordinates are the ones `pick()` wants.
289
+
290
+ From scene JSON, the **`Clickable`** behavior needs no code at all:
291
+
292
+ ```json
293
+ { "name": "Tile", "type": "MeshInstance3D", "script": { "name": "Clickable" } }
294
+ ```
295
+
296
+ | prop | default | |
297
+ | --- | --- | --- |
298
+ | `button` | `0` | 0 left · 1 middle · 2 right |
299
+ | `maxDistance` | `0` | ignore clicks further than this (0 = any) |
300
+ | `enabled` | `true` | stop responding without detaching |
301
+
302
+ Signals: **`clicked`**, **`hovered`**, **`unhovered`** — wire them in
303
+ `connections` like any other. `clicked` fires on RELEASE over the same node the
304
+ press started on (a drag that ends elsewhere is not a click, the way every
305
+ button on every platform behaves), and a hit on a CHILD counts as a hit on the
306
+ node — the raycast lands on the visual mesh, which is usually a child.
307
+
308
+ Headless there is no renderer and therefore no raycast: `Clickable` is inert
309
+ rather than wrong, and `engine.picker` is null.
310
+
261
311
  ## Placement rules
262
312
 
263
313
  `CharacterController2D` MUST be a direct child of a `CharacterBody2D` — the
@@ -12,6 +12,20 @@ You usually cannot SEE the game (no browser in most agent environments). The
12
12
  scene state itself is your screenshot. The loop is: **edit → check → scripted
13
13
  run → read the capture → fix**. Never hand a game back without steps 2–3.
14
14
 
15
+ **The four things to read before you call it done.** Three of them are new, and
16
+ each answers a failure that used to look like nothing at all:
17
+
18
+ | read this | when it is not clean | see |
19
+ | --- | --- | --- |
20
+ | `incanto-check` | the scene will not load, or renders black | §1, §5b |
21
+ | `stats().errors` | something threw and was skipped to keep the game alive | §6b |
22
+ | `assetErrors()` | a model/texture 404'd — the thing is simply not there | §6c |
23
+ | `framing` | the camera is pointed the wrong way, or nothing is lit | §6a |
24
+
25
+ A run that finishes is not a run that worked. `runScript()` fails the run when
26
+ `stats().errors` is non-zero, and prints the engine's warnings under the
27
+ failures — but `framing` and `assetErrors()` you have to ASK for.
28
+
15
29
  ## 1. After EVERY scene edit: `incanto-check`
16
30
 
17
31
  ```bash
@@ -144,6 +158,25 @@ const res = validateScene(sceneJson); // { ok } | { ok: false, error }
144
158
  if (!res.ok) console.error(res.error.code, res.error.details); // details.path/prop/signal/validOptions
145
159
  ```
146
160
 
161
+ ## 5b. What a green `incanto-check` now covers
162
+
163
+ `validateScene` / `incanto-check` hard-fail on all of these, so they never reach
164
+ a browser:
165
+
166
+ - **Every prop's type AND its enum values** — `"mesh": "crystal"` names what is
167
+ valid instead of printing `ok` and throwing at boot. All 31 enum props.
168
+ - **`input{}`** — a declaration's shape (`"keys": "Space"` instead of
169
+ `["Space"]`, an unknown `type`, a wrong `touch`). This used to be checked only
170
+ by `Engine.setScene`, i.e. never headlessly.
171
+ - **`assets{}`** — every entry an object with a non-empty `url`.
172
+ - **`environment.preset`** — resolved only inside Renderer3D before, so a typo
173
+ survived every check and died at boot.
174
+
175
+ And `auditScene` (the warnings `incanto-check` prints) gained the loudest
176
+ "why is my screen black" of all: **nothing lights this 3D scene** — no light
177
+ node, no `environment` sky/hdri/preset, no ambient intensity. Ambient defaults to
178
+ 0, so such a scene renders nothing, throws nothing and logs nothing.
179
+
147
180
  ## 6. Catch broken edits at save time (vite)
148
181
 
149
182
  ```ts
@@ -161,44 +194,47 @@ ANY scene in the project — not just the one the game booted with. `incantoScen
161
194
  root })` bounds that to a directory other than vite's; nothing outside it is readable
162
195
  or writable, and none of it exists in a build (`configureServer`).
163
196
 
164
- ## 5b. What a green `incanto-check` now covers
197
+ ## 6a. See what the camera sees — without a GPU
165
198
 
166
- `validateScene` / `incanto-check` hard-fail on all of these, so they never reach
167
- a browser:
199
+ `describe()` tells you a gem is at `[4, 1, -2]`. It cannot tell you whether you
200
+ can SEE it, which is the question you actually had. `framing()` answers that,
201
+ from geometry alone — no renderer, no canvas, no browser:
168
202
 
169
- - **Every prop's type AND its enum values** — `"mesh": "crystal"` names what is
170
- valid instead of printing `ok` and throwing at boot. All 31 enum props.
171
- - **`input{}`** a declaration's shape (`"keys": "Space"` instead of
172
- `["Space"]`, an unknown `type`, a wrong `touch`). This used to be checked only
173
- by `Engine.setScene`, i.e. never headlessly.
174
- - **`assets{}`** every entry an object with a non-empty `url`.
175
- - **`environment.preset`** resolved only inside Renderer3D before, so a typo
176
- survived every check and died at boot.
203
+ ```
204
+ > framing
205
+ camera /World/Player/Cam at [0, 2, 8] looking [0, -0.2, -0.97] fov 60
206
+ lit by: /World/Sun (DirectionalLight3D), environment.sky
207
+ 3 on screen, 1 off screen, 2 behind the camera
208
+ onScreen /World/Island (MeshInstance3D) [0, 0, 0] screen [0.02, -0.11] 8.2m
209
+ behind /World/Chest (ModelInstance3D) [0, 0, 40] 32m
210
+ offscreen /World/Gem (MeshInstance3D) [80, 0, 0] screen [4.4, 0] 80.1m
211
+ overlap /World/Platform ∩ /World/Gem
212
+ ```
177
213
 
178
- And `auditScene` (the warnings `incanto-check` prints) gained the loudest
179
- "why is my screen black" of all: **nothing lights this 3D scene** — no light
180
- node, no `environment` sky/hdri/preset, no ambient intensity. Ambient defaults to
181
- 0, so such a scene renders nothing, throws nothing and logs nothing.
214
+ Read it for the three visual failures that look identical from a node list:
182
215
 
183
- ## 6c. Engine diagnostics you can actually read
216
+ - **`behind`** the thing is fine, the camera has its back to it. Indistinguishable
217
+ from "my model did not load" until you can see this line.
218
+ - **`lit by: NOTHING`** — the scene renders black. No error, no log, nothing wrong
219
+ with any node.
220
+ - **`overlap`** — two declared colliders intersect: the gem is inside the platform.
184
221
 
185
- Every internal warning the engine produces — a GLB that 404'd, a body with no
186
- collider, a retarget that dropped bones, an exception swallowed inside a physics
187
- trigger handler used to go to `console.*` and nowhere else, which is the one
188
- channel a headless tool cannot read. The symptom was always the same: the model
189
- simply never appears, and `logs` comes back empty.
222
+ Also `incanto-play`'s `framing` command, and `describeFraming(scene)` /
223
+ `framingText(report)` from `incanto/test` for the structured form. A node counts
224
+ as on screen when its `size` box (or collider) is, not only its origin — so a
225
+ 200 m island whose origin sits off to the left is correctly `onScreen`.
190
226
 
191
- They now go to **`engine.log` as well as the console**, so:
227
+ **For actual pixels**, turn on `preserveDrawingBuffer` WebGL clears the buffer
228
+ the instant a frame composites, so `canvas.toDataURL()` and every screenshot
229
+ built on it come back BLANK without it:
192
230
 
193
- - `runScript().logs` contains them, and `describe()` prints them under the
194
- failures (`! warn: …`).
195
- - The debug overlay's **Logs** panel shows them live.
196
- - `game.assetErrors()` lists every asset that failed **with its reason and ref**
197
- — the direct answer to "why is my model not there". The store itself is
198
- reachable as `renderer.assets` in both dimensions.
231
+ ```ts
232
+ createGame3D({ ..., preserveDrawingBuffer: true });
233
+ // or in the scene: "environment": { "rendering": { "preserveDrawingBuffer": true } }
234
+ ```
199
235
 
200
- So the after-every-edit check is: `ok`, then **`stats().errors`**, then
201
- **`assetErrors()`**.
236
+ Then drive the page with whatever browser tool you have. It costs a buffer copy
237
+ per frame, which is why it is off by default.
202
238
 
203
239
  ## 6b. A thrown error no longer stops the game
204
240
 
@@ -223,6 +259,26 @@ stopped. It no longer can.
223
259
 
224
260
  So the check after every edit is still `ok`, plus: **read `stats().errors`**.
225
261
 
262
+ ## 6c. Engine diagnostics you can actually read
263
+
264
+ Every internal warning the engine produces — a GLB that 404'd, a body with no
265
+ collider, a retarget that dropped bones, an exception swallowed inside a physics
266
+ trigger handler — used to go to `console.*` and nowhere else, which is the one
267
+ channel a headless tool cannot read. The symptom was always the same: the model
268
+ simply never appears, and `logs` comes back empty.
269
+
270
+ They now go to **`engine.log` as well as the console**, so:
271
+
272
+ - `runScript().logs` contains them, and `describe()` prints them under the
273
+ failures (`! warn: …`).
274
+ - The debug overlay's **Logs** panel shows them live.
275
+ - `game.assetErrors()` lists every asset that failed **with its reason and ref**
276
+ — the direct answer to "why is my model not there". The store itself is
277
+ reachable as `renderer.assets` in both dimensions.
278
+
279
+ So the after-every-edit check is: `ok`, then **`stats().errors`**, then
280
+ **`assetErrors()`**.
281
+
226
282
  ## 7. See inside the RUNNING game (debug overlay)
227
283
 
228
284
  ```ts
@@ -5,15 +5,17 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
5
5
  manual, version-pinned to what is installed. Start with
6
6
  `incanto-building-3d-games.md`, `incanto-3d-character.md`,
7
7
  `incanto-gameplay-behaviors.md`, `incanto-environment.md` and
8
- `incanto-audio.md` — this template composes all five.
8
+ `incanto-hud.md` — this template composes them
9
+ and FINISH with `incanto-verifying-your-game.md` (rule 11).
9
10
  2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and
10
- replication all live in `src/game.scene.json`. Add nodes there (or with
11
+ replication all live in `src/*.scene.json`. Add nodes there (or with
11
12
  `npx incanto-editor`), NOT by constructing them ad-hoc in code.
12
13
  3. 🧠 LOGIC IS BEHAVIORS: gameplay code = small TypeScript Behavior classes,
13
14
  registered with `registerBehavior('Name', Class)` and attached in JSON via
14
15
  `"script": { "name": "Name" }`. Reach for a BUILT-IN gameplay behavior
15
- (Health, Chase, WaveSpawner, DamageOnContact, ScoreKeeper…) before writing
16
- your own this game's only custom behaviors are `Shoot` and `HudUpdater`.
16
+ before writing your own this template already uses
17
+ Health, DamageOnContact, Interactable, PathFollow, DayNight, and its only custom behaviors are
18
+ `IsleDirector` and `SwordStrike`.
17
19
  4. 🔑 UIDS ARE GENERATED: every node uid comes from
18
20
  `import { newUid } from 'incanto'`. NEVER invent readable uid strings.
19
21
  5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference
@@ -23,21 +25,40 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
23
25
  (Euler XYZ). Negative gravity falls.
24
26
  7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "box"|"sphere"|"capsule", … }`
25
27
  on a body node — never child shape nodes. Wrong shapes hard-fail at load.
26
- 8. 🖥️ THE HUD IS DOM, NOT NODES: 3D scenes have no 2D `UILayer`/`Label` nodes
27
- (those are `incanto/2d`). The HUD + crosshair + banner are plain HTML in
28
- `index.html`; `HudUpdater` writes the live numbers into them (guarded so
29
- headless runs are a no-op).
30
- 9. 🐛 DEBUG WHEN UNSURE: `physics.debugDraw = true` (the object
31
- `enablePhysics3D` returns) draws every collider in the running game; or boot
32
- with `?incanto_debug=1` for the live Explorer/Inspector/Stats overlay.
28
+ 8. 🖥️ THE HUD IS NODES: this template's HUD lives in the scene as a
29
+ `HudLayer` with `UiText`/`UiBar` children the engine builds the DOM for
30
+ them and positions them by anchor. Edit the HUD in `*.scene.json` like any
31
+ other node. (A DOM-in-`index.html` HUD is the other valid shape, and what
32
+ `tps-3d` does; both are supported do not mix them in one screen.)
33
+ 9. 🐛 DEBUG WHEN UNSURE: `VITE_INCANTO_DEBUG=1 bun run dev` adds the ☰ debug
34
+ menu Explorer/Inspector (click a node, see and edit its live props), Logs,
35
+ Stats and Colliders (every physics shape as a wireframe, drawn by the same
36
+ Rapier the game runs). There is NO `VITE_INCANTO_DEBUG=1` URL toggle: it was
37
+ removed as deploy-unsafe. Never ship the env flag on.
33
38
  10. 🎬 DELTA DISCIPLINE: only write props that differ from defaults — the
34
39
  loader treats unknown/garbage props as hard errors, which is your friend.
35
- 11. ✅ VERIFY LIKE A USER: `bun run check` validates the scene, `bun run verify`
36
- proves the loop headlessly, then `bun run dev` and actually PLAY what you
37
- changed (move, shoot, clear a wave) before declaring it done.
38
- 12. 🔍 X-RAY WHEN IT LOOKS WRONG: `VITE_INCANTO_DEBUG=1 bun run dev` adds a ☰
39
- debug menu Explorer/Inspector (click a node, see and edit its live props),
40
- Logs, Stats (fps · tris · draw calls) and Colliders (every physics shape as
41
- wireframe). Blocked in mid-air or falling through the floor is a Colliders
42
- question; a mystery frame-rate is a Stats question. Never ship the flag on.
40
+ 11. ✅ VERIFY LIKE A USER, THEN READ THE FOUR SIGNALS. `bun run check` and
41
+ `bun run verify` are the start, not the end you cannot SEE this game, and
42
+ each of these answers a failure that otherwise looks like nothing at all:
43
+
44
+ | read | it is not clean when |
45
+ | --- | --- |
46
+ | `bunx incanto-check` | the scene will not load, or renders black (no light) |
47
+ | `stats().errors` | something threw and was skipped to keep the game alive |
48
+ | `assetErrors()` | a model or texture 404'd — the thing is simply not there |
49
+ | `framing` | the camera points the wrong way, or nothing is lit |
50
+
51
+ ```bash
52
+ printf 'step 500\nframing\nquit\n' | bunx incanto-play src/game.scene.json
53
+ ```
54
+
55
+ `framing` reports what the camera SEES — on screen / off screen / behind it
56
+ — plus what lights the scene and which colliders intersect. `behind` looks
57
+ exactly like "my model did not load"; nothing else can tell you apart.
58
+ Then `bun run dev` and actually PLAY what you changed.
59
+ 12. 🧯 A THROWN ERROR NO LONGER STOPS THE GAME: a behavior that throws is
60
+ reported once (node, script, phase, file:line) and SKIPPED — your script
61
+ stops, the node it is attached to keeps running, the game plays on. So a
62
+ game that looks fine can still be broken: read `stats().errors`, and
63
+ `engine.resumeErroredNodes()` after you fix it.
43
64
  </userRequest>
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.31.0",
17
+ "incanto": "^0.33.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -5,15 +5,17 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
5
5
  manual, version-pinned to what is installed. Start with
6
6
  `incanto-building-3d-games.md`, `incanto-3d-character.md`,
7
7
  `incanto-gameplay-behaviors.md`, `incanto-environment.md` and
8
- `incanto-audio.md` — this template composes all five.
8
+ `incanto-audio.md` — this template composes them
9
+ and FINISH with `incanto-verifying-your-game.md` (rule 11).
9
10
  2. 🧱 STRUCTURE IS JSON: scenes, nodes, props, assets, input maps and
10
- replication all live in `src/game.scene.json`. Add nodes there (or with
11
+ replication all live in `src/*.scene.json`. Add nodes there (or with
11
12
  `npx incanto-editor`), NOT by constructing them ad-hoc in code.
12
13
  3. 🧠 LOGIC IS BEHAVIORS: gameplay code = small TypeScript Behavior classes,
13
14
  registered with `registerBehavior('Name', Class)` and attached in JSON via
14
15
  `"script": { "name": "Name" }`. Reach for a BUILT-IN gameplay behavior
15
- (Health, Chase, WaveSpawner, DamageOnContact, ScoreKeeper…) before writing
16
- your own this game's only custom behaviors are `Shoot` and `HudUpdater`.
16
+ before writing your own this template already uses
17
+ Health, DamageOnContact, Chase, WaveSpawner, ScoreKeeper, and its only custom behaviors are
18
+ `Shoot` and `HudUpdater`.
17
19
  4. 🔑 UIDS ARE GENERATED: every node uid comes from
18
20
  `import { newUid } from 'incanto'`. NEVER invent readable uid strings.
19
21
  5. 📦 ASSETS ARE DECLARED: `assets` entries need `type` + `url`; reference
@@ -23,21 +25,41 @@ YOU MUST follow these rules EXACTLY when vibe-coding on this Incanto template:
23
25
  (Euler XYZ). Negative gravity falls.
24
26
  7. 🧲 COLLIDERS ARE PROPS: `"collider": { "shape": "box"|"sphere"|"capsule", … }`
25
27
  on a body node — never child shape nodes. Wrong shapes hard-fail at load.
26
- 8. 🖥️ THE HUD IS DOM, NOT NODES: 3D scenes have no 2D `UILayer`/`Label` nodes
27
- (those are `incanto/2d`). The HUD + crosshair + banner are plain HTML in
28
- `index.html`; `HudUpdater` writes the live numbers into them (guarded so
29
- headless runs are a no-op).
30
- 9. 🐛 DEBUG WHEN UNSURE: `physics.debugDraw = true` (the object
31
- `enablePhysics3D` returns) draws every collider in the running game; or boot
32
- with `?incanto_debug=1` for the live Explorer/Inspector/Stats overlay.
28
+ 8. 🖥️ THE HUD IS DOM HERE: this template's HUD, crosshair and banner are
29
+ plain HTML in `index.html`, and `HudUpdater` writes the live numbers into
30
+ them (guarded, so headless runs are a no-op). That is a CHOICE, not a
31
+ limit `HudLayer` + `UiText`/`UiBar` nodes work in 3D scenes too (see
32
+ `incanto-hud.md`, and the `beacon-isle-3d` template). Do not mix them in
33
+ one screen.
34
+ 9. 🐛 DEBUG WHEN UNSURE: `VITE_INCANTO_DEBUG=1 bun run dev` adds the debug
35
+ menu — Explorer/Inspector (click a node, see and edit its live props), Logs,
36
+ Stats and Colliders (every physics shape as a wireframe, drawn by the same
37
+ Rapier the game runs). There is NO `VITE_INCANTO_DEBUG=1` URL toggle: it was
38
+ removed as deploy-unsafe. Never ship the env flag on.
33
39
  10. 🎬 DELTA DISCIPLINE: only write props that differ from defaults — the
34
40
  loader treats unknown/garbage props as hard errors, which is your friend.
35
- 11. ✅ VERIFY LIKE A USER: `bun run check` validates the scene, `bun run verify`
36
- proves the loop headlessly, then `bun run dev` and actually PLAY what you
37
- changed (move, shoot, clear a wave) before declaring it done.
38
- 12. 🔍 X-RAY WHEN IT LOOKS WRONG: `VITE_INCANTO_DEBUG=1 bun run dev` adds a ☰
39
- debug menu Explorer/Inspector (click a node, see and edit its live props),
40
- Logs, Stats (fps · tris · draw calls) and Colliders (every physics shape as
41
- wireframe). Blocked in mid-air or falling through the floor is a Colliders
42
- question; a mystery frame-rate is a Stats question. Never ship the flag on.
41
+ 11. ✅ VERIFY LIKE A USER, THEN READ THE FOUR SIGNALS. `bun run check` and
42
+ `bun run verify` are the start, not the end you cannot SEE this game, and
43
+ each of these answers a failure that otherwise looks like nothing at all:
44
+
45
+ | read | it is not clean when |
46
+ | --- | --- |
47
+ | `bunx incanto-check` | the scene will not load, or renders black (no light) |
48
+ | `stats().errors` | something threw and was skipped to keep the game alive |
49
+ | `assetErrors()` | a model or texture 404'd — the thing is simply not there |
50
+ | `framing` | the camera points the wrong way, or nothing is lit |
51
+
52
+ ```bash
53
+ printf 'step 500\nframing\nquit\n' | bunx incanto-play src/game.scene.json
54
+ ```
55
+
56
+ `framing` reports what the camera SEES — on screen / off screen / behind it
57
+ — plus what lights the scene and which colliders intersect. `behind` looks
58
+ exactly like "my model did not load"; nothing else can tell you apart.
59
+ Then `bun run dev` and actually PLAY what you changed.
60
+ 12. 🧯 A THROWN ERROR NO LONGER STOPS THE GAME: a behavior that throws is
61
+ reported once (node, script, phase, file:line) and SKIPPED — your script
62
+ stops, the node it is attached to keeps running, the game plays on. So a
63
+ game that looks fine can still be broken: read `stats().errors`, and
64
+ `engine.resumeErroredNodes()` after you fix it.
43
65
  </userRequest>
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.31.0",
16
+ "incanto": "^0.33.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {