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
@@ -81,7 +81,7 @@ All 3D nodes extend `Node3D` and therefore have the transform props:
81
81
  ### `MeshInstance3D`
82
82
  | Prop | Default | Notes |
83
83
  |---|---|---|
84
- | `mesh` | `"box"` | `box \| sphere \| capsule \| plane \| cylinder \| gem` — `gem` is a faceted crystal (icosahedron); with `material.flatShading: true` + low `roughness` it reads as a sparkling jewel (spin/tumble it for the sparkle) |
84
+ | `mesh` | `"box"` | `box \| sphere \| capsule \| plane \| cylinder \| gem` — `gem` is a faceted crystal (icosahedron); with `material.flatShading: true` + low `roughness` it reads as a sparkling jewel (spin/tumble it for the sparkle). **This list is the whole list**: for a CURVED hull — a car body, a boat, a canopy, a fuselage — there is no primitive, and stacking boxes never gives you one. Use `LoftMesh3D` (below) |
85
85
  | `size` | `[1,1,1]` | box: extents · sphere/gem: radius = x · capsule: radius = x, height = y · plane: x·z ground (laid FLAT on XZ) · cylinder: radius = x, height = y |
86
86
  | `material` | `{}` | `{color: '#hex', metalness: 0..1, roughness: 0..1, opacity: 0..1, clearcoat: 0..1, clearcoatRoughness: 0..1, envMapIntensity: >=0, wireframe, flatShading, depthTest, depthWrite, emissive, emissiveIntensity, map, normalMap, repeat: [u,v]}` — the object IS the material state: omitted keys reset to defaults (`#ffffff`, 0, 1, 1, false, false, depthTest/Write true). `opacity < 1` turns on transparency; `flatShading` gives faceted per-face glints (the gem sparkle). `depthTest: false` (+ a high `renderOrder`) makes a flat `plane`/`cylinder` an ALWAYS-ON-TOP ground decal — AoE telegraphs, selection/spawn rings — that never z-fights with or is hidden by bumpy terrain (`depthWrite: false` keeps it from occluding later effects). `map`/`normalMap` are texture URLs (lazy-loaded, headless-safe; map samples sRGB, normalMap linear), tiled `repeat` times across each face — box/plane UVs span 0..1 per face, so for worldspace density use `repeat: [length/tile, height/tile]` (e.g. a 6×2.5 m brick wall at one tile per 2 m → `[3, 1.25]`). `color` tints the map (near-white keeps the texture's own color). CAR PAINT: `clearcoat: 1` adds a glossy lacquer layer with its own reflection over a metallic base (`clearcoatRoughness` ~0.05-0.1 = mirror lacquer) — THE premium vehicle-paint look; `envMapIntensity` scales how strongly the surface mirrors the scene's sky/HDRI (glass windows want ~2, matte surfaces <1; needs a `sky`/`hdri` environment to reflect — at night with no sky there is nothing to mirror). NB: under ACES a very bright `emissiveIntensity` blows out to white — keep it modest (~1–2) + a saturated color for a vivid GLOWING look. Unknown keys, malformed `repeat`, or `repeat` without a map hard-fail at load |
87
87
  | `castShadow` / `receiveShadow` | `false` | |
@@ -270,10 +270,43 @@ and `Fill.position = [-(1 - frac) / 2, 0, 0.01]`. The offsets stay local to the
270
270
  (rotated) bar, so the drain always hugs the bar's left edge.
271
271
 
272
272
  ### `Camera3D`
273
- `fov: 60`, `near: 0.1`, `far: 1000`, `current: false`.
273
+ `fov: 60`, `near: 0.1`, `far: 1000`, `current: false`, `lookAt: ""`.
274
274
  Mark exactly ONE camera `current: true` (otherwise the first camera in tree order is used).
275
- Aspect ratio is automatic. To frame an origin-centered scene, a good default is
276
- `position: [7, 6, 9]`, `rotation: [-26, 36, 0]`.
275
+ Aspect ratio is automatic.
276
+
277
+ **Point it with `lookAt`, not with a `rotation` you worked out.**
278
+
279
+ ```json
280
+ { "name": "Cam", "type": "Camera3D",
281
+ "props": { "current": true, "position": [7, 6, 9], "lookAt": "%Player" } }
282
+ ```
283
+
284
+ `lookAt` is a node path, re-aimed every frame, so it also tracks a target that
285
+ moves. It orients with WORLD UP, which is the part you cannot write yourself in
286
+ scene JSON — and cannot write in game code either without importing three, which
287
+ this engine's own guardrail forbids ("mutate node props — never touch three.js
288
+ objects").
289
+
290
+ This is not a convenience. A hand-written `rotation` for a camera is
291
+ **wrong at every heading but one**, and the way it is wrong is invisible from
292
+ inside the frame:
293
+
294
+ - The one published direction→rotation formula (`incanto-3d-character.md`, "the
295
+ +Z-FORWARD rule", *"There is ONE facing formula"*) is derived for a SKIN — a rod,
296
+ which has no meaningful up. Measured over 72 look headings, applying it to a
297
+ camera aims perfectly (dot 1.000 in all 72) and puts the world **upside down in
298
+ 36 of them**. Its own published check, `rodAxis · (target − eye) ≈ 1`, scores
299
+ 1.000 on every upside-down one.
300
+ - **No two-angle recipe can be right.** `rotation` is Euler XYZ, so
301
+ `[pitch, yaw, 0]` is Rx·Ry while a level camera is Ry·Rx: an upright camera's
302
+ third angle is NON-ZERO. The `[-26, 36, 0]` this document recommended for years
303
+ aims at 0.9924 and tilts the horizon **16°**; the level answer is
304
+ `[-33.69, 32.91, 19.91]`, which nobody would write by hand.
305
+ - A camera looks down its own local **−Z**, unlike every skin in the engine.
306
+
307
+ `framing()` now prints `up [x, y, z]` and the roll in degrees, so a tilted or
308
+ inverted camera is something you can SEE in a report rather than something you
309
+ notice in a screenshot.
277
310
 
278
311
  ### Lights
279
312
  - `DirectionalLight3D` — `color: '#ffffff'`, `intensity: 1`, `castShadow: false`.
@@ -524,6 +557,13 @@ for (const o of off) console.error(` ${o.path}: ${o.gap > 0 ? 'floats' : 'burie
524
557
  Put it in your `verify.ts` next to `auditScene`. It costs nothing and it is the
525
558
  difference between shipping a floating bridge and not.
526
559
 
560
+ **Mark what is not meant to touch the ground.** `snapToGround: false` is the
561
+ escape and it INHERITS, so one mark exempts a whole assembly: a cloud made of
562
+ puffs, a palm made of trunk segments, a lamp at the top of a lighthouse, a rock
563
+ sunk into the slope on purpose. Put it on the group, not on its parts — before
564
+ 0.73 it read the node's own prop only, and this repo's own flagship template
565
+ reported 64 floating cloud puffs.
566
+
527
567
  ## The loading screen has to cover the WHOLE boot
528
568
 
529
569
  Downloading the assets is the first half. Building a dressed scene and
@@ -569,6 +609,20 @@ previous explosion across the level) as
569
609
  point sprites in meters: preset distances auto-scale ÷100 (presets are authored
570
610
  in 2D px). See incanto-building-2d-games for the full prop list.
571
611
 
612
+ **Firing one from a behaviour is `replay()`** — move the emitter, then replay:
613
+
614
+ ```ts
615
+ const dust = this.node.getNode('Dust') as Particles3D;
616
+ dust.position = [x, 0.2, z];
617
+ dust.replay(); // `emitting: false` stays false; this is the game asking
618
+ ```
619
+
620
+ Not `play`, not `burst`, not `emit` (that one belongs to signals). The 2D skill
621
+ has always named it and this one did not, so a 3D game built from this page
622
+ alone had a one-shot it could not fire. `particles()` is the same node's test
623
+ hook — the live particle list, for a harness asking whether anything came out.
624
+ `engine.effects.countOf('explosion')` is the cheaper question.
625
+
572
626
  Points render through a built-in soft sprite with a SOLID opaque core (not hard
573
627
  squares), so colours read even at small sizes, and `'normal'` blend fades cleanly
574
628
  to transparent (per-particle RGBA alpha). `paletteColors` (per-particle
@@ -18,8 +18,20 @@ bunx incanto-editor level.scene.json --output build/level.scene.json --port 5179
18
18
 
19
19
  **Project mode** (no file argument — the usual way) scans the current directory for
20
20
  `*.scene.json` (skipping node_modules/dist/hidden dirs): one scene auto-opens; several
21
- open the scene browser. The page can only ever address `*.scene.json` files under the
22
- launch directory.
21
+ open the scene browser.
22
+
23
+ **What the server will and will not touch**, since the browser lists every
24
+ `.json` and the answer comes from the server:
25
+
26
+ | | |
27
+ | --- | --- |
28
+ | reads | any `.json` **under the launch directory** — but only sends it if it IS a scene; anything else comes back with the reason and its contents never leave |
29
+ | writes | a scene, over a file that is already a scene, or to a file that does not exist yet |
30
+ | refuses | a path outside the project, a non-scene body, and **overwriting a file that is not a scene** — `?file=package.json` gets a 409 naming what it found |
31
+
32
+ That last row used to be missing: the write path checked the body and never what
33
+ it was about to land on, so one query parameter could replace a manifest with a
34
+ scene. The GET on the same endpoint already refused it.
23
35
 
24
36
  ### The scene browser (`scenes`)
25
37
 
@@ -108,7 +120,13 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
108
120
  changed. A prop is rewritten only if its type declares `nodePath: true`, so a
109
121
  text prop that merely reads "Player" is never touched. `+` adds a child of any registered
110
122
  type, `✕` deletes the selection. Selecting the ⚙ scene row edits the header
111
- (dimension, gravity with real inputs, environment/input/assets/multiplayer/strings).
123
+ (dimension, gravity with real inputs, environment/input/assets/multiplayer),
124
+ including a **STRINGS table** — one row per `@t:` key, one column per
125
+ language. It shows the two things raw JSON cannot: an untranslated cell shows
126
+ the English it will silently fall back to (greyed, because that fallback is
127
+ the design), and a key the BASE locale is missing is marked in red, because
128
+ that one has nothing to fall back to and paints the raw key on screen.
129
+ `+ key`, `+ language`, and deleting a key that a prop still says asks first.
112
130
  - **CONNECTIONS on the selected node** — the inspector's last section shows what
113
131
  the node **emits** and what it **receives**, each row being signal → target →
114
132
  handler with a ✕ to remove and `+ connect` to add. Signals come from the node
@@ -116,8 +134,12 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
116
134
  node in the scene; the handler is a dropdown of real methods when the target
117
135
  has no script, and free text when it does (those handlers live in your
118
136
  TypeScript, which the editor never loads). An end that no longer resolves is
119
- marked red with the reason. For what a row cannot express — `filter`, `once`,
120
- bound args — the scene header still has `connections` as raw JSON.
137
+ marked red with the reason. For what a row cannot express — `filter` and `once`
138
+ — the scene header still has `connections` as raw JSON. (It used to say "bound
139
+ args" as well: a connection has never had them. Its keys are exactly
140
+ `[signal, from, to, handler, once, filter]`, and since they became a closed
141
+ set an invented one is a load error rather than a key that quietly does
142
+ nothing.)
121
143
  **Validation now runs WITH connections**: the editor used to strip them before
122
144
  loading, so it reported green for a scene the game hard-fails to open.
123
145
  - **Viewport with DIRECT MANIPULATION** — a mode toolbar (top left) + the
@@ -167,8 +189,11 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
167
189
  `invulnerableFor`/`freeOnDeath` with their defaults; a `Patrol`'s `mode` is a
168
190
  `loop`/`pingpong` menu), delta-only like every other field. A name that is NOT
169
191
  built in is your game's TypeScript: it keeps the raw-JSON props box and says
170
- so, with copy-paste Behavior boilerplate in its help — and `groups` (tag
171
- chips). Every one has a `?` help popover with examples. Values equal to the
192
+ so, with copy-paste Behavior boilerplate in its help — `groups` (name chips)
193
+ and `tags` (the node's free-form identity JSON: what
194
+ `connections[].filter.tag` matches on, and where a chart, a card or a loot
195
+ entry lives; a value the loader would reject is marked invalid rather than
196
+ written). Every one has a `?` help popover with examples. Values equal to the
172
197
  default are removed (delta-only, like the serializer).
173
198
  **A prop that holds a NODE PATH** (`Chase.target`, `Camera2D.follow`,
174
199
  `Spawner.prefab`, `Joint3D.target`, `skinPath`, `terrain`…) offers every node
@@ -358,6 +383,20 @@ editing wherever the simulation happened to be. Games hosting the editor
358
383
  themselves (`createGame3D({ editor: … })`) get a **save** button only when they
359
384
  pass `editor.save` — otherwise edits live as long as the session does.
360
385
 
386
+ **The handle you booted with does not survive the round trip.** ▶ builds a NEW
387
+ game, so after one ✎→▶ the `game` your app is holding refers to something that
388
+ no longer exists. It is inert rather than destructive — a stale `dispose()` and
389
+ a stale `openEditor()` both do nothing — but if your app keeps using the handle,
390
+ take the live one:
391
+
392
+ ```ts
393
+ let game = await createGame3D({
394
+ scene, canvas,
395
+ debug: DEBUG,
396
+ editor: DEBUG && { library: true, onReboot: (next) => { game = next; } },
397
+ });
398
+ ```
399
+
361
400
  ## Animating a model from the inspector
362
401
 
363
402
  Select a `ModelInstance3D` and everything about what it is DOING sits in one
@@ -201,6 +201,24 @@ template). Options: `seed`, `size` (32 blocks), `height` (8), `water`. The
201
201
  voxel `generateIsland` (radial dome falloff, sandy shore) also remains for
202
202
  the same use case.
203
203
 
204
+ **Digging and building** is the node's own API, and for a long time none of it
205
+ was written down here — the whole point of a voxel world is that it changes:
206
+
207
+ ```ts
208
+ const world = this.node.getNode('World') as VoxelGrid3D;
209
+ world.tileAt(x, y, z); // the tile there, or undefined
210
+ world.removeBlockAt(x, y, z); // dig — true if there was one
211
+ world.addBlock({ x, y, z, tile: 3 });
212
+ world.setBlocks(blocks); // replace the lot (emits `blocksChanged`)
213
+ world.blockCount(); // how many are standing
214
+ world.blocks(); // every one of them, to save or inspect
215
+ ```
216
+
217
+ Grid coordinates, not world ones — multiply by the node's `blockSize` to place
218
+ something on a block. `TileMap2D` answers the same questions in two dimensions
219
+ (`cellAt`/`tileAt`/`worldAt`/`setTile`), and that one takes WORLD coordinates
220
+ because a flat world has a cursor in it.
221
+
204
222
  ## Library
205
223
 
206
224
  ```ts
@@ -609,7 +627,7 @@ is radians of `atan(|∇h|)`. Default textures stream from the live agent8 CDN
609
627
  | `theme` | `"island"` | see table — `custom` requires `layers` |
610
628
  | `roughness` | `0.5` | detail-octave persistence (higher = more rugged) |
611
629
  | `detail` | `4` | detail octave count |
612
- | `flatThreshold` | `0.95` | snap-flatten heights within ±0.6 m of `maxHeight·flatThreshold` |
630
+ | `flatThreshold` | `0.95` | snap-flatten heights within ±0.6 m of `maxHeight·flatThreshold` — a fraction of the SCALE, not of the peak, so the default plateau lands around 18% of the realized height (measured: maxHeight 28 → band at 26.6, peak 150). Read `heightAt` before choosing one |
613
631
  | `islandEdge` | `null` | `null` = theme decides; force the rim wrap on/off with a boolean |
614
632
  | `layers` | `[]` | custom splat layers (theme `custom` only) |
615
633
  | `textureBase` | agent8 CDN | base URL: `<base>/<name>.png` + `<base>/<name>_normal.png` |
@@ -14,6 +14,7 @@ bunx incanto-feel src/game.scene.json
14
14
  ```
15
15
  feel of /Game/Player
16
16
 
17
+ pushed 'move' [1, 0]
17
18
  top speed 5.62 u/s
18
19
  to full speed 383 ms
19
20
  to a stop 133 ms
@@ -28,12 +29,58 @@ feel of /Game/Player
28
29
  jump buffer 150 ms
29
30
  ```
30
31
 
32
+ A `✗` or `!` line above the numbers is what the ENGINE said while they were
33
+ being taken — a script that threw and was quarantined, a camera following a node
34
+ with no position. The measurements below it are still real (a behaviour is
35
+ quarantined, the node is not), but a page of confident numbers taken while a
36
+ script was dying reads as a healthy character, so read those lines first.
37
+
31
38
  ## Why this exists
32
39
 
40
+ **`top speed` is what the character SUSTAINS, not its fastest frame.** The probe
41
+ holds `move` for five seconds and takes the 90th percentile of the run, because
42
+ the maximum belongs to whatever hit the character last. Measured on the shipped
43
+ `platformer-2d`, whose `RUN_SPEED` is 250: it reported `top speed 300 u/s` —
44
+ that is `KNOCK_X`, from an enemy the probe walked into — and `to full speed
45
+ 2367 ms`, which was the walk to the enemy, on a character that reaches its speed
46
+ in ONE frame. Four numbers, all about a character being attacked. When the peak
47
+ and the sustained speed disagree the report says so:
48
+
49
+ ```
50
+ top speed 250 u/s
51
+ to full speed 17 ms
52
+ peak 300 u/s ← SOMETHING ELSE moved it
53
+ ```
54
+
55
+ Measure feel somewhere nothing can hit you.
56
+
57
+ **A character that jumps in MID-AIR has no coyote window to measure.** The probe
58
+ lifts the body, waits, presses, and reads "vertical velocity went up" as a jump —
59
+ so a double jump answers yes at any delay, and so does any behaviour that pushes
60
+ the body up while `jump` is held (a glide, a jetpack, an updraft). It used to
61
+ print `coyote window always (no window)`, which reads as the opposite of the
62
+ truth, on two of the six shipped starters — one of which declares
63
+ `coyoteSeconds: 0.12` five lines from its glide. It says
64
+
65
+ ```
66
+ coyote window not measurable — this character jumps in MID-AIR
67
+ jump buffer 33 ms (suspect — see above)
68
+ ```
69
+
70
+ and names the declared value it cannot confirm. To measure the window, probe a
71
+ build with the air jump off.
72
+
33
73
  **`jumpVelocity: 4` is not a height.** It is an impulse, and what it produces
34
74
  depends on gravity, mass, drag and `fallGravity`. The apex in world units and the
35
75
  hang time in milliseconds are what you are actually choosing.
36
76
 
77
+ **It says what it pushed.** The `pushed` line names the action AND the direction
78
+ that produced every number under it. The probe used to hold `(1, 0)` and only
79
+ `(1, 0)`, so a game whose movement reads the OTHER axis of the same action —
80
+ `-v.y`, which is this engine's own documented forward, `getVector` being y-down —
81
+ measured nothing and printed the nothing as `top speed 0 u/s` with four blank
82
+ rows and exit 0. A racer that its own harness clocked at 41 m/s reported 0.
83
+
37
84
  **The windows are MEASURED, not read.** `coyoteMs` comes from probing — lift the
38
85
  character clear of its support, wait, press, see whether it launched, binary
39
86
  search the boundary. A `coyoteSeconds: 0.12` that the controller never consults
@@ -52,6 +99,29 @@ reports **0 ms**. That was a real bug in this engine's own 3D controller until
52
99
  | `coyote window` | **0 reads to players as "the controls are broken"**, not as a missing feature. |
53
100
  | `jump buffer` | 0 throws away a press made a few frames before landing. |
54
101
 
102
+ ## When it cannot move your player
103
+
104
+ If the default push finds no motion, the tool SEARCHES: every declared `vector2`
105
+ action, each pushed ±x and ±y, and it keeps whichever moved the body furthest.
106
+ That is a fallback, never a replacement — a scene that already answered cannot
107
+ have its numbers changed by it. Two real games it rescues: a throttle on `-v.y`,
108
+ and a game where an unrelated `aim` happened to be declared FIRST in the input
109
+ map (the default move action is "the first `vector2`").
110
+
111
+ When nothing moves it says so instead of printing a zero:
112
+
113
+ ```
114
+ pushed 'move', 'aim' every way (±x, ±y) — the player did not move
115
+
116
+ could not move /Game/Turret: no motion in 300 frames with 'move', 'aim' pushed
117
+ ±x and ±y. Either the movement is not wired to a vector action, or it is
118
+ driven by buttons. Nothing below was measured.
119
+ ```
120
+
121
+ `--move <action>` pins the action explicitly, and pinning it turns the search
122
+ OFF — you asked about that action, so a silent answer about a different one
123
+ would be worse than no answer.
124
+
55
125
  ## It reports, it does not grade
56
126
 
57
127
  There are no PASS/FAIL bands here on purpose. A twitch platformer and a survival
@@ -71,12 +71,13 @@ warn: World/Enemy: Chase.target — '%Playerr' matches no node in this scene.
71
71
 
72
72
  — which is the one failure that used to be completely silent (a prop path
73
73
  resolves with `getNodeOrNull`, so the scene opens and the enemy just never
74
- chases). That is for a behaviour the ENGINE ships: `incanto-check` reads scene
75
- files and cannot load your TypeScript, so a `nodePath: true` prop on a
76
- behaviour of YOUR OWN is invisible to it. `auditScene` from `incanto/test`
77
- gives the identical sentence once your behaviours are registered — which is why
78
- every scaffolded `verify.ts` calls it before `runScript`, and why a
79
- hand-written harness that skips it loses this check.
74
+ chases). It works for a behaviour of YOUR OWN too, given the file that registers it:
75
+ `incanto-check src --behaviors src/behaviors.ts` prints the identical sentence
76
+ for `Archer.arrowTemplate`. Without the flag the checker has no schema for your
77
+ behaviours and their node-path props are invisible to it — which is why every
78
+ scaffolded project's `check` script passes it, and why a hand-written one that
79
+ does not loses the check silently. `auditScene` from `incanto/test` is the same
80
+ audit from inside your harness, once your behaviours are registered.
80
81
 
81
82
  `Chase.target` and `FollowCamera.target` are `required`: an EMPTY one
82
83
  is a load error naming the node, not a surprise in the browser.
@@ -106,6 +107,7 @@ Hit points with regen and post-hit invulnerability (i-frames). The universal
106
107
  | `regenPerSec` | `0` | HP regained per second (0 = off) |
107
108
  | `invulnerableFor` | `0` | seconds of immunity after a hit (i-frames; 0 = off) |
108
109
  | `freeOnDeath` | `false` | `queueFree()` this node when it dies (clone-safe) |
110
+ | `freeParent` | `false` | with `freeOnDeath`, free the PARENT — the enemy, when `Health` rides a child node |
109
111
 
110
112
  Signals: `damaged(amount, current)` · `healed(amount, current)` ·
111
113
  `healthChanged(current, max)` · `died` · `revived(current)`
@@ -150,6 +152,13 @@ still while the character heals. Use `damaged`/`healed` for reacting to the HIT
150
152
  > node-local, so it clones perfectly and frees the whole enemy with no wiring.
151
153
  > (Connections are still the right tool for NON-cloned nodes, e.g. the player's
152
154
  > `died → ScoreKeeper.loseLife`.)
155
+ >
156
+ > A node carries ONE script, so if the root is already taken — a `Patrol` you
157
+ > did not give `moveParent`, an `Area2D` with a `Clickable` — put `Health` on a
158
+ > child and add **`freeParent: true`**. Without it the child frees ITSELF and
159
+ > the body walks on with no health at all: an immortal enemy that takes damage
160
+ > from nothing and reports nothing. Same prop, same reason, as `Lifetime` and
161
+ > `DamageOnContact`.
153
162
 
154
163
  ## Lifetime
155
164
 
@@ -182,7 +191,10 @@ once; reaching 0 lives emits `lost` once.
182
191
  | `lives` | `0` | starting lives (0 = lives disabled) |
183
192
  | `scoreToWin` | `0` | score that triggers `won` (0 = disabled) |
184
193
 
185
- Signals: `scoreChanged(score)` · `won` · `lost` · `lifeLost(lives)`
194
+ Signals: `scoreChanged(score)` · `won` · `lost` · `lifeLost(lives)` ·
195
+ `livesChanged(lives)` — the EVENT and the COUNT: `lifeLost` fires when a life is
196
+ spent (flash, respawn), `livesChanged` also fires when a save is restored, so a
197
+ lives counter belongs on that one.
186
198
  Methods: `addScore(n)` · `setScore(n)` · `loseLife()`
187
199
 
188
200
  ```json
@@ -384,7 +396,7 @@ author markers). Emits `reachedPoint(index)` on arrival.
384
396
 
385
397
  | Prop | Default | Meaning |
386
398
  |---|---|---|
387
- | `points` | `[]` | waypoints: position arrays or node-path strings (required) |
399
+ | `points` | `[]` | waypoints: position arrays (LOCAL to the moved node) or node-path strings (that marker's WORLD position) — required |
388
400
  | `speed` | `60` | units per second along the path |
389
401
  | `loop` | `true` | wrap to the first point after the last (loop mode) |
390
402
  | `mode` | `"loop"` | `'loop'` wraps · `'pingpong'` reverses at the ends |
@@ -395,6 +407,18 @@ author markers). Emits `reachedPoint(index)` on arrival.
395
407
 
396
408
  Signals: `reachedPoint(index)`
397
409
 
410
+ **The two waypoint forms are in different frames, on purpose.** An array is a
411
+ LOCAL offset — the only thing numbers typed into a prop can mean. A node path
412
+ means *where that marker actually is*, so markers under `/root/Path` and
413
+ walkers under a spawn container work, which is how a lane is organised. Before
414
+ 0.71 a node path was read as a local offset too, so a walker whose parent was
415
+ not at the markers' origin walked a shifted copy of the path in silence.
416
+
417
+ Need to CHANGE the route at runtime rather than author it? That is
418
+ `PathFollow` and its `setPath()` (see *Pathfinding*, below) — `Patrol.points` is read live
419
+ but the index and direction are not re-derived, so writing points mid-route
420
+ moves the corners under a walker that is still heading for the old one.
421
+
398
422
  ```json
399
423
  { "name": "Guard", "type": "Node2D",
400
424
  "script": { "name": "Patrol", "props": { "points": [[100, 0], [300, 0]], "speed": 80, "mode": "pingpong" } } }
@@ -576,6 +600,13 @@ placeTower(pad);
576
600
  "you cannot afford that" is one wire to a banner. It saves through `serialize()`
577
601
  like `Health` and `ScoreKeeper`.
578
602
 
603
+ A NON-POSITIVE amount is ignored, the way `Health.damage`/`heal` have always
604
+ ignored one — and a negative is reported, because it is a sign flip in the
605
+ caller rather than an offer. `spend(-5)` used to PAY you five (any negative
606
+ passes the affordability test, and the subtraction then added) and `earn(-5)`
607
+ to take five, going round `canAfford` and `refused` entirely. `spend(0)` returns
608
+ `true`: you can always afford nothing.
609
+
579
610
  ## Wander
580
611
 
581
612
  Seeded random roaming inside a circle around the spawn point — idle critters,
@@ -665,6 +696,15 @@ are NOT cloned):
665
696
  - **(B) scene `connection`** (fine for a single authored entity, NOT for clones):
666
697
  `{ "signal": "movementStateChanged", "from": "Enemy/AI", "to": "Enemy/Skin", "handler": "setClip" }`.
667
698
 
699
+ **Subscribing in `onReady` is safe to do to anything, including a node that
700
+ outlives you.** A connection made there is owned by the node making it, so
701
+ `free()` takes it with the node — you do not have to keep the returned
702
+ disconnect and call it yourself. That matters most for the shape this pattern
703
+ invites: every enemy subscribing to the score keeper or the player, where the
704
+ emitter outlives thousands of listeners. (Wiring a signal from a `update()` or
705
+ from outside a lifecycle method still hands ownership to you — keep the
706
+ disconnect, or pass `{ owner: someNode }`.)
707
+
668
708
  Same `ZombieSkin` is the place to also FACE the heading and GROUND the body each frame
669
709
  (see incanto-3d-character.md "+Z-FORWARD rule" and incanto-3d-models.md "Grounding").
670
710
 
@@ -722,7 +762,7 @@ all a raft, a buoy, a crate or a body needs to bob, settle and ride the swell.
722
762
 
723
763
  | Prop | Default | Meaning |
724
764
  |---|---|---|
725
- | `water` | `""` | Water3D node path; empty = the first Water3D in the tree (a wrong path fails hard) |
765
+ | `water` | `""` | Water3D node path; empty = the first Water3D in the tree (a wrong path fails hard; NO water in the scene is an `incanto-check` warning) |
726
766
  | `draft` | `0.35` | where the resting WATERLINE sits below the body origin, in meters — the hull floats with its origin that far above the water |
727
767
  | `stiffness` | `3` | how stiffly it returns to that waterline (extra g of lift per draft of submersion): 1 wallows, 3 is a firm dinghy, 8 is a cork |
728
768
  | `damping` | `3.5` | vertical damping (1/s) — how fast the bobbing settles |
@@ -763,6 +803,44 @@ a `draft` that puts the waterline on it:
763
803
  For the water's own height anywhere (jetties, spawn heights, an AI deciding
764
804
  where the shore is), `Water3D.heightAt(x, z)` answers the same surface.
