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
@@ -42,10 +42,17 @@ Reloading from source sidesteps all three. The structure comes from the file
42
42
  (authoritative, already validated); the save carries only what the file cannot
43
43
  know.
44
44
 
45
- **The cost, stated plainly: spawned enemies and mid-level positions are not
46
- restored.** You resume at the scene's start with stats, inventory, unlocks and
47
- quest flags intact — a checkpoint save. If your game needs a position, save it:
48
- `serialize()` returns anything.
45
+ **The cost, stated plainly: spawned enemies, mid-level positions and anything
46
+ else that lives in the TREE are not restored.** You resume at the scene's start
47
+ with stats, inventory, unlocks and quest flags intact — a checkpoint save. If
48
+ your game needs a position, save it: `serialize()` returns anything.
49
+
50
+ "Inventory" there means a behavior's DATA — a list of item ids, a wallet, a
51
+ quest flag. An inventory made of WIDGETS MOVED BETWEEN SLOTS
52
+ (`incanto-hud.md`'s drag recipe) is structure, so it reloads exactly as the
53
+ file has it: measured on a bench with one item dragged across, the save
54
+ mentions no slot and the reload puts the item back on its shelf. That skill
55
+ prints the `serialize` that keeps the slot map as data.
49
56
 
50
57
  **What the run CONSUMED is remembered.** Reloading from the file brings back
51
58
  every gem you already picked up, which would let a collect-five-to-win run
@@ -83,11 +90,59 @@ class QuestLog extends Behavior {
83
90
  `maxHealth` no — that comes back from the scene JSON, and duplicating it makes
84
91
  old saves fight your balance patches.
85
92
 
93
+ **Randomness is carried for you.** `captureState()` records where
94
+ `engine.rng` has got to and `restoreState()` puts it back, because without that
95
+ a load replays the seeded sequence from the top — the first "random" event after
96
+ a save is the one the RUN opened with, and every loot roll, wander and spawn
97
+ wobble repeats the opening of the game. Nothing to do; `engine.rng.position` is
98
+ readable if you want it for something else.
99
+
86
100
  `deserialize` is defensive on purpose: that data may come from a build of your
87
101
  game that shipped six weeks ago. Check what you read.
88
102
 
89
- Built-ins that already save: `Health` (current, dead), `ScoreKeeper` (score,
90
- lives, won/lost), `Collector` (total).
103
+ Built-ins that already save: `Collector` (total), `Currency` (amount),
104
+ `DayNight` (hour, paused), `Health` (current, dead), `SavePoint` (playtime),
105
+ `ScoreKeeper` (score, lives, won/lost), `WaveSpawner` (which wave).
106
+
107
+ `WaveSpawner` RESTARTS the wave you were on rather than resuming mid-spawn —
108
+ spawned entities are never restored, so resuming "four enemies into wave 3"
109
+ would resume a wave whose enemies do not exist. `waveStarted` fires again, so a
110
+ HUD wired to it catches up on its own.
111
+
112
+ Every one of them needs a `uid` on its node, and `incanto-check` says so if it
113
+ has none — that check reads the same list.
114
+
115
+ ## Tell the screen: `announce()`
116
+
117
+ `deserialize` writes fields. Your HUD is wired to **signals** — that is what
118
+ `incanto-hud.md` teaches and the only thing the editor can wire — so a restore
119
+ that only writes fields leaves the screen showing a fresh start:
120
+
121
+ ```
122
+ HUD score="0" hp=100/100 gems="0"
123
+ TRUTH score=1400 hp=38 gems=7
124
+ ```
125
+
126
+ with no error anywhere and a restore report of `{missing: [], restored: 3}`.
127
+
128
+ So there is a third hook, called once after the whole restore pass:
129
+
130
+ ```ts
131
+ override announce() {
132
+ this.emit('questChanged', this.stage); // what the screen SHOWS
133
+ }
134
+ ```
135
+
136
+ **Emit only what displays.** Not `died`, not `won`, not `levelUp` — a save is
137
+ being read, nothing just happened, and re-firing an outcome signal on load is
138
+ how a Continue lands straight on the game-over screen it was loaded to escape.
139
+ It runs after every `deserialize` in the pass, so a handler that reads a sibling
140
+ sees restored values there too.
141
+
142
+ `Health`, `ScoreKeeper`, `Collector` and `Currency` implement it. `ScoreKeeper`
143
+ gained **`livesChanged`** for this: `lifeLost` is the EVENT (flash, sound,
144
+ respawn) and fires only on a real loss, while `livesChanged` is the COUNT and
145
+ fires on both. Wire a lives counter to `livesChanged`.
91
146
 
92
147
  ## Every node you save — and every node that VANISHES — needs a uid
93
148
 
@@ -109,6 +164,20 @@ it just wrote is already wrong. It does not guess a replacement: a uid survives
109
164
  a rename and a reparent, and a saved PATH would point at whatever node moved
110
165
  into that slot after your next edit.
111
166
 
167
+ **Put the uid on the node that OWNS the state, not on its art.** A collectible
168
+ is normally one node with the script and a plain child for the sprite:
169
+
170
+ ```jsonc
171
+ { "name": "Sword", "type": "Area2D", "uid": "n_sr9scvhygy01mgxr",
172
+ "script": { "name": "ItemPickup" },
173
+ "children": [ { "name": "Icon", "type": "Sprite2D" } ] } // no uid needed
174
+ ```
175
+
176
+ Freeing the sword takes the icon with it, so the parent's uid in `#freed`
177
+ records both — the audit says nothing about the child, and adding a uid to it
178
+ changes nothing. What the audit still catches is the child that goes while its
179
+ parent LIVES: that one really does come back.
180
+
112
181
 
113
182
 
114
183
  The uid is the join key, because it is the one identifier that survives a rename
@@ -156,6 +225,35 @@ if (slot) {
156
225
  `onReady` is where a behavior sets its starting values, so restoring first would
157
226
  be overwritten.
158
227
 
228
+ **Not from `onReady` itself, either**, which is the tempting place to put
229
+ "continue my run" and the one that cannot work: the ready pass runs while the
230
+ scene is still loading, and `engine.scene` — which both halves of this API read
231
+ — is not assigned until `setScene`. Both say so now rather than answering
232
+ quietly:
233
+
234
+ ```
235
+ [incanto] restoreState() ran before the engine had a scene, and restored NOTHING.
236
+ ```
237
+
238
+ Before that it reported every uid in the save as `missing`, which is the
239
+ signature of a save format change, so the hunt started in the wrong place
240
+ entirely.
241
+
242
+ **To keep the continue INSIDE the game** (where a harness can reach it —
243
+ `runScript` boots the scene, never your `main.ts`), do it on the first frame:
244
+
245
+ ```ts
246
+ class Game extends Behavior {
247
+ private continued = false;
248
+ override update(): void {
249
+ if (this.continued) return;
250
+ this.continued = true;
251
+ const slot = this.slots.read('1');
252
+ if (slot) this.engine.restoreState(slot.state);
253
+ }
254
+ }
255
+ ```
256
+
159
257
  It never throws. A save naming a uid this build deleted reports it in
160
258
  `report.missing` and restores everything else; refusing to load would mean a
161
259
  patch that moves one node deletes everyone's progress.
@@ -194,6 +292,49 @@ explicitly when one file is entered more than one way (`"chapter-2-rescue"`).
194
292
  physics registers the new bodies before that scene's first frame — so a
195
293
  character walks in level two exactly as it did in level one.
196
294
 
295
+ ## State that outlives a SCENE, not just a session
296
+
297
+ A save and a scene swap are different mechanisms and the word "persist" covers
298
+ both, so this is the one thing a two-scene quest game has to be told: **the
299
+ three hooks above carry state across a SAVE and nothing across a SWAP.**
300
+
301
+ `goToScene` replaces the tree. The new scene's behaviors are new objects with
302
+ `onReady` starting values — a quest flag set in the village is gone when the
303
+ cellar loads, and nothing reports it, because nothing went wrong.
304
+
305
+ A quest that has to survive both is three parts, and each one alone fails
306
+ differently. Measured on a two-scene game built for this:
307
+
308
+ | | swap | save |
309
+ | --- | --- | --- |
310
+ | a module object in YOUR code | ✅ crosses | ❌ `captureState()` cannot see it — `restored: 0`, and the report is clean |
311
+ | `serialize`/`deserialize` alone | ❌ the new scene's behavior starts fresh | ✅ `restored: 1` |
312
+ | …plus `announce()` | — | the HUD, which otherwise reads the old value over restored state |
313
+
314
+ So: keep the truth in a module object, and give ONE behavior that exists in
315
+ every scene the job of writing it in and out.
316
+
317
+ ```ts
318
+ /** Not engine state — YOUR game's, and a module is where a swap cannot reach. */
319
+ export const quest = { accepted: false, carrying: false, done: false };
320
+
321
+ export class Hold extends Behavior {
322
+ static signals = ['questChanged'];
323
+ override serialize() { return { ...quest }; }
324
+ override deserialize(d: JsonValue) { Object.assign(quest, d as object); }
325
+ override announce() { this.emit('questChanged', quest.stage); } // the screen
326
+ }
327
+ ```
328
+
329
+ **Give that node the same `uid` in every scene it appears in.** The save is keyed
330
+ by uid, so `village.scene.json` and `cellar.scene.json` sharing one uid on their
331
+ root is what lets a save taken in either one restore into either one. Generate it
332
+ once with `newUid()` and paste it into both files — this is the single case where
333
+ one uid legitimately appears in two scenes, because it is one thing.
334
+
335
+ `behaviorsWithoutSave()` finds the behavior you forgot: on the game above it
336
+ named `/Village (Hold)` first, out of 46% of the scripted nodes it lists.
337
+
197
338
  ## A title screen, in JSON
198
339
 
199
340
  A `SavePoint` can ASK without loading. `probeOnReady` fires on the first frame
@@ -220,7 +361,19 @@ The label leads because that is what a menu shows; an unlabelled slot falls back
220
361
  to its scene key, so the line is never blank.
221
362
 
222
363
  **The button's press is still yours**, and rightly: `pressed → your router`. See
223
- the three lines above.
364
+ the three lines above. `examples/lanternhold-2d` ships this exact menu — a fresh
365
+ install with no Continue, and a returning player whose slot line reads what the
366
+ game SAVED (`last saved: Village` — the SavePoint's own label, not something the
367
+ title screen guessed).
368
+
369
+ Two things that cost a run each to learn there:
370
+
371
+ - **A hand-written slot with `state: {}` restores nothing.** The engine says so
372
+ — `restored 0 of 2 saveable behaviour(s)` — but a menu built on one looks
373
+ perfectly green. Write the slot the way the game writes it.
374
+ - **Hold the engine before you route.** `goToScene` frees the tree the button
375
+ lives in, so `this.engine` on the next line throws. See "Game flow" in
376
+ `incanto-gameplay-behaviors.md`.
224
377
 
225
378
  ## Several slots
226
379
 
@@ -235,6 +388,63 @@ slots.clear(); // "delete all data"
235
388
  One `SavePoint` per slot is the declarative version: three nodes with
236
389
  `slot: "1" | "2" | "3"`, each probing into its own row of the menu.
237
390
 
391
+ ### Restore into a scene that has been PLAYED
392
+
393
+ A save is restored INTO a freshly loaded scene. The structure comes from the
394
+ file; `#freed` then takes away the nodes that run had consumed — and nothing
395
+ puts any back. So a pause-menu Load, a slot menu, or death wired straight to
396
+ `SavePoint.restore()` leaves THIS run's collectibles deleted:
397
+
398
+ ```
399
+ CONTROL restart the scene from source, then restore → gems [Gem2,Gem3], won
400
+ death wired straight to restore (no reload) → gems [Gem3], lost
401
+ ```
402
+
403
+ …and the next autosave writes that hybrid back to the slot, so the unwinnable
404
+ run survives a page reload. Reload first — `GameFlow.restart` with
405
+ `restoreOnReady`, or `restartScene(engine)` and then restore. After a restart
406
+ the SavePoint node is a NEW one, so a held behavior reference is stale.
407
+
408
+ `report.stale` names the authored nodes this tree consumed that the save does
409
+ not account for, and the engine warns when it is non-empty. It is a warning
410
+ rather than an error because a node that freed ITSELF on a timer lands in the
411
+ same set, and only your game knows which of its nodes are transient.
412
+
413
+ ### A save hook that throws
414
+
415
+ `serialize()`, `deserialize()` and `announce()` are your code, and your code
416
+ throws. All three are caught now, and each says something different:
417
+
418
+ - a **`serialize()`** that throws leaves a HOLE in the save — that behavior's
419
+ whole run is missing — so `SavePoint.save()` **refuses to write** and emits
420
+ `saveFailed` instead of `saved`. Overwriting the previous slot with a holed
421
+ save destroys the progress the player actually had. `engine.lastCaptureFailures`
422
+ names them if you call `captureState()` yourself.
423
+ - a **`deserialize()`** that throws is in `report.skipped` as before, and now
424
+ also in **`report.refused`** with the reason — a save this build cannot read
425
+ and a node with no `deserialize` at all are different problems.
426
+ - an **`announce()`** that throws means the state IS restored and the SCREEN was
427
+ not told: **`report.unannounced`**. It reported `restored: 2, expected: 2`
428
+ over a HUD showing zeros, and it does not take an override to reach — a
429
+ `getNode('HUD/ScoreLabel')` on a renamed node lands there.
430
+
431
+ ### A save that cannot be read
432
+
433
+ A truncated write (a power cut, a tab closed mid-save) or a slot from a build
434
+ that predates this one is not offered by `all()` — but it is not *gone* either,
435
+ and a load menu that quietly shows one fewer row than the player remembers is
436
+ the worst thing a save system can do:
437
+
438
+ ```ts
439
+ for (const { id, why } of slots.problems()) {
440
+ render(`slot ${id}: ${why === 'corrupt' ? 'damaged' : 'from an older version'}`);
441
+ }
442
+ ```
443
+
444
+ The slot INDEX is rebuilt from storage when it cannot be read, so one corrupt
445
+ byte in a derived list no longer hides every save on the machine — nor lets the
446
+ next `write()` orphan them. It says so on the console when it does.
447
+
238
448
  ## Checking your coverage
239
449
 
240
450
  ```ts
@@ -33,14 +33,22 @@ wiring, and composition are all in the file. TS is for *behavior only* (attached
33
33
  ```
34
34
 
35
35
  - `format` MUST be `1`. `type` MUST be `"scene"`. `name` non-empty.
36
- - `dimension` is optional: `"2d"` or `"3d"` only.
36
+ - `dimension` is optional: `"2d"` or `"3d"` only. Left out, it is INFERRED from
37
+ the node types in the tree (anything ending `3D` makes the scene 3D) — write
38
+ it anyway, because it is what the environment validator, the audit's
39
+ no-camera/no-light warnings and `physics: "auto"` all read.
37
40
  - `fragment` (optional, default `false`) says this file is a PIECE of another
38
41
  scene — a spawner's prefab, a sub-scene something instances — and is never
39
42
  rendered on its own. It changes nothing at runtime; it tells the checker to
40
- stop asking a prefab where its camera and its sun are. Set it on any scene
41
- you load from TypeScript and hand to a spawner. (A scene another scene
42
- embeds with `instance` needs no key `incanto-check` walks the project and
43
- can see that for itself.)
43
+ stop asking a prefab where its camera and its sun are, and to stop answering
44
+ the two questions only the HOST can answer: a node path that leaves the file
45
+ (`/root/Bolts` is the fragment's own root only while the fragment is alone)
46
+ and a `DamageOnContact.targetGroup` whose victims live in the host scene.
47
+ Those two are still NAMED, with a reworded verdict — a typo is most likely in
48
+ exactly this file, written away from the scene that gives it meaning. Set it
49
+ on any scene you load from TypeScript and hand to a spawner. (A scene another
50
+ scene embeds with `instance` needs no key — `incanto-check` walks the project
51
+ and can see that for itself.)
44
52
  - `viewport` (optional) makes scene JSON own responsive layout: author the world
45
53
  in fixed `design` pixels (`[width, height]`, positive numbers) and the renderer
46
54
  maps them onto any canvas size. `fit`: `"expand"` (design rect always fully
@@ -96,12 +104,24 @@ Rules the engine enforces with **hard errors** (it never warns silently):
96
104
  3. Prop keys and value kinds are validated against the type's schema:
97
105
  unknown key → `UNKNOWN_PROP` (message lists valid keys); wrong JSON kind
98
106
  (e.g. string where the default is boolean) → `PROP_TYPE_MISMATCH`.
107
+ A few props accept TWO kinds — `Flowers3D.density` takes a preset name or
108
+ plants per m², `Water3D.underwater` takes `true`/`false` or a settings
109
+ object — and `incanto-node-reference.md` names both in its Kind column
110
+ (`one of: lush sparse none, or number`). The error names them too, so a
111
+ value the loader refuses tells you every shape it would have taken.
99
112
  4. Sibling names must be unique. Colliding names are auto-renamed by incrementing a
100
113
  trailing number (`Enemy` → `Enemy2` → `Enemy3`) — **write unique names yourself** so your
101
114
  connection paths stay valid (connections resolve after renaming).
102
115
  5. Node names must not contain `/` or `%` and must be non-empty.
103
116
  6. `groups` are string tags for queries (`getNodesInGroup`, connection filters).
104
117
  `tags` is free-form JSON for game-logic identity (`{"kind": "ITEM", "value": 10}`).
118
+ **`groups` is a LIST and `tags` is an OBJECT**, and the loader now says so:
119
+ `"groups": "player"` used to load clean and spread the string one CHARACTER
120
+ at a time (`["p","l","a","y","e","r"]`), so `player` was in no group and
121
+ every query and `filter: {group: "player"}` silently matched nothing. Every
122
+ node key is checked for its KIND now — `props`/`overrides` an object,
123
+ `children` a list, `uid` a non-empty string from `newUid()` — as are the
124
+ header's `constants`, `multiplayer` (objects) and `connections` (a list).
105
125
  7. `script` resolves to a registered Behavior at load (`incanto-behaviors-and-scripts.md`);
106
126
  `network` drives multiplayer replication (`incanto-multiplayer.md`). Both round-trip
107
127
  losslessly through export.
@@ -321,10 +341,21 @@ that do exist at the failing spot).
321
341
  never recovers.
322
342
  ```
323
343
 
324
- `died → ScoreKeeper.addScore` is the natural way to score a kill and it sets the
325
- score to `NaN` on the first one, with the win condition then permanently out of
326
- reach. A handler that DEFAULTS what it is not given is correct and stays silent:
327
- `won GameFlow.win` works, because `win(text = 'YOU WIN')` needs nothing. On **both**, it is
344
+ `died → ScoreKeeper.addScore` is the natural way to score a kill and it used to
345
+ set the score to `NaN` on the first one, with the win condition then
346
+ permanently out of reach. The numeric handlers REFUSE a non-number now, so the
347
+ same wire fails loudly instead and so does the one the arity check cannot
348
+ see: `clicked → addScore` hands the NODE, which is one argument into one
349
+ parameter, and `score + node` is a STRING that grows forever. A handler that DEFAULTS what it is not given is correct and stays silent:
350
+ `won → GameFlow.win` works, because `win(text = 'YOU WIN')` needs nothing.
351
+
352
+ Writing your OWN handler, the same rule applies to you: an optional parameter
353
+ is a **default**, not a `?`. `hurt(amount: number, from?: Node)` counts as
354
+ needing 2, because `?` is a TypeScript annotation that does not survive to
355
+ runtime — write `from: Node | undefined = undefined` and the wire is judged on
356
+ what it actually needs. Every engine method a wire can name has been corrected
357
+ to this; before that, four of five documented HUD wires were reported as
358
+ broken while working perfectly. On **both**, it is
328
359
  `AMBIGUOUS_HANDLER`: the node's method wins, so the script's would never run
329
360
  and nothing would say so. A core node answers to 62 public methods before any
330
361
  adapter adds more (`stop` `play` `show` `clear` `say` `start` `free` …), which
@@ -333,7 +364,11 @@ that do exist at the failing spot).
333
364
  - Unresolvable `from`/`to` → `DANGLING_CONNECTION` at load. Renaming a node breaks its
334
365
  connections **loudly** — update paths in the same edit.
335
366
  - `filter` gates firing on the first emitted argument: it must be a node in `filter.group`
336
- and/or match every `filter.tag` entry. With `once: true`, the connection is consumed only
367
+ and/or match every `filter.tag` entry. **A filter's keys are `group` and `tag`, and a
368
+ connection's are `[signal, from, to, handler, once, filter]`** — anything else is a load
369
+ error, because a filter the matcher cannot read (`{"gruop": "player"}`, or the bare
370
+ string `"player"`) passes EVERYTHING, which is the opposite of what a filter is for, and
371
+ `"once": "no"` is truthy so it fires exactly once. With `once: true`, the connection is consumed only
337
372
  when the filter matches.
338
373
 
339
374
  ## Sub-scene composition
@@ -345,7 +380,20 @@ that do exist at the failing spot).
345
380
  - `instance` embeds another scene's tree; `overrides` deep-merge onto the sub-scene root's
346
381
  props. At the instancing site, `groups` and `children` COMPOSE (union/append), while
347
382
  `tags`/`script`/`network` REPLACE the sub-scene root's values when declared (omitted = kept).
348
- - The sub-scene's own `connections` are wired inside its subtree automatically.
383
+ - The sub-scene's own `connections` are wired inside its subtree automatically,
384
+ and so are its `assets`, `input`, `strings`, `constants` and `orderGroups` —
385
+ **a prefab brings everything it declares**. (Its `orderGroups` used to come
386
+ through as NAMES only, so `"orderGroup": "loot"` validated and the band's
387
+ number was dropped: the sprite sorted in the default band and the scene loaded
388
+ clean.) A `coin.scene.json` with its own
389
+ spritesheet, or a `player.scene.json` with its own `move`/`jump` bindings, is
390
+ a complete, reusable thing.
391
+ - The HOST wins any asset key, action name or string it declares itself, so a
392
+ level can re-point a prefab's art or rebind its controls without editing the
393
+ prefab. Two INSTANCED scenes declaring one asset key with different urls — or
394
+ one action name with different keys — is a hard error naming both:
395
+ first-writer-wins would hand the second prefab the first one's art in silence.
396
+ The same key with the same value is just two prefabs agreeing.
349
397
  - There is **no** scene inheritance — composition only.
350
398
  - Current limitation: exporting expands instances into full trees (the `instance` reference is
351
399
  not preserved on export yet).
@@ -373,6 +421,12 @@ that do exist at the failing spot).
373
421
  | `DUPLICATE_BEHAVIOR` | two classes registered under one behavior name | rename one, or `{ replace: true }` for hot reload |
374
422
  | `TREE_VIOLATION` | invalid name, re-parenting without detach, cycles, double root | follow the rule in the message |
375
423
 
424
+ Every one of these lists what WOULD have worked, and names the nearest
425
+ candidate when there is one — `Unknown node type 'Label2D'. Did you mean
426
+ "Label"?`, `Unknown prop 'size' on 'Label'. Did you mean "fontSize"?`. A name
427
+ that resembles nothing gets the list alone, because a suggestion that is not the
428
+ answer is worse than none.
429
+
376
430
  Scene-load and registry errors carry structured `details` (`path`, `uid`, `nodeType`,
377
431
  `prop`, `signal`, `validOptions`) mirroring the prose — prefer those over regexing the
378
432
  message (a few runtime errors still carry prose only). Scene-load
@@ -384,7 +438,10 @@ errors append the offending node's path: `… (at '/Level/Enemies/Slime3')`.
384
438
  import { loadScene, registerCoreNodes, duplicateNode } from 'incanto';
385
439
 
386
440
  registerCoreNodes(); // explicit — never an import side effect
387
- const scene = loadScene(json, { resolveScene }); // throws IncantoError on any problem
441
+ // `{ engine }` on a manual boot: `onReady` runs during the LOAD, so a behaviour
442
+ // that reads `this.engine` or `this.rng` there throws without it. `createGame2D`
443
+ // /`3D` and `runScript` pass it for you.
444
+ const scene = loadScene(json, { engine, resolveScene }); // throws IncantoError on any problem
388
445
  scene.root.getNode('Player').emit('hit', 10);
389
446
  const copy = duplicateNode(scene.root.getNode('Coin'));
390
447
  const exported = scene.toJSON(); // lossless (delta-only props)