incanto 0.61.0 → 0.63.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-verify.mjs +134 -55
  2. package/dist/2d.d.ts +42 -0
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +22 -0
  5. package/dist/3d.js +5 -5
  6. package/dist/{create-game-DH7JI5xx.js → create-game-lLeITaZ5.js} +8 -8
  7. package/dist/{create-game-IX5lEH0P.js → create-game-viBqXUoZ.js} +6 -6
  8. package/dist/{duplicate-CGqAmK2h.js → duplicate-BOOKmkQ7.js} +1 -1
  9. package/dist/editor.js +39 -18
  10. package/dist/{environment-presets-CNxCuhZF.js → environment-presets-QR7_75KJ.js} +5 -3
  11. package/dist/{gameplay-Dtzd2itW.js → gameplay-CuqoHHUB.js} +153 -16
  12. package/dist/gameplay.js +1 -1
  13. package/dist/index.d.ts +92 -1
  14. package/dist/index.js +6 -6
  15. package/dist/{loader-D7jTvDQv.js → loader-zDynoew_.js} +160 -2
  16. package/dist/net.js +1 -1
  17. package/dist/{physics-2d-CBnor8Zf.js → physics-2d-CllJXlic.js} +11 -3
  18. package/dist/{physics-3d-BTUfUSWO.js → physics-3d-BL_pFJ19.js} +123 -17
  19. package/dist/react.js +1 -1
  20. package/dist/{register-DL3izw8j.js → register-1cKM8DEj.js} +60 -9
  21. package/dist/{register-xuSRyD6b.js → register-Btm7_Emq.js} +158 -2
  22. package/dist/{replay-D7-yle3s.js → replay-Bhdntkvq.js} +15 -2
  23. package/dist/{split-screen-5Ban4q4n.js → split-screen-Dx0LvzqS.js} +2 -2
  24. package/dist/{src-DozXvyZS.js → src-BlV9Sv4m.js} +1 -1
  25. package/dist/{teardown-B6rwJOyS.js → teardown-BwhkcNt8.js} +1 -1
  26. package/dist/{test-Dch_7VQD.js → test-Ct1_zf5S.js} +64 -20
  27. package/dist/test.d.ts +41 -0
  28. package/dist/test.js +2 -2
  29. package/dist/vite.js +2 -2
  30. package/editor/assets/{agent8-CF1JL2tR.js → agent8-CLZXBRoM.js} +1 -1
  31. package/editor/assets/{debug-3QzYhOPA.js → debug-eaSKFAKW.js} +1 -1
  32. package/editor/assets/{index-CAD2c5ug.js → index-D66AuRwG.js} +91 -91
  33. package/editor/index.html +1 -1
  34. package/package.json +1 -1
  35. package/schemas/scene.schema.json +91 -0
  36. package/skills/README.md +4 -0
  37. package/skills/incanto-behaviors-and-scripts.md +37 -0
  38. package/skills/incanto-building-2d-games.md +20 -1
  39. package/skills/incanto-building-3d-games.md +1 -1
  40. package/skills/incanto-game-feel.md +3 -2
  41. package/skills/incanto-gameplay-behaviors.md +45 -6
  42. package/skills/incanto-node-reference.md +12 -0
  43. package/skills/incanto-physics-and-input.md +53 -2
  44. package/skills/incanto-scene-json-authoring.md +15 -1
  45. package/skills/incanto-verifying-your-game.md +31 -1
  46. package/skills/incanto-your-first-game.md +237 -0
  47. package/templates-app/beacon-isle-3d/package.json +1 -1
  48. package/templates-app/platformer-2d/package.json +1 -1
  49. package/templates-app/star-survivor/package.json +1 -1
  50. package/templates-app/star-survivor/src/game.scene.json +4 -2
  51. package/templates-app/tps-3d/package.json +1 -1
  52. package/templates-app/tps-3d/src/game.scene.json +3 -2
  53. package/templates-app/tps-3d/verify.ts +38 -0
  54. package/templates-app/village-quest-3d/package.json +1 -1
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-CAD2c5ug.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-D66AuRwG.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.61.0",
3
+ "version": "0.63.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -295,6 +295,9 @@
295
295
  {
296
296
  "$ref": "#/$defs/Particles3D"
297
297
  },