765
805
 
806
+ ## PrefabShelf
807
+
808
+ The shelf of templates the GAME clones itself — a tower the player places, a
809
+ bolt that tower fires, the `+14` a kill pays. `Spawner` owns its one prefab and
810
+ detaches it; this owns a whole shelf of them and detaches all of them.
811
+
812
+ ```json
813
+ { "name": "Prefabs", "type": "Node2D", "script": { "name": "PrefabShelf" },
814
+ "children": [
815
+ { "name": "Tower", "type": "Node2D", "children": [] },
816
+ { "name": "Bolt", "type": "Node2D", "children": [] }
817
+ ] }
818
+ ```
819
+ ```ts
820
+ const shelf = this.node.getNode('/Game/Prefabs').behavior as PrefabShelf;
821
+ const tower = shelf.make('Tower'); // detached, awake all the way down
822
+ tower.position = at; // set it up BEFORE it readies
823
+ this.node.getNode('/Game/Towers').addChild(tower);
824
+ ```
825
+
826
+ | Prop | Default | Meaning |
827
+ |---|---|---|
828
+ | `prefabs` | `""` | comma-separated child names to take; empty takes them ALL |
829
+
830
+ Methods: `make(name)` · `names()`
831
+
832
+ - **Its children never enter the tree.** Claimed in `onEnterTree` (parent-first,
833
+ so a template carrying its own shelf is taken intact), so nothing has to be
834
+ `visible: false` and nothing has to be `"enabled": false`. A template left in
835
+ the tree is a live node: its behaviors tick, and its `autoplay` audio plays.
836
+ - **`make()` returns the clone DETACHED**, like `duplicateNode` — `onReady`
837
+ fires on attach, and a behavior that banks its node's position there
838
+ (`FloatAway`) would bank the wrong one. Set it up, then `addChild`.
839
+ - **A clone is reported as the template it came from.** `wiringPath()` answers
840
+ with the authored address wherever the clone lands, so a hundred towers are
841
+ one emitter in the audio/effects logs and in the `feels` rung — the promise
842
+ those logs already made for a spawner's waves, now true for a shelf too.
843
+
766
844
  ## Spawner
