incanto 0.69.0 → 0.71.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 (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
@@ -324,3 +324,55 @@ rendered a five-pass frame at 3.3 ms where `low` rendered one pass at 1.24 ms
324
324
  A tier is also REVERSIBLE now: it is applied against the scene FILE rather than
325
325
  against whatever the last tier left behind, so `low` → `high` restores exactly
326
326
  what you authored instead of leaving it stripped.
327
+
328
+ ## How big a game actually is, and the chunk named after the wrong thing
329
+
330
+ Measured on all six templates, scaffolded from the packed tarball and built:
331
+
332
+ ```
333
+ eager on disk
334
+ molehill-2d 967 KB 5.8 MB
335
+ platformer-2d 981 KB 5.9 MB
336
+ star-survivor 962 KB 5.9 MB
337
+ beacon-isle-3d 1414 KB 5.8 MB
338
+ tps-3d 1376 KB 5.8 MB
339
+ village-quest-3d 1469 KB 5.8 MB
340
+ ```
341
+
342
+ **`dist/` is six megabytes and a player downloads one.** The rest is lazy: the
343
+ Rapier wasm builds (2D and 3D, ~4.5 MB together) load only when a scene has
344
+ physics bodies, and the editor chunk only if someone opens it. Read the
345
+ `<script>` and `<link rel=modulepreload>` tags in `dist/index.html` for what
346
+ actually loads — `du -sh dist` answers a question nobody asked.
347
+
348
+ **The vendor chunk is named after whichever module the bundler happened to pick,
349
+ and it is not the one you think.** In every one of those builds the ~790 KB
350
+ chunk is called `quiet-rapier-*.js` and contains **three.js** — no Rapier at
351
+ all. A size audit that reads names concludes the physics engine is eager in a
352
+ mouse game with no bodies in it. Read the contents:
353
+
354
+ ```bash
355
+ grep -c WebGLRenderer dist/assets/quiet-rapier-*.js # three.js lives here
356
+ ```
357
+
358
+ Name them yourself if it matters to you — it changes nothing about what loads,
359
+ only what the file is called:
360
+
361
+ ```ts
362
+ export default defineConfig({
363
+ build: {
364
+ rollupOptions: {
365
+ output: {
366
+ manualChunks: (id) =>
367
+ id.includes('node_modules/three') ? 'three'
368
+ : id.includes('@dimforge/rapier') ? 'rapier'
369
+ : undefined,
370
+ },
371
+ },
372
+ },
373
+ });
374
+ ```
375
+
376
+ three is eager in a 2D game too, and that is not a bug: `Renderer2D` draws
377
+ through the same WebGL renderer as the 3D one — shared-PlaneGeometry meshes and
378
+ an orthographic camera. It is the price of one renderer instead of two.
@@ -80,14 +80,22 @@ mesh or one model and warns when the body carries more.
80
80
  - **`StaticBody2D/3D`** — immovable (ground, walls). Props: `collider`.
81
81
  - **`RigidBody2D/3D`** — simulated. Props: `collider`, `mass 1`, `gravityScale 1`,
82
82
  `fixedRotation false`, `friction 0.5`, `restitution 0`, `linearVelocity` (write to launch,
83
- read back every step).
83
+ read back every step; `velocity` is an alias for it). 3D also has
84
+ `angularVelocity`. Every one of them is LIVE — write it mid-run and the solver
85
+ picks it up on the next step.
84
86
  - **`Area2D/3D`** — sensor. Emits `triggerEnter(other)` / `triggerExit(other)`. Never blocks
85
87
  movement. Solid bodies emit the same signals on real contact (one mental model).
86
88
  Areas overlapping OTHER Areas fire too (e.g. a weapon-hitbox Area over an
87
89
  enemy-hitbox Area) — neither side needs to be a Body.
88
90
  - **`CharacterBody2D/3D`** — kinematic character (Rapier KCC). Props: `collider`