298
+ {
299
+ "$ref": "#/$defs/Respawn"
300
+ },
298
301
  {
299
302
  "$ref": "#/$defs/RigidBody2D"
300
303
  },
@@ -2040,6 +2043,10 @@
2040
2043
  "slopeLimitDeg": {
2041
2044
  "type": "number",
2042
2045
  "default": 45
2046
+ },
2047
+ "stepHeight": {
2048
+ "type": "number",
2049
+ "default": 0.35
2043
2050
  }
2044
2051
  },
2045
2052
  "additionalProperties": false
@@ -5327,6 +5334,90 @@
5327
5334
  },
5328
5335
  "required": ["name", "type"]
5329
5336
  },
5337
+ "Respawn": {
5338
+ "type": "object",
5339
+ "x-signals": ["respawned"],
5340
+ "properties": {
5341
+ "name": {
5342
+ "type": "string"
5343
+ },
5344
+ "uid": {
5345
+ "type": "string"
5346
+ },
5347
+ "type": {
5348
+ "const": "Respawn"
5349
+ },
5350
+ "groups": {
5351
+ "type": "array",
5352
+ "items": {
5353
+ "type": "string"
5354
+ }
5355
+ },
5356
+ "tags": {
5357
+ "type": "object"
5358
+ },
5359
+ "props": {
5360
+ "type": "object",
5361
+ "properties": {
5362
+ "target": {
5363
+ "type": "string",
5364
+ "description": "A node path: '%UniqueName', '/Absolute/From/Root', or relative to this node ('../Skin').",
5365
+ "default": ".."
5366
+ },
5367
+ "below": {
5368
+ "default": null
5369
+ },
5370
+ "to": {
5371
+ "type": "array",
5372
+ "default": []
5373
+ },
5374
+ "resetVelocity": {
5375
+ "type": "boolean",
5376
+ "default": true
5377
+ }
5378
+ },
5379
+ "additionalProperties": false
5380
+ },
5381
+ "script": {
5382
+ "type": "object",
5383
+ "properties": {
5384
+ "name": {
5385
+ "type": "string"
5386
+ },
5387
+ "props": {
5388
+ "type": "object"
5389
+ }
5390
+ },
5391
+ "required": ["name"]
5392
+ },
5393
+ "network": {
5394
+ "type": "object",
5395
+ "properties": {
5396
+ "mode": {
5397
+ "enum": ["owner", "observer"]
5398
+ },
5399
+ "sync": {
5400
+ "type": "array",
5401
+ "items": {
5402
+ "type": "string",
5403
+ "minLength": 1
5404
+ }
5405
+ },
5406
+ "throttleMs": {
5407
+ "type": "number"
5408
+ }
5409
+ },
5410
+ "additionalProperties": false
5411
+ },
5412
+ "children": {
5413
+ "type": "array",
5414
+ "items": {
5415
+ "$ref": "#/$defs/node"
5416
+ }
5417
+ }
5418
+ },
5419
+ "required": ["name", "type"]
5420
+ },
5330
5421
  "RigidBody2D": {
5331
5422
  "type": "object",
5332
5423
  "x-signals": ["triggerEnter", "triggerExit"],
package/skills/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # incanto skills (shipped)
2
2
 
3
3
  Full agent skills for building games with this **installed version** of Incanto.
4
+ **New here? `incanto-your-first-game.md` is the walkthrough** — scaffold to a game
5
+ with a real win and lose, the shape of the loop, and the traps that cost a first
6
+ author an hour. Then come back to this list.
7
+
4
8
  An AI agent should start with `incanto-scene-json-authoring.md`, then the domain skill
5
9
  for the task at hand, and **finish every task with `incanto-verifying-your-game.md`**
6
10
  — you cannot see the game, and that skill is how you find out whether it works.
@@ -145,6 +145,43 @@ declared by their classes. Escape hatch for runtime one-offs: `node.declareSigna
145
145
  the emitting node. For per-emitter logic, attach a small behavior to the emitter itself
146
146
  (see the `Pickup` pattern: a coin's own `triggerEnter` → its own `onTaken` → `queueFree`).
147
147
 
148
+ ## Respawn (core node — catch a player who leaves the world)
149
+
150
+ Hang it off the thing it guards. That is the whole feature:
151
+
152
+ ```json
153
+ { "name": "Player", "type": "RigidBody3D", "props": { "…": "…" },
154
+ "children": [
155
+ { "name": "Controller", "type": "CharacterController3D" },
156
+ { "name": "Catch", "type": "Respawn" }
157
+ ] }
158
+ ```
159
+
160
+ With no props it guards its parent, catches it 50 m under the spawn (1000 px in
161
+ 2D — the same line `incanto-playtest` calls `fell`), puts it back where it
162
+ started and zeroes the velocity the fall built up. Works in both dimensions.
163
+
164
+ | Prop | Default | Meaning |
165
+ |---|---|---|
166
+ | `target` | `".."` | who is being caught — the parent, normally |
167
+ | `below` | `null` | the line, in the scene's own down. `null` = auto (see above) |
168
+ | `to` | `[]` | where to put it back. Empty = wherever it started |
169
+ | `resetVelocity` | `true` | drop the speed the fall built up — off and it falls straight back through |
170
+
171
+ Signal `respawned(target, y)`, and a `falls` counter. It deliberately does NOT
172
+ decide what falling COSTS: wire `respawned` to a `ScoreKeeper.loseLife` or a
173
+ `Health.damage` if it should hurt, and leave it alone if it should not.
174
+
175
+ **A node, not a behavior**, because a node holds one behavior and the player's
176
+ is already spoken for.
177
+
178
+ Nothing in the engine did this before 0.62, and five shipped examples proved
179
+ what that cost: with the `plays` rung finally counting defects,
180
+ `basic-3d-sideview` failed 8 seeded runs of 8, `water-lake-3d` 7,
181
+ `water-ocean-3d` 5, `water-pool-3d` 4, `water-river-3d` 1 — every one of them
182
+ the player walking off the terrain and falling forever. One `Respawn` node each,
183
+ no TypeScript, and all five are clean.
184
+
148
185
  ## Timer (core node — never setTimeout in game logic)
149
186
 
150
187
  ```json
@@ -170,7 +170,16 @@ ALWAYS confirm in the browser (`bun run dev`) that the feet touch the surface.
170
170
  | `zoom` | `1` | 2 = world pixels doubled |
171
171
  | `limits` | `[]` | `[minX,minY,maxX,maxY]` — view rect clamped inside |
172
172
  | `current` | `false` | mark exactly one |
173
- Camera `position` is the view CENTER. With no camera, the view shows (0,0)–(w,h).
173
+ Camera `position` is the view CENTER, and it composes with its ancestors like any
174
+ other node — so parenting the camera to the player (the Godot/Phaser idiom) works,
175
+ and so does putting it inside the container that holds the level. `follow` and the
176
+ `FollowCamera` behavior compare WORLD positions at both ends, so the target can live
177
+ under a different parent than the camera. With no camera, the view shows (0,0)–(w,h).
178
+
179
+ (Before 0.62 the view centre was the camera's LOCAL prop. A camera under a player at
180
+ `[1400, 900]` framed the world origin — and `framing`, which composes full world
181
+ matrices, reported `centred [1400, 900] … 1 in view`, certifying a frame the renderer
182
+ never drew.)
174
183
 
175
184
  ### `Label`
176
185
  `text`, `fontSize 16`, `color '#ffffff'`, `font 'monospace'`, `align 'left'|'center'|'right'`,
@@ -357,6 +366,16 @@ Mark unchanging subtrees (tile decor, backgrounds) `static: true` — the
357
366
  renderer syncs them once and skips them every frame. Flip to `false` to
358
367
  resume live syncing.
359
368
 
369
+ The freeze waits for the node to have something worth freezing: a `Sprite2D` or
370
+ `TileMap2D` whose texture has not decoded yet keeps syncing until it has, and a
371
+ texture that never arrives never freezes at all (`assetErrors()` is what reports
372
+ that one). Before 0.62 the latch fired on frame one regardless — and a
373
+ `TextureLoader` fills `.image` asynchronously while the renderer loads and syncs
374
+ in the same frame, so the state frozen was `visible: false`, permanently. A
375
+ static backdrop was invisible for the life of the game with nothing reporting
376
+ it: the texture loaded fine, `stats().errors` was 0, and `framing` reads props
377
+ rather than pixels and still called the node on-screen.
378
+
360
379
  ## Before you hand it back
361
380
 
362
381
  You cannot see this game. `incanto-verifying-your-game.md` is how you find out
@@ -69,7 +69,7 @@ All 3D nodes extend `Node3D` and therefore have the transform props:
69
69
  |---|---|---|
70
70
  | `position` | `[0,0,0]` | meters; +Y up |
71
71
  | `rotation` | `[0,0,0]` | **degrees**, Euler XYZ |
72
- | `scale` | `[1,1,1]` | |
72
+ | `scale` | `[1,1,1]` | resizes the DRAWN mesh. `Terrain3D`/`Water3D`/`River3D` reject a non-unit scale at load — they keep a CPU twin (`heightAt`, the heightfield collider, the water surface query) built from `size`, so scaling would move the picture and not the ground. Resize those with `size` |
73
73
  | `visible` | `true` | hides the whole subtree's rendering |
74
74
 
75
75
  ### `MeshInstance3D`
@@ -20,7 +20,8 @@ feel of /Game/Player
20
20
  turnaround 367 ms
21
21
  input → motion 1 frame(s)
22
22
 
23
- jump apex 0.262 u
23
+ jump apex 0.262 u (tapped)
24
+ held apex 0.981 u ← size your level against THIS
24
25
  time to apex 133 ms
25
26
  airtime 267 ms
26
27
  coyote window 100 ms
@@ -47,7 +48,7 @@ reports **0 ms**. That was a real bug in this engine's own 3D controller until
47
48
  | `to a stop` | how much you slide. A platformer that overshoots ledges has a long one. |
48
49
  | `turnaround` | the cost of changing your mind. The number players feel most and name least. |
49
50
  | `input → motion` | 1 frame is immediate. More than 2 and something is buffering. |
50
- | `jump apex` / `airtime` | compare against the gaps and platform heights in your level. |
51
+ | `jump apex` / `airtime` | compare against the gaps and platform heights in your level. **Use the HELD apex** — the plain one is a one-frame TAP, and with `jumpCutMultiplier` (the variable-height jump the 3D character skill recommends at `0.45`) that is a fraction of the real jump: measured 0.478 u tapped against 1.831 u held on the same scene. The held line only appears when the two differ. |
51
52
  | `coyote window` | **0 reads to players as "the controls are broken"**, not as a missing feature. |
52
53
  | `jump buffer` | 0 throws away a press made a few frames before landing. |
53
54
 
@@ -222,18 +222,41 @@ Signals: `totalChanged(total)` — Methods: `collect(value)` — state: `total`
222
222
  Deals damage to whatever it touches — projectiles, spikes, lava, enemy hitboxes.
223
223
  **Goes on an Area.** On `triggerEnter`, finds the contacted entity's `Health` and
224
224
  calls `damage(amount)`, then emits `dealtDamage(amount, healthOwnerNode)`.
225
- `oncePerTarget` (default) prevents re-hitting a resting body every frame;
226
- `destroySelf` frees the hazard after a hit (single-use projectiles).
227
225
 
228
226
  | Prop | Default | Meaning |
229
227
  |---|---|---|
230
228
  | `amount` | `10` | HP removed per contact |
231
229
  | `targetGroup` | `""` | only damage targets whose Health-owner is in this group (`""` = any) |
232
- | `oncePerTarget` | `true` | damage each target at most once |
230
+ | `oncePerTarget` | `true` | damage each target at most **once, ever** |
231
+ | `repeatEvery` | `0` | seconds between re-hits while still overlapping (`0` = entry only) |
233
232
  | `destroySelf` | `false` | `queueFree()` after the first hit |
234
233
 
235
234
  Signals: `dealtDamage(amount, target)`
236
235
 
236
+ ### `repeatEvery` is the "and it keeps hurting" knob
237
+
238
+ **Contact events fire on ENTRY and EXIT, not per frame.** So `oncePerTarget:
239
+ false` does not mean "hurt every frame" — it means "hurt again on RE-entry". An
240
+ enemy that closes on you and stops moving has entered once, and deals one hit
241
+ and then nothing. Measured, 6 seconds of unbroken overlap at `amount: 10`:
242
+
243
+ ```
244
+ oncePerTarget=false repeatEvery=0 → hp 90 ← one hit, then nothing
245
+ oncePerTarget=false repeatEvery=0.5 → hp 0
246
+ oncePerTarget=true repeatEvery=0.5 → hp 0
247
+ oncePerTarget=true repeatEvery=0 → hp 90
248
+ ```
249
+
250
+ `repeatEvery` is the only prop that makes a resting overlap keep hurting, and it
251
+ overrides `oncePerTarget` — lava pools, poison clouds, an enemy standing on you.
252
+ Set it on **every enemy contact hitbox**; without it a game plays as though the
253
+ enemies are harmless the moment one of them corners you, and every check comes
254
+ back green.
255
+
256
+ Pair it with `Health.invulnerableFor`, which is the real "don't drain me every
257
+ frame" guard: with `invulnerableFor: 0.6`, a `repeatEvery` under 0.6 s just
258
+ lands on i-frames, so 0.5–1 s is the useful range.
259
+
237
260
  **`targetGroup` — stop enemies killing each other.** With no collision layers in
238
261
  v0 (everything overlaps everything), an enemy's contact hitbox would damage any
239
262
  `Health` it touches, including other enemies clustered at the spawn point. Gate
@@ -513,6 +536,11 @@ pickups, spinning/pulsing coins. Drives one `axis` of `position`, `rotation`
513
536
  "script": { "name": "Oscillate", "props": { "axis": "z", "amplitude": 180, "frequency": 0.5, "mode": "rotation" } } }