767
845
 
768
846
  Drip-feed clones of a TEMPLATE into the scene on a timer — enemy generators,
@@ -782,7 +860,7 @@ later clones get `EnemyTemplate2`, `EnemyTemplate3`, …)
782
860
  | Prop | Default | Meaning |
783
861
  |---|---|---|
784
862
  | `prefab` | `""` | node path of the template to clone (required) |
785
- | `interval` | `1` | seconds between spawns |
863
+ | `interval` | `1` | seconds between spawns — `0` is one clone per FRAME and is refused unless `max` or `total` caps it |
786
864
  | `max` | `0` | max LIVE instances (0 = unlimited) |
787
865
  | `at` | `[]` | where the clone appears, as an offset from the spawner ([x,y(,z)]) |
788
866
  | `autoStart` | `true` | begin ticking at ready |
@@ -814,7 +892,17 @@ also frees timed spawns.
814
892
  | `waves` | `[]` | array of `{ prefab, count, interval, delayBefore }` (required) |
815
893
  | `autoStart` | `true` | begin the first wave at ready |
816
894
 
817
- Signals: `waveStarted(index)` · `waveCleared(index)` · `allCleared`
895
+ Signals: `spawned(node)` · `waveStarted(index)` · `waveCleared(index)` ·
896
+ `allCleared`
897
+
898
+ `spawned` hands you the clone itself, before it has been stepped — the route to
899
+ anything PER-ENEMY that the template cannot say: a difficulty setting that
900
+ raises health, an elite that gets a tint, a list your own code keeps, a target
901
+ handed over on arrival.
902
+
903
+ ```json
904
+ { "signal": "spawned", "from": "Waves", "to": "Game", "handler": "onRaider" }
905
+ ```
818
906
 