89
- (capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45`.
90
- API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`.
91
+ (capsule recommended), `velocity`, `stickToGround true`, `slopeLimitDeg 45`,
92
+ `stepHeight` (3D `0.35` m / 2D `35` px — see below).
93
+ API: `moveAndSlide()` (call from `fixedUpdate`), `isOnFloor()`,
94
+ **`isOnWall()`** and **`isOnCeiling()`**. Which wall is answered in the shape
95
+ each dimension has: 2D's `wallSide()` is `-1` left / `+1` right, 3D's
96
+ `wallNormal()` is the wall's normal pointing away from it — the direction a
97
+ wall jump pushes. Without them a wall jump or a wall slide is not awkward to
98
+ write, it is impossible: there is nothing to ask.
91
99
 
92
100
  ### `enabled` — a collider that is off for now
93
101
 
@@ -105,6 +113,13 @@ sword.enabled = true; // the active window of the swing
105
113
  sword.enabled = false; // …and done
106
114
  ```
107
115
 
116
+ **A collider's keys are CLOSED**: `shape`, `size`, `radius`, `height`, `offset`
117
+ and (2D) `oneWay` — plus `vertices`/`indices` for a 3D trimesh. Anything else is
118
+ a load error naming the nearest one, because
119
+ `{"shape": "circle", "radius": 8, "offest": [0, -24]}` used to load clean with
120
+ its hitbox 24 px from where it was meant to be. `friction`, `restitution` and
121
+ mass are props of the BODY, not of its collider.
122
+
108
123
  **Do not arm a hitbox by swapping `collider` in and out.** Replacing the
109
124
  collider prop tears the rigid body down and builds a new one — twice per swing —
110
125
  and a scene authored `"collider": {}` warns `has no collider — physics skips it`
@@ -169,9 +184,19 @@ Space/arrows; pass `'none'` to opt out). Keys typed into INPUT/TEXTAREA/SELECT/
169
184
  contenteditable are ignored entirely, so DOM UI overlays keep working.
170
185
  `engine.input.dispose()` detaches every attached source (keyboard and pointer).
171
186
 
172
- ⚠️ `justPressed` edges settle once per RENDER tick: on a dropped frame with multiple fixed
173
- steps, the edge is visible in every fixed step of that tick. Gate one-shot actions on a
174
- state change too (the jump example works because `isOnFloor()` flips after the first step).
187
+ **One press is ONE edge**, in `update` and in `fixedUpdate` alike the fixed pass has
188
+ its own edge view, drained after its first step, so a dropped frame carrying five steps
189
+ delivers the edge once and a 144 Hz tick that runs no fixed step at all does not lose it.
190
+ Same for `justReleased`. (Until 0.63 this warned that the edge repeated in every fixed
191
+ step of a slow frame; it has not since the fixed pass got its own edge sets, and nothing
192
+ noticed for two releases.)
193
+
194
+ ⚠️ What IS worth knowing: **a press injected from inside the frame lands on the NEXT
195
+ tick.** `pressAction`/`releaseAction` called from a `fixedUpdate`, an `update` or an
196
+ `engine.updated` handler — replay drivers, AI input, touch overlays — is invisible for
197
+ the rest of that tick and arrives at the next one, because an edge created after the
198
+ pass that reads it would be created and destroyed without anything seeing it. The HELD
199
+ state (`isPressed`) is immediate either way.
175
200
 
176
201
  ## Touch controls (mobile web)
177
202
 
@@ -307,11 +332,25 @@ body.velocity = [2, 0, 0]; // or set the velocity outright
307
332
 
308
333
  `applyImpulse` is what the character controller itself runs on. It is an
309
334
  IMPULSE, not a force: it changes velocity once, so a continuous push is one call
310
- per frame from `update(dt)` scaled by `dt`.
311
-
312
- Readable/writable body state: `velocity`, `mass`, `gravityScale`, `friction`,
313
- `restitution`, `fixedRotation`. There is no torque call and no `angularVelocity`
314
- yet a body's spin can be neither set nor read.
335
+ per frame from `update(dt)` scaled by `dt`. In 2D the impulse is in px·kg/s,
336
+ y-down, exactly like `linearVelocity`.
337
+
338
+ Readable/writable body state, in BOTH adapters: `velocity` (= `linearVelocity`),
339
+ `angularVelocity`, `mass`, `gravityScale`, `friction`, `restitution`. Read the
340
+ spin to see how fast a lever is swinging; write it to launch a spinning body.
341
+ 3D's is a 3-vector about the world axes, 2D's is the scalar rad/s about z with
342
+ the same sign as `rotation` (clockwise, because 2D is y-down). There is no
343
+ torque call in either. Writes land on the next step — these are live props, not
344
+ load-time constants.
345
+
346
+ `fixedRotation` is authored, not live: it locks the body's rotation when the
347
+ body is CREATED. Changing it mid-run does nothing, and a locked body's
348
+ `angularVelocity` is zero rather than whatever was authored.
349
+
350
+ > Four of these were once false, silently: `applyImpulse` existed only in
351
+ > 3D (a TypeError in 2D), `body.velocity = […]` wrote a stray field nothing read
352
+ > in both, `mass`/`friction`/`restitution` were read once at creation in both,
353
+ > and `gravityScale` was live in 3D and dead in 2D.
315
354
 
316
355
  ## What is inside this Area right now?
317
356
 
@@ -354,7 +393,12 @@ question you usually mean.
354
393
  only** — never their contact with the rest of the world. Defaulting a spring
355
394
  off drops a barrel roped to the floor straight through it, which is how this
356
395
  table was arrived at.
357
- 3. **`anchor`/`targetAnchor` are local offsets from each body's ORIGIN**,
396
+ 3. **A joint's two ends must be two DIFFERENT bodies.** It is a child of one and
397
+ points at the other, so `"target": ".."` — which reads like naming the body
398
+ it belongs to — is now a load-time error: Rapier accepts a body jointed to
399
+ itself and constrains nothing, and the rope fell exactly as if it were not
400
+ there.
401
+ 4. **`anchor`/`targetAnchor` are local offsets from each body's ORIGIN**,
358
402
  resolved against the bodies' positions at load. If the two disagree the solver
359
403
  snaps the body into place on the first frame, and a 4 cm typo is silent. Author
360
404
  them with arithmetic (generate the scene) rather than by hand.
@@ -380,19 +424,41 @@ Both runtimes expose the same query (2D in PIXELS y-down, 3D in meters):
380
424
 
381
425
  ```ts
382
426
  const hit = physics.castRay(origin, dir, maxLen, excludeBody?, { staticOnly?: true });
427
+ // hit: { distance, normal, node, point } — `point` is WHERE it landed, in the
428
+ // scene's own units. `dir` may be any length; the ray normalizes it, so
429
+ // `origin + dir * distance` by hand is wrong unless you normalize first.
383
430
  // → { distance, normal, node } | null (sensors never block rays)
384
431
  ```
385
432
 
433
+ Both also answer two questions about a body that the node props cannot:
434
+
435
+ ```ts
436
+ physics.velocityOf(body); // px/s (2D, y-down) or m/s (3D) — from the SOLVER
437
+ physics.massOf(body); // what the solver settled on, collider-derived
438
+ ```
439
+
440
+ `linearVelocity` on the node is written back once per step, AFTER the solve — so
441
+ a behavior reading it from inside `fixedUpdate`, which is where a collision
442
+ response belongs, sees the value from BEFORE the impact. That is exactly the
443
+ moment a game wants to know how hard it hit something. Scale a push by `massOf`
444
+ rather than by the authored `mass` prop, which the solver may not be using.
445
+
446
+ `dir` may be ANY length — `target - eye` is the usual spelling and is metres
447
+ long. It is normalized for you, so `distance` and `maxLen` are always plain
448
+ metres (2D: pixels), never multiples of the vector you passed. A zero-length
449
+ direction points nowhere and returns `null`.
450
+
386
451
  Exclude the shooter's own body when casting from inside it — a ray that starts
387
452
  inside its own collider hits itself at distance 0, which reads as "blocked" and
388
453
  is the reason a vision cone can come back permanently blind.
389
454
 
390
- 3D also has a THICK ray — a sphere sweep for probes where skimming matters
391
- (the camera boom, ledge feelers):
455
+ Both also have a THICK ray — a sphere sweep in 3D, a circle sweep in 2D — for
456
+ probes where skimming matters (the camera boom, ledge feelers, a shot that must
457
+ not thread a one-pixel gap between two floor tiles):
392
458
 
393
459
  ```ts
394
460
  const hit = physics.castSphere(origin, dir, radius, maxLen, excludeBody?, { staticOnly?: true });
395
- // → { distance, node } | null (distance = travel of the sphere CENTER)
461
+ // → { distance, node } | null (distance = travel of the CENTRE)
396
462
  ```
397
463
 
398
464
  ## Gamepad
@@ -440,12 +506,15 @@ the tree says, without your scene falling over while you look at it. Use it for
440
506
  any "show me the collision geometry" tool. Do NOT use it to freeze a running
441
507
  game — that is `engine.timeScale = 0`, which keeps the world coherent.
442
508
 
443
- ## A walking body climbs a step (`stepHeight`, 3D)
509
+ ## A walking body climbs a step (`stepHeight`, 2D **and** 3D)
444
510
 
445
- `CharacterBody3D.stepHeight` (default `0.35` m, `0` = off) is how high a ledge
446
- the body walks UP without jumping. It is Rapier's autostep, and **Rapier does not
447
- autostep unless it is asked** before 0.63 nothing asked, so any walking body
448
- was stopped dead by a curb.
511
+ `CharacterBody3D.stepHeight` (default `0.35` m) and `CharacterBody2D.stepHeight`
512
+ (default `35` px the same 0.35 m at the 2D world's scale of 100 px = 1 m,
513
+ which is why gravity defaults to 980) are how high a ledge the body walks UP
514
+ without jumping. `0` = off. It is Rapier's autostep, and **Rapier does not
515
+ autostep unless it is asked** — before 0.63 nothing asked in 3D, and for longer
516
+ still nothing asked in 2D at all, where a lip ONE PIXEL high stopped a walking body
517
+ dead.
449
518
 
450
519
  You will not see this on the player: `CharacterController3D` rides a hover spring
451
520
  rather than the character controller, so it floats over small ledges already.
@@ -522,6 +591,16 @@ game.pick(x, y); // the node under that pixel, or null
522
591
  A stretched canvas is handled: `pointerPosition()` scales client → canvas pixels,
523
592
  so the coordinates are the ones `pick()` wants.
524
593
 
594
+ **The pointer has to be attached, and now attaches itself.** `createGame2D`/
595
+ `createGame3D` turn it on when the scene contains a `Clickable` — the same
596
+ `'auto'` shape `physics` has always had — and they ask again on every scene
597
+ change, because a title screen you click into a level has no `Clickable` in it
598
+ (a `UiButton` is DOM) and the level is full of them. Pass `pointer: true` explicitly when a
599
+ behaviour of YOUR OWN reads `pointerPosition()`; the boot cannot see that. A
600
+ `Clickable` that finds no pointer (or no picker) for two seconds says so by
601
+ name instead of sitting there inert, which is what it used to do: no error, a
602
+ clean `incanto-check`, a clean audit, and a board that did nothing when clicked.
603
+
525
604
  From scene JSON, the **`Clickable`** behavior needs no code at all:
526
605
 
527
606
  ```json
@@ -531,7 +610,7 @@ From scene JSON, the **`Clickable`** behavior needs no code at all:
531
610
  | prop | default | |
532
611
  | --- | --- | --- |
533
612
  | `button` | `0` | 0 left · 1 middle · 2 right |
534
- | `maxDistance` | `0` | ignore clicks further than this (0 = any) |
613
+ | `maxDistance` | `0` | ignore clicks further than this **from the current camera** (0 = any) |
535
614
  | `enabled` | `true` | stop responding without detaching |
536
615
 
537
616
  Signals: **`clicked(node)`**, **`hovered(node)`**, **`unhovered(node)`** — wire
@@ -550,11 +629,31 @@ ends elsewhere is not a click, the way every button on every platform behaves),
550
629
  and a hit on a CHILD counts as a hit on the node — the raycast lands on the