514
537
  ```
515
538
 
539
+ On a physics BODY, `mode: "rotation"` turns the collider too — a spinning blade
540
+ cuts, a swinging gate blocks. (Before 0.62 it turned only the mesh: a body's
541
+ angle was applied once at creation and never followed again, so every spinner
542
+ built this way was decoration.)
543
+
516
544
  ## Buoyancy
517
545
 
518
546
  Float on water. Hangs on a `RigidBody3D` and asks the water how high it is at
@@ -585,7 +613,7 @@ later clones get `EnemyTemplate2`, `EnemyTemplate3`, …)
585
613
  | `prefab` | `""` | node path of the template to clone (required) |
586
614
  | `interval` | `1` | seconds between spawns |
587
615
  | `max` | `0` | max LIVE instances (0 = unlimited) |
588
- | `at` | `[]` | offset added to the spawner position ([x,y(,z)]) |
616
+ | `at` | `[]` | where the clone appears, as an offset from the spawner ([x,y(,z)]) |
589
617
  | `autoStart` | `true` | begin ticking at ready |
590
618
  | `total` | `0` | total LIFETIME spawns (0 = infinite) |
591
619
 
@@ -634,6 +662,15 @@ MOVEMENT by design: **compose** it with `DamageOnContact` (deal damage), `Lifeti
634
662
  `direction` is a vector OR `'forward'` (from the node `rotation`); it's a
635
663
  union-typed prop so its default is `null` (= `'forward'`).
636
664
 
665
+ In 3D, `'forward'` is the engine's ONE facing convention — **+Z**, the same
666
+ `atan2(dx, dz)` basis `CharacterController3D` uses and `incanto-3d-character.md`
667
+ spells out under "the +Z-FORWARD rule". So aim the bullet (or its shooter) with
668
+ that formula and it flies at what you aimed at; do not add 180°. (Before 0.62
669
+ this returned three.js's −Z instead, so every `'forward'` shot left the muzzle
670
+ pointing at the shooter's back — at the right speed, with no error, which reads
671
+ as a gun that simply never hits anything.) In 2D `rotation` is a scalar and
672
+ `'forward'` is +x turned clockwise by it, unchanged.
673
+
637
674
  | Prop | Default | Meaning |
638
675
  |---|---|---|
639
676
  | `speed` | `300` | units per second along `direction` |
@@ -679,7 +716,8 @@ interaction is a `connection`.
679
716
  "script": { "name": "Pickup", "props": { "value": 1 } } },
680
717
  { "name": "Spike", "type": "Area2D", "props": { "position": [120, 80],
681
718
  "collider": { "shape": "rect", "size": [24, 24] } },
682
- "script": { "name": "DamageOnContact", "props": { "amount": 40, "oncePerTarget": false } } }
719
+ "script": { "name": "DamageOnContact",
720
+ "props": { "amount": 40, "oncePerTarget": false, "repeatEvery": 0.5 } } }
683
721
  ]
684
722
  },
685
723
  "connections": [
@@ -764,7 +802,8 @@ glue an early sim had to hand-write):
764
802
  { "name": "Hit", "type": "Area2D",
765
803
  "props": { "collider": { "shape": "circle", "radius": 12 } },
766
804
  "script": { "name": "DamageOnContact",
767
- "props": { "amount": 10, "targetGroup": "player", "oncePerTarget": false } } }
805
+ "props": { "amount": 10, "targetGroup": "player",
806
+ "oncePerTarget": false, "repeatEvery": 0.5 } } }
768
807
  ] }
769
808
  ] }
770
809
  ]
@@ -239,6 +239,7 @@ Signals: `triggerEnter(other)` · `triggerExit(other)`
239
239
  | `velocity` | `[0,0,0]` | array |
240
240
  | `stickToGround` | `true` | boolean |
241
241
  | `slopeLimitDeg` | `45` | number |
242
+ | `stepHeight` | `0.35` | number |
242
243
 
243
244
  Signals: `triggerEnter(other)` · `triggerExit(other)`
244
245
 
@@ -706,6 +707,17 @@ Signals: `finished`
706
707
 
707
708
  Signals: `finished`
708
709
 
710
+ ## `Respawn` — `incanto`
711
+
712
+ | Prop | Default | Kind |
713
+ |---|---|---|
714
+ | `target` | `".."` | node path |
715
+ | `below` | `null` | null |
716
+ | `to` | `[]` | array |
717
+ | `resetVelocity` | `true` | boolean |
718
+
719
+ Signals: `respawned`
720
+
709
721
  ## `RigidBody2D` — `incanto/2d`
710
722
 
711
723
  | Prop | Default | Kind |
@@ -50,6 +50,16 @@ the corners. Give the body its mesh as a CHILD and let the shape follow:
50
50
  "props": { "mesh": "box", "size": [14, 0.22, 2.2] } } ] }
51
51
  ```
52
52
 
53
+ The child's OWN `position`, `rotation` and `scale` are part of the shape: raise the plank on
54
+ the child instead of the body and the collider goes up with it; scale the crate 4× and it is
55
+ solid at 4×. (It was not before 0.62 — the child transform was dropped and the collider sat
56
+ at the body's origin at the authored size, so you walked through the deck you could see and
57
+ landed on an invisible slab at ground level. Measured: a plank drawn with its top at y=3.2
58
+ had its surface at y=0.2.) `collider.offset` stacks on top of that.
59
+
60
+ A ball, a capsule and a cylinder have ONE radius between them, so a child scaled unevenly in
61
+ x and z has no exact shape — the widest of the two is used and the engine says so by name.
62
+
53
63
  Bodies ignore an ANCESTOR's rotation, so a yawed structure puts each body at top level with
54
64
  its own `rotation` — the child mesh then turns with it, visual and collider together.
55
65
 
@@ -143,7 +153,13 @@ engine.input.isPressed('jump'); // held
143
153
  engine.input.justPressed('jump'); // one-frame edge (settled per tick)
144
154
  engine.input.getVector('move'); // normalized {x, y}, y-down (up = -y)
145
155
  ```
146
- Keys are `KeyboardEvent.code` strings (`KeyW`, `Space`, `ArrowLeft`). Unknown actions and
156
+ Keys are `KeyboardEvent.code` strings (`KeyW`, `Space`, `ArrowLeft`) — plus `Mouse0..4`
157
+ and `Pad0..16`, which live in the same space. **A value outside that set is a load
158
+ error** naming the one you probably meant (`'W' → "KeyW"`, `'space' → "Space"`,
159
+ `'Shift' → "ShiftLeft" or "ShiftRight"`): the map is looked up by exact string, so a
160
+ misspelled code binds to nothing and the player silently does not have that control.
161
+ An EMPTY key list is still fine — `{"keys": [], "touch": "button"}` is a touch-only
162
+ control. Unknown actions and
147
163
  wrong-kind queries (getVector on a button) are hard errors listing valid names.
148
164
  Action declarations RESET on every `setScene` (no keybind bleed between scenes).
149
165
 
@@ -239,7 +255,12 @@ Injected state combines with key state (vectors clamped to unit length).
239
255
  - Pickups: `Area2D` in a group + `triggerEnter` → check `other.isInGroup('player')` →
240
256
  `queueFree()` — wire it via JSON `connections` to a behavior method (the `Pickup`
241
257
  pattern in `incanto-behaviors-and-scripts.md`), or imperatively with `node.on(...)`.
242
- - Teleport: just write `node.position` — the body follows. Launch: write `linearVelocity`.
258
+ - Teleport: just write `node.position` — the body follows. **`node.rotation` follows
259
+ too**: turn a static or kinematic body at runtime and its COLLIDER turns with it, so
260
+ a spinning blade cuts and a swinging gate blocks. (It did not before 0.62 — the
261
+ angle was applied once at creation, the mesh turned and the collider stayed put.)
262
+ A free dynamic body's rotation belongs to the solver and is left alone.
263
+ Launch: write `linearVelocity`.
243
264
  - 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
244
265
  `Player` node type. Verified in Chromium end-to-end.
245
266
 
@@ -336,6 +357,36 @@ the tree says, without your scene falling over while you look at it. Use it for
336
357
  any "show me the collision geometry" tool. Do NOT use it to freeze a running
337
358
  game — that is `engine.timeScale = 0`, which keeps the world coherent.
338
359
 
360
+ ## A walking body climbs a step (`stepHeight`, 3D)
361
+
362
+ `CharacterBody3D.stepHeight` (default `0.35` m, `0` = off) is how high a ledge
363
+ the body walks UP without jumping. It is Rapier's autostep, and **Rapier does not
364
+ autostep unless it is asked** — before 0.63 nothing asked, so any walking body
365
+ was stopped dead by a curb.
366
+
367
+ You will not see this on the player: `CharacterController3D` rides a hover spring
368
+ rather than the character controller, so it floats over small ledges already.
369
+ It is the ENEMIES that walk, and a chaser walks a straight line — it cannot go
370
+ around. Measured on the shipped `tps-3d` template, whose arena has a 0.6 m ramp
371
+ between the spawn and the player, standing still for 45 seconds:
372
+
373
+ ```
374
+ before hits=0 hp=100 closest an enemy ever got = 33.91m
375
+ after hits=9 hp=.. closest = 1.80m (exactly Chase.stopRange)
376
+ ```
377
+
378
+ They chased at full speed the whole time and piled up against the near face of a
379
+ knee-high box. Raise `stepHeight` for a world with real stairs; set it to `0` for
380
+ something that genuinely should be blocked by a curb.
381
+
382
+ **It loses to a large downward velocity.** Autostep happens inside the movement
383
+ solve, so a body driven with a big constant gravity term every frame never
384
+ climbs. Same 0.3 m ledge, same `stepHeight: 0.35`, only the `velocity[1]`
385
+ differs: `0` → cleared, `−0.1` → cleared, `−2` → stopped at the face. If you are
386
+ integrating your own gravity, apply it as a falling SPEED that resets on the
387
+ ground, not as a constant push. (`moveBody`, which `Chase` and `Patrol` use,
388
+ derives velocity from a position delta and carries no such term.)
389
+
339
390
  ## Moving platforms carry their riders (2D **and** 3D)
340
391
 
341
392
  A `CharacterBody2D`/`CharacterBody3D` standing on a body that moves is dragged
@@ -299,7 +299,21 @@ that do exist at the failing spot).
299
299
  - `signal` must be DECLARED on the from node (the class's or its behavior's
300
300
  `static signals`) — validated at load → `UNKNOWN_SIGNAL` otherwise.
301
301
  - `handler` must be a method on the target node OR its behavior (script) — both are
302
- validated hard at load. Otherwise → `UNKNOWN_HANDLER`. On **both**, it is
302
+ validated hard at load. Otherwise → `UNKNOWN_HANDLER`.
303
+ - **The ARGUMENTS are checked when the wire fires**, not at load — nothing declares
304
+ how many a signal carries, so the pair can only be compared at the moment both
305
+ are known. A handler given fewer than it needs is reported once, by name:
306
+
307
+ ```
308
+ [incanto] 'died' from 'Enemy' carries 0 argument(s) and 'Keeper.addScore' needs 1.
309
+ The missing one(s) arrive as undefined — a number handler gets NaN and
310
+ never recovers.
311
+ ```
312
+
313
+ `died → ScoreKeeper.addScore` is the natural way to score a kill and it sets the
314
+ score to `NaN` on the first one, with the win condition then permanently out of
315
+ reach. A handler that DEFAULTS what it is not given is correct and stays silent:
316
+ `won → GameFlow.win` works, because `win(text = 'YOU WIN')` needs nothing. On **both**, it is
303
317
  `AMBIGUOUS_HANDLER`: the node's method wins, so the script's would never run
304
318
  and nothing would say so. A core node answers to 62 public methods before any
305
319
  adapter adds more (`stop` `play` `show` `clear` `say` `start` `free` …), which
@@ -32,6 +32,7 @@ failures — but `framing` and `assetErrors()` you have to ASK for.
32
32
  $ bunx incanto verify # finds your scene AND your behaviours
33
33
  · behaviours: src/behaviors.ts (found, not named)
34
34
  ✓ loads — the scene is legal and its assets resolve
35
+ ! /Game/Chest: script Chest — target '%Ke' matches nothing
35
36
  ? plays — 8 runs played without reaching a win (4 lost, 4 ran out the clock)
36
37
  ✓ feels — 5 of 7 fired — silent: /Game/Boss/Roar, /Game/Boss/Boom
37
38
  · agrees — not run — this scene has no `multiplayer` header
@@ -67,11 +68,40 @@ encodes so you do not have to remember them:
67
68
 
68
69
  - a **failed** rung makes the ones above it meaningless, so only the first is
69
70
  worth reading — the rest are marked `·` skipped;
71
+ - a rung that PASSED can still have something to say. Anything its own tool
72
+ warned about is printed under it with a `!` — `loads` runs `incanto-check`,
73
+ and the ladder used to read only whether it passed and drop the warnings, so a
74
+ scene the check describes as "It will render black" came back as
75
+ `✓ loads — the scene is legal and its assets resolve`;
70
76
  - an **unmeasured** rung (`?`) is not a failure. "No dev server" means the
71
77
  question was never asked; treating that as a broken game sends you editing a
72
78
  scene that is fine;
73
79
  - a scene that declares **no win** is not a scene that cannot be won. A
74
- walkabout has no end, and that is reported as unmeasured, not failed.
80
+ walkabout has no end, and that is reported as unmeasured, not failed;
81
+ - but a **defect** fails the rung whatever the scene declares. `error` (a
82
+ behaviour threw) and `fell` (the player left the world) are counted BEFORE
83
+ anything else is decided:
84
+
85
+ ```
86
+ ✗ plays — 8 of 8 runs ended in a defect (8 fell out of the world)
87
+ next: the player left the world with nothing catching them — give the level a
88
+ floor, walls, or a respawn
89
+ ```
90
+
91
+ The routed answer to that last one is a `Respawn` node under the player
92
+ (`incanto-behaviors-and-scripts.md`) — one line of JSON, no per-frame `y`
93
+ check in your own code.
94
+
95
+ This used to be the sentence `8 runs played without error` — printed for a
96
+ scene with no declared win no matter what happened in it, including 8/8
97
+ `fell`, and exit 0. The child `incanto-playtest` had just printed
98
+ `✗ fell in 8/8`; the parent said the opposite of it.
99
+
100
+ `stuck` — the player went nowhere — is the third defect and the one that needs
101
+ a fair chance before it counts. It is only a failure when there WAS someone to
102
+ move and your behaviours were loaded. A scene with no character controller, no
103
+ `player` group and no node named `Player` reports `nothing here is drivable`,
104
+ and a run without `--behaviors` reports that instead: neither is a broken game.
75
105
 
76
106
  `feels` is the sound-and-effects rung: it lists what the scene DECLARES against
77
107
  what actually fired during the playtest. A game whose feedback is wired but