819
907
  ```json
820
908
  { "name": "Waves", "type": "Node2D",
@@ -862,19 +950,55 @@ union-typed prop so its default is `null` (= `'forward'`).
862
950
 
863
951
  In 3D, `'forward'` is the engine's ONE facing convention — **+Z**, the same
864
952
  `atan2(dx, dz)` basis `CharacterController3D` uses and `incanto-3d-character.md`
865
- spells out under "the +Z-FORWARD rule". So aim the bullet (or its shooter) with
866
- that formula and it flies at what you aimed at; do not add 180°. (Before 0.62
867
- this returned three.js's −Z instead, so every `'forward'` shot left the muzzle
868
- pointing at the shooter's back — at the right speed, with no error, which reads
869
- as a gun that simply never hits anything.) In 2D `rotation` is a scalar and
870
- `'forward'` is +x turned clockwise by it, unchanged.
953
+ spells out under "the +Z-FORWARD rule". So aim the BULLET with that formula and
954
+ it flies at what you aimed at; do not add 180°. (Before 0.62 this returned
955
+ three.js's −Z instead, so every `'forward'` shot left the muzzle pointing at the
956
+ shooter's back — at the right speed, with no error, which reads as a gun that
957
+ simply never hits anything.) In 2D `rotation` is a scalar and `'forward'` is +x
958
+ turned clockwise by it, unchanged.
959
+
960
+ **Aim the bullet, not the shooter.** Turning a turret does not turn a bullet
961
+ parented under it: world position is composed TRANSLATION-ONLY, so a bolt under
962
+ a turret at `rotation: 90` still lands at world `[100, 0]`, not `[0, 100]`. Set
963
+ the heading on the projectile itself.
871
964
 
872
965
  | Prop | Default | Meaning |
873
966
  |---|---|---|
874
- | `speed` | `300` | units per second along `direction` |
875
- | `direction` | `null` | a vector `[x,y(,z)]` OR `'forward'` (`null` = forward) |
967
+ | `speed` | `300` | units per second along `direction` — **live**, rescales in flight |
968
+ | `direction` | `null` | a vector `[x,y(,z)]` OR `'forward'` (`null` = forward) — **live** |
876
969
  | `gravity` | `0` | constant +y (downward, 2D y-down) pull for arcing shots |
877
970
 
971
+ ### Aiming a bullet you just spawned
972
+
973
+ Every prop above is LIVE, and the heading is resolved on the projectile's FIRST
974
+ STEP — which is after `spawned(node)` fires. That window is the whole point:
975
+ `SpawnSource.spawn` ends with `addChild(clone)` and `Spawner` emits `spawned`
976
+ afterwards, so a handler is by definition "too late" for anything baked at
977
+ `onReady`.
978
+
979
+ ```ts
980
+ spawner.on('spawned', (bolt) => {
981
+ // either spelling works — set the node's rotation…
982
+ (bolt as { rotation: number }).rotation = (Math.atan2(dy, dx) * 180) / Math.PI;
983
+ // …or hand the behavior a vector
984
+ (bolt.behavior as Projectile).direction = [dx / len, dy / len];
985
+ });
986
+ ```
987
+
988
+ Mid-flight, writing `direction` or `speed` re-bakes immediately (the fall
989
+ `gravity` has accumulated is kept — it is a fact about the world, not about the
990
+ shot). What is deliberately NOT live is `rotation` after that first step: a
991
+ projectile that spins for looks must not have its spin become its trajectory.
992
+ Call **`aim()`** to re-derive from the current rotation on purpose:
993
+
994
+ ```ts
995
+ bolt.rotation = newHeadingDeg;
996
+ (bolt.behavior as Projectile).aim(); // or aim([dx, dy]) to pass a vector
997
+ ```
998
+
999
+ Before 0.70 all of this was dead: `direction` accepted the write, READ BACK the
1000
+ new value, and the bolt kept flying the old way.
1001
+
878
1002
  ```json
879
1003
  { "name": "Bullet", "type": "Area2D", "props": { "rotation": 0 },
880
1004
  "script": { "name": "Projectile", "props": { "speed": 600, "direction": "forward" } } }
@@ -1175,6 +1299,14 @@ The panel is hidden while playing and shown while paused — `GameFlow` owns its
1175
1299
  `visible`, so do not set it yourself. `pauseAction` and `pausePanelPath` rename
1176
1300
  either half; `pausePanelPath: ""` turns it off.
1177
1301
 
1302
+ While the panel is up its `HudLayer` gets `focusNavigation` — arrow keys, d-pad
1303
+ and A — and the ring starts on the first widget. The layer's own value is put
1304
+ back on resume, so a game that navigates by pad all the time keeps doing so —
1305
+ unless the game CHANGED it during the pause, in which case the game had the last
1306
+ word and keeps it.
1307
+ That is the one thing every pause menu needs and nobody remembers: a menu you
1308
+ can only click is not playable on a controller.
1309
+
1178
1310
  The pause key is polled in `update()`, which still runs at `timeScale 0` — a
1179
1311
  menu that cannot be closed by the key that opened it is worse than no menu.
1180
1312
 
@@ -1188,7 +1320,7 @@ physics, input. Listen to the `flowChanged(state)` signal for custom UI.
1188
1320
  `restartScene(engine)` is exported standalone.
1189
1321
 
1190
1322
  **Any scene swap thaws.** The freeze lives on the engine, not on the flow, so
1191
- leaving a game-over screen by your own route — `engine.setScene(loadScene(next))`
1323
+ leaving a game-over screen by your own route — `engine.setScene(loadScene(next, { engine }))`
1192
1324
  from a menu button — puts `timeScale` back to 1 as surely as `flow.restart()`
1193
1325
  does. You cannot restart into a frozen level.
1194
1326
 
@@ -1206,11 +1338,31 @@ swap into a physics scene run unsimulated — imperceptible, and the alternative
1206
1338
  was a game that never simulated. `physics: false` stays off across swaps, and a
1207
1339
  scene with no bodies still pays for nothing.
1208
1340
 
1341
+ **The line after a swap runs in a node that no longer exists.** `goToScene` and
1342
+ `setScene` FREE the tree they replace, and `this.engine` is
1343
+ `this.node.tree.engine` — so a Continue button that swaps and then talks to what
1344
+ arrived throws on its second line. Hold the engine first:
1345
+
1346
+ ```ts
1347
+ const engine = this.engine; // BEFORE the swap
1348
+ goToScene(engine, villageJson, { fadeSeconds: 0 });
1349
+ (engine.scene?.root.behavior as Hold).continueGame();
1350
+ ```
1351
+
1352
+ The engine says so by name (`has been FREED — a scene swap frees the tree it
1353
+ replaces`), and `node.freed` answers the same question in code.
1354
+
1209
1355
  ## DayNight (live 24-hour cycle)
1210
1356
 
1211
1357
  Drives the scene `environment` (needs an atmosphere sky): sun elevation/
1212
1358
  azimuth arc, ambient + exposure ramps with a moonlight floor.
1213
1359
 
1360
+ **No `sky` in the scene environment = nothing to drive.** The clock still runs
1361
+ and `dayPhaseChanged` still fires, so the signals look right and the screen
1362
+ never changes; the behaviour says so once, in the engine log. Add
1363
+ `"sky": { "type": "atmosphere" }` to the scene's `environment` — or set it at
1364
+ runtime, which it keeps looking for.
1365
+
1214
1366
  ```jsonc
1215
1367
  { "script": { "name": "DayNight", "props": { "daySeconds": 240, "startHour": 9 } } }
1216
1368
  ```