551
630
  visual mesh, which is usually a child.
552
631
 
632
+ **A finger that lifts stops hovering.** `hovered`/`unhovered` follow the cursor,
633
+ and a mouse cursor stays where you left it — but a finger ceases to exist, so a
634
+ touch release clears the pointer at the end of that frame (after the click has
635
+ fired, not with it). Without that the last-tapped node stayed `hovering: true`
636
+ for the rest of the session: a permanently highlighted tile, mole or card on
637
+ every phone. Do not build a hover-only affordance — on touch there is no hover
638
+ before the tap, only after it, and only for a frame.
639
+
640
+ **`visible: false` is not clickable.** A pixel the frame does not draw has
641
+ nothing under it, so hiding a node (or any ancestor of it) takes it out of the
642
+ pick — which is how you disable a button, a dialogue choice or a mole down its
643
+ hole without detaching anything. Before 0.71 three's raycaster reported hidden
644
+ geometry and `pick()` handed it straight to `Clickable`.
645
+
553
646
  ### Testing a mouse-driven game headlessly
554
647
 
555
- There is no renderer and therefore no raycast, so `engine.picker` is null and
556
- `Clickable` is inert. That is not a dead end: give the engine a picker of your
557
- own and drive the cursor.
648
+ **Use `runScript`'s `click` step** `{ atMs: 100, click: 'Board/Tile3' }` which
649
+ installs a geometric picker, points the cursor and spreads the press and release
650
+ across the two frames `Clickable` needs. `incanto-playtest` clicks too, and its
651
+ report says how many clicks it landed. See `incanto-verifying-your-game.md`.
652
+
653
+ The raw recipe below is what that does, and is still what you want when the
654
+ cursor has to be somewhere a node is not. There is no renderer and therefore no
655
+ raycast, so `engine.picker` is null and `Clickable` is inert. That is not a dead
656
+ end: give the engine a picker of your own and drive the cursor.
558
657
 
559
658
  ```ts
560
659
  // A picker that answers from the tree instead of a GPU raycast.
@@ -42,6 +42,24 @@ find a door, not enough to solve a puzzle.
42
42
  No renderer is involved, so twenty minutes of simulated play takes a second or
43
43
  two.
44
44
 
45
+ **Two buttons it will stop pressing.** The bot drives every declared button
46
+ action, then notices what a player would notice. If pressing one STOPS THE
47
+ CLOCK, that is a pause menu, and a run spent inside one measures nothing — it
48
+ lets go and never presses that button again. If pressing one replaces the scene
49
+ with THAT SAME SCENE, that is a restart, and it does not just waste the run, it
50
+ ends it: the bot is bound to the tree it started in. One run of the set pays for
51
+ that lesson and the rest play the game. Both are reported (`the bot stopped
52
+ pressing …`), both are OBSERVED rather than read off the words "pause" and
53
+ "restart" — your game may call them anything, or nothing — and neither is a
54
+ defect in your game.
55
+
56
+ The second one matters more than it sounds: mid-run retry is ordinary in puzzle,
57
+ racing and tower-defense games, and `GameFlow` restarts only from a terminal
58
+ state, so the games that reload mid-play are the ones that call `restartScene`
59
+ themselves. Every one of them used to report `8 runs played without reaching a
60
+ win (8 changed scene)` — and each run died a second in, which took the emitters,
61
+ the hazards and the destinations down with it.
62
+
45
63
  ## What it reports, and where each answer comes from
46
64
 
47
65
  | line | source |
@@ -51,12 +69,36 @@ two.
51
69
  | `fell` | the player left the world past `--fall-below` (default: 50 m under the spawn in 3D, 1000 px under it in 2D) |
52
70
  | `error` | `stats().errors` went above zero — a behavior threw |
53
71
  | `stuck` | the clock ran out AND the player never got more than 3 m (96 px) from its spawn — it is wedged, or nothing moves it |
54
- | `swapped` | the game changed scene. The bot binds player, destinations and the win/lose oracle ONCE, from the tree it started in, so nothing after a `goToScene` is measured — the run stops there and says so. It is not a defect, and the destinations it never got to are reported as unmeasured rather than missed. Judge a multi-scene game with a scripted `runScript`. |
72
+ | `swapped` | the game changed scene. The bot binds player, destinations and the win/lose oracle ONCE, from the tree it started in, so nothing after a `goToScene` is measured — the run stops there and says so. It is not a defect, and the destinations it never got to are reported as unmeasured rather than missed. Judge a multi-scene game with a scripted `runScript`. The bot's own KEYS are re-read on the swap: it used to hold the first scene's buttons into the second, and letting go of one the new scene never declared threw the whole playtest away. |
55
73
  | `unfinished` | the clock ran out on a player that was getting around. Not a defect: a win that needs a SEQUENCE (talk to the NPC, then fetch, then return) is out of reach of a random walker, forever |
56
74
  | `never reached` | destinations the bot never came within `--reach-radius` of (default 2 m in 3D, 32 px in 2D) |
57
75
  | `never fired` | signals a `connections[]` entry listens to that never happened |
76
+ | `declared and never read` | actions in the scene's `input{}` that NOTHING asked about, in any run — a key the control list promises and no code implements. Measured as a read, not a press, so the bot pressing it does not clear it. Two shipped starters had one: `tps-3d` declared `restart` (on `KeyR`, already the reload key) and `MISSION FAILED` was a dead end you left with F5. An action polled only on a screen no run reached lands here honestly — read it, do not gate on it |
58
77
  | `danger` | how many times the player's `Health` emitted `damaged` — and when that is ZERO, WHICH zero it is: a scene with no hazard wired at all is a walkabout and worth shouting about; a scene full of them that never landed one is a report about the BOT, which a real player would outrun less easily. Hazards are counted from the scene FILE, because a `Spawner` detaches its prefab at ready and a wave game's hazards are not in the tree yet. |
59
78
 
79
+ **The bot presses the HUD.** Every focusable widget a player can currently see —
80
+ `UiButton`, `UiToggle`, `UiSelect`, `UiSlider` — gets pressed occasionally,
81
+ through the same entry point Enter and the pad's A use. That is the whole
82
+ interface of a menu, a shop, a settings screen or an idle game, and it was on
83
+ none of the lists the bot searched: one built from the tarball reported
84
+ `nothing here is drivable … nothing to point at` and `✗ stuck in 2/2` about a
85
+ game that plays perfectly. A widget under a hidden panel is not pressed, because
86
+ a player could not press it either. `report.runs[].presses` counts them.
87
+
88
+ **The bot drags between SLOTS.** Every `draggable` widget gets picked up now and
89
+ then and dropped on a `dropTarget` that is not its own parent, through
90
+ `dropOnto` — the same entry point a harness uses, with the same rules as the
91
+ pointer path. An inventory is the one screen the HUD skill says a click cannot
92
+ express, and it was the one screen the bot could not touch: a potion bench whose
93
+ every verb is a drag reported `nothing here is drivable` and `✗ stuck in 6/6`,
94
+ and now reports `324 slot drag(s)` and which of its wires never fired.
95
+ `report.runs[].widgetDrags` counts them.
96
+
97
+ **The line names every channel it used**, not the first one that fired: a game
98
+ played by dragging AND by keys reports both counts, because an either/or ladder
99
+ printed the drags and said nothing about three hundred key presses on the same
100
+ run.
101
+
60
102
  **The bot can use the MOUSE.** If your scene has `Clickable` nodes, it presses
61
103
  and releases over them at random — through the real `Clickable`, the real
62
104
  `mouseJustPressed` edge and your real connection, with a picker that answers
@@ -70,6 +112,12 @@ A random clicker still cannot finish a game that needs a SEQUENCE — select a
70
112
  trooper, then a tile, then end the turn — and that is reported as unmeasured,
71
113
  not failed.
72
114
 
115
+ **A wire on a HUD widget is visible now too.** The whole screen-space subtree was
116
+ skipped when the run collected signals — right for "never reached", which is
117
+ about walking, and it took every `pressed`, `changed` and `dropped` wire with
118
+ it, so the line that exists to say "this wire does nothing" could never say it
119
+ about a HUD.
120
+
73
121
  **Nothing here is a guess about your design.** The scene already declares what
74
122
  winning is; this only listens.
75
123
 
@@ -102,10 +150,54 @@ geometry says you cannot. Check the jump height, the gap, the collider.
102
150
  **`never fired`** means you drew a wire that does nothing. Either the signal is
103
151
  unreachable or the connection is wrong.
104
152
 
153
+ **`silent`** names a declared emitter — an `AudioPlayer`, a particle node — that
154
+ never went off in any run. Emitters on a SPAWNER's prefab count: the template is
155
+ lifted out of the tree at load, so the check reads the scene FILE as well as the
156
+ loaded tree, and a wave of forty enemies is reported as its one authored
157
+ emitter, not forty.
158
+
159
+ **`made and still held`** counts the clones alive when the run ended, grouped by
160
+ the TEMPLATE they came from, with what the count was at the halfway mark:
161
+
162
+ ```
163
+ · made and still held: 12 × /Game/Prefabs/Bolt (3 at halfway),
164
+ 6 × /Game/Prefabs/Tower (3 at halfway)
165
+ ```
166
+
167
+ Six towers from three is a player who kept building; twelve projectiles from
168
+ three is a projectile that never learned to stop. **Furniture holds steady; a
169
+ leak climbs** — and nothing in the engine can tell them apart, so both are
170
+ printed and the judgement is yours. It exists because a shipped tower defense
171
+ kept every bolt that MISSED alive forever (its `Lifetime` was asleep on the
172
+ prefab shelf) and no instrument said a word: the playtest was green, the audit
173
+ was green, the harness was green, every rung of the ladder was green.
174
+
105
175
  **`nothing here can hurt you`** is a design report, not an error. Sometimes it
106
176
  is exactly right (a walking simulator). Usually it means the hazards are not
107
177
  wired up.
108
178
 
179
+ **`!` lines** are what the ENGINE warned about during the runs — a camera whose
180
+ `follow` resolves to a node with no position, a `DayNight` with no sky, a body
181
+ with no collider. They are not failures and they do not change the exit code;
182
+ they are the things that leave a game running and wrong, and they are usually
183
+ the first thing to read. One line per distinct warning, however many times it
184
+ fired.
185
+
186
+ **`error`** prints the throw under it — the behavior, the node and the
187
+ exception, in the engine's own words:
188
+
189
+ ```
190
+ ✗ error in 8/8
191
+ behavior 'Boom' on /Game/Player/Sword (Node2D) threw in update — THE
192
+ SCRIPT is now skipped … Error: the sword script is broken
193
+ ```
194
+
195
+ A script that throws in `update` is quarantined (the node keeps running, its
196
+ script does not), so everything downstream of it simply stops happening and
197
+ every other line of the report describes the silence rather than the cause.
198
+ Read this one first. `incanto-verify` carries the same lines under its `plays`
199
+ rung.
200
+
109
201
  **`stuck` in every run** means the player is not going anywhere: wedged in
110
202
  geometry, spawned inside a collider, or missing the input map that moves it.
111
203
  That is a real bug and worth chasing.
@@ -156,6 +248,18 @@ real thing:
156
248
  bunx incanto-playtest src/game.scene.json --behaviors src/behaviors.ts
157
249
  ```
158
250
 
251
+ **More than one file.** Behaviours split as a game grows — bosses in `boss.ts`,
252
+ the scene table in `scenes.ts` — so comma-separate them or repeat the flag,
253
+ rather than writing a barrel that re-exports everything for the CLI's benefit:
254
+
255
+ ```bash
256
+ bunx incanto-playtest src/game.scene.json --behaviors src/behaviors.ts,src/boss.ts
257
+ bunx incanto-verify src/game.scene.json --behaviors src/behaviors.ts --behaviors src/boss.ts
258
+ ```
259
+
260
+ Every bin that takes `--behaviors` accepts both forms: `check`, `playtest`,
261
+ `verify`, `feel`, `play`, `multiplay`.
262
+
159
263
  ## From TypeScript
160
264
 
161
265
  ```ts
@@ -166,7 +270,13 @@ console.log(playtestText(report));
166
270
  ```
167
271
 
168
272
  `report.runs` carries per-run outcome, timing, reached paths, fired signals,
169
- damage and the replay.
273
+ damage and the replay. `report.inertActions` is the dead-control list above.
274
+
275
+ The same question, without a playtest — after any run, scripted or hand-driven:
276
+
277
+ ```ts
278
+ game.engine.input.unreadActions(); // ['restart'] — declared, nothing read it
279
+ ```
170
280
 
171
281
  ## Pairs with the feel report
172
282