incanto 0.70.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 (67) hide show
  1. package/bin/incanto-verify.mjs +36 -98
  2. package/dist/2d.d.ts +18 -3
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +3 -3
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CmNF01gA.js → agent8-Cfmd3ar_.js} +1 -1
  7. package/dist/{audio-player-DaMxqfNE.d.ts → audio-player-L8yccdMP.d.ts} +1 -1
  8. package/dist/{behavior-DZExDn9o.d.ts → behavior-Dcz0fr1S.d.ts} +11 -0
  9. package/dist/{create-game-Bwvh6q8A.js → create-game-CH4kt78v.js} +7 -7
  10. package/dist/{create-game-C7ffQWW7.js → create-game-LCHBLfA4.js} +7 -7
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/editor.js +29 -25
  13. package/dist/{environment-presets-D6Q5BxeE.js → environment-presets-99BzP_L-.js} +3 -3
  14. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  15. package/dist/{gameplay-BfHkuzVb.js → gameplay-CNULJvwh.js} +108 -6
  16. package/dist/gameplay.d.ts +68 -4
  17. package/dist/gameplay.js +2 -2
  18. package/dist/index.d.ts +27 -4
  19. package/dist/index.js +4 -4
  20. package/dist/{loader-Cff09LMm.d.ts → loader-8-IHvbKD.d.ts} +1 -1
  21. package/dist/net.d.ts +2 -2
  22. package/dist/net.js +2 -2
  23. package/dist/{physics-2d-CE0Qvy3V.js → physics-2d-FhrXUc6B.js} +2 -2
  24. package/dist/{physics-3d-CpH-2gn5.js → physics-3d-CFEGxBXZ.js} +3 -3
  25. package/dist/{picking-CQJ_PJKh.js → picking-DVo7fI13.js} +3 -3
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-Bkk0wSDB.js → register-BYQCBySi.js} +100 -8
  29. package/dist/{register-6DYnKZcy.js → register-DuVdzxbq.js} +49 -15
  30. package/dist/{replay-DjAkAzMq.js → replay-DEvp3kyV.js} +2 -2
  31. package/dist/{replay-Dmw-PKQu.d.ts → replay-F7IZHdFR.d.ts} +1 -1
  32. package/dist/{save-slots-BXVg148r.js → save-slots-Bvuh2p_r.js} +56 -3
  33. package/dist/{split-screen--k-XpBjr.d.ts → split-screen-CIYf1zSf.d.ts} +2 -2
  34. package/dist/{split-screen-PL78oVXP.js → split-screen-eULetcg-.js} +3 -3
  35. package/dist/{src-Cxfiv1Hg.js → src-_jk0qLsS.js} +1 -1
  36. package/dist/{test-iHYVUcDK.js → test-CmN54kWv.js} +252 -24
  37. package/dist/test.d.ts +113 -5
  38. package/dist/test.js +3 -3
  39. package/dist/vite.js +3 -3
  40. package/editor/assets/{agent8-D0MS174y.js → agent8-yLIEYHbd.js} +1 -1
  41. package/editor/assets/{debug-BnXkKuYu.js → debug-u31w_yhq.js} +1 -1
  42. package/editor/assets/{index-CIu3uc3l.js → index-DK9xMGpW.js} +94 -94
  43. package/editor/index.html +1 -1
  44. package/package.json +1 -1
  45. package/schemas/scene.schema.json +4 -0
  46. package/skills/incanto-behaviors-and-scripts.md +30 -9
  47. package/skills/incanto-building-2d-games.md +20 -2
  48. package/skills/incanto-gameplay-behaviors.md +63 -1
  49. package/skills/incanto-hud.md +14 -0
  50. package/skills/incanto-node-reference.md +18 -2
  51. package/skills/incanto-playtesting.md +34 -0
  52. package/skills/incanto-save-slots.md +56 -1
  53. package/skills/incanto-verifying-your-game.md +11 -0
  54. package/templates-app/beacon-isle-3d/package.json +1 -1
  55. package/templates-app/beacon-isle-3d/src/game.scene.json +44 -226
  56. package/templates-app/molehill-2d/package.json +1 -1
  57. package/templates-app/molehill-2d/src/game.scene.json +10 -60
  58. package/templates-app/platformer-2d/package.json +1 -1
  59. package/templates-app/platformer-2d/src/game.scene.json +80 -416
  60. package/templates-app/star-survivor/package.json +1 -1
  61. package/templates-app/star-survivor/src/game.scene.json +5 -24
  62. package/templates-app/tps-3d/package.json +1 -1
  63. package/templates-app/tps-3d/src/game.scene.json +13 -71
  64. package/templates-app/village-quest-3d/package.json +1 -1
  65. package/templates-app/village-quest-3d/src/behaviors.ts +21 -4
  66. package/templates-app/village-quest-3d/src/grove.scene.json +16 -87
  67. package/templates-app/village-quest-3d/src/village.scene.json +5 -27
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-CIu3uc3l.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-DK9xMGpW.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -6757,6 +6757,10 @@
6757
6757
  "type": "object",
6758
6758
  "default": {}
6759
6759
  },
6760
+ "colors": {
6761
+ "type": "object",
6762
+ "default": {}
6763
+ },
6760
6764
  "solid": {
6761
6765
  "type": "array",
6762
6766
  "default": []
@@ -177,7 +177,7 @@ What behavior code actually calls at runtime — all instance methods, no global
177
177
  | `engine.stop()` / `engine.start()` | pause / resume the loop — stop resets the clock and accumulator, so no banked sim time leaks into the resume |
178
178
  | `engine.step()` | advance exactly ONE fixed step + one update (both dt = the fixed step) — the unit of time for headless tests |
179
179
  | `engine.tick(timestampMs)` | manual frame advance — takes an **absolute** ms timestamp (rAF-style), NOT a dt; the first call after (re)start only primes the clock |
180
- | `engine.setScene(scene)` | swap scenes: the previous root is freed, the input map is cleared and redeclared from the new scene's `input{}`, the clock resets (`time`, `unscaledTime`, and **`timeScale` back to 1** — a level restarted out of a frozen game-over must not boot frozen), then `sceneChanged` fires |
180
+ | `engine.setScene(scene)` | swap scenes: the previous root is freed, the input map is cleared and redeclared from the new scene's `input{}`, the clock resets (`time`, `unscaledTime`, and **`timeScale` back to 1** — a level restarted out of a frozen game-over must not boot frozen), then `sceneChanged` fires. It fires with **null** at `dispose()` too — a handler that reads `scene.root` has to check, or teardown says so and continues without it |
181
181
  | `node.off(signal, fn)` | disconnect ONE listener you connected with `on` — pass the same function reference. (`free()` disconnects everything a node owns, so this is for a listener that must stop while the node lives on) |
182
182
  | `node.signal('died')` | the `Signal` object itself, for `connect`/`disconnect` by hand; undeclared names throw, like `emit` |
183
183
  | `node.listenerCount('died')` | how many are listening — a test's way to prove a wire was made, or dropped |
@@ -187,19 +187,40 @@ The recurring traps: `duplicateNode` does NOT insert the clone anywhere — a
187
187
  "spawner that does nothing" usually forgot `addChild`.
188
188
 
189
189
  **The template you clone FROM is a live node.** Its behaviors run, its timers
190
- tick, its turret shoots — `visible: false` hides a node, it does not switch it
191
- off. `Spawner` and `WaveSpawner` sidestep this by DETACHING their `prefab` at
192
- ready; a shelf of templates you clone yourself is still in the tree, so author
193
- their scripts `"enabled": false` and wake the clone:
190
+ tick, its turret shoots, its `autoplay` audio plays — `visible: false` hides a
191
+ node, it does not switch it off. `Spawner` and `WaveSpawner` sidestep this by
192
+ DETACHING their `prefab` at enter. For a shelf of templates you clone yourself,
193
+ put `PrefabShelf` on the shelf node and it does the same for all of them:
194
194
 
195
+ ```json
196
+ { "name": "Prefabs", "type": "Node2D", "script": { "name": "PrefabShelf" },
197
+ "children": [ { "name": "Tower", "type": "Node2D", "children": [] } ] }
198
+ ```
195
199
  ```ts
196
- const tower = duplicateNode(this.node.getNode('/Game/Prefabs/Tower'));
197
- tower.behavior?.enable(); // the template stays asleep, this one works
200
+ const shelf = this.node.getNode('/Game/Prefabs').behavior as PrefabShelf;
201
+ const tower = shelf.make('Tower'); // detached, awake all the way down
202
+ tower.position = at; // set it up BEFORE it readies
198
203
  this.node.getNode('/Game/Towers').addChild(tower);
199
204
  ```
200
205
 
201
- Without it an invisible tower at the origin defends your map, and the game looks
202
- fine. **And look the template up by PATH, not `%Name`**: a clone keeps its
206
+ Its children never ENTER the tree, so nothing has to be hidden and nothing has
207
+ to be asleep. `make()` returns the clone DETACHED, like `duplicateNode` not a
208
+ formality: `onReady` fires on attach, and a behavior that banks its node's
209
+ position there (`FloatAway`) would bank the wrong one. `names()` lists what the
210
+ shelf holds, and asking for anything else fails saying so.
211
+
212
+ **Why not just author the templates `"enabled": false`?** That was the old
213
+ advice and it half-works: `enabled` is a per-behavior pause switch, so
214
+ `clone.behavior?.enable()` wakes ONE node — while the shape this same skill
215
+ teaches (one behavior per node, so a prefab's parts live on children) leaves
216
+ every child asleep, silently and for good. Measured on a shipped tower defense:
217
+ the bolt's root `Projectile` was woken and its sibling `Lifetime` was not, so
218
+ every bolt that MISSED flew forever — five still in the tree at 69 seconds. And
219
+ `enabled` never silenced the template's `autoplay` audio at all, which is how
220
+ that game's `feels` rung read `every one of the 4 emitters fired` while two of
221
+ the four were templates going off on the shelf. `enabled: false` is for a state
222
+ a node deliberately starts in (a `Chase` waiting on `spotted`), not for hiding a
223
+ template from the tree. **And look the template up by PATH, not `%Name`**: a clone keeps its
203
224
  template's name, so `%Tower` is unambiguous exactly until the first one is
204
225
  placed, and then it throws `DUPLICATE_UNIQUE_NAME` from inside your build
205
226
  handler — a game that works once. `engine.tick(16)` does
@@ -105,8 +105,26 @@ stop hand-placing dozens of ColorRect walls:
105
105
  - Cell (0,0) hangs its TOP-LEFT on the node's origin; position the node to
106
106
  place the level. `columns: 0` (default) derives atlas columns from the
107
107
  texture width.
108
- - No texture yet? Colliders still work pair with ColorRect2D placeholders or
109
- just leave it invisible while you block out the level.
108
+ - **No art yet? Paint with `colors`.** A char `#rrggbb` map draws that cell as
109
+ a flat quad, in the same single draw call, with no atlas at all:
110
+
111
+ ```jsonc
112
+ { "name": "Walls", "type": "TileMap2D", "props": {
113
+ "tileSize": 56,
114
+ "cells": ["########", "#......#", "########"],
115
+ "colors": { "#": "#3a4356" },
116
+ "solid": ["#"]
117
+ } }
118
+ ```
119
+
120
+ Exclusive with `texture` (one material, one draw call — a half-textured level
121
+ that loads clean is exactly the silence this engine fails loudly about), and
122
+ `solid` takes the CHAR you painted as well as an atlas index, because that is
123
+ the thing an author knows. Everything else is unchanged: `cellAt`, `tileAt`,
124
+ `setTile`, `worldAt` and the merged colliders work the same. It exists because
125
+ a sokoban built from the published package came out as 24 hand-placed
126
+ `ColorRect2D` walls — the one node that answers every question a grid game
127
+ asks could not be SEEN.
110
128
  - Change the map at runtime by REPLACING `cells` (mutations are not watched):
111
129
  `map.cells = [...rows]` — geometry and colliders rebuild next frame. For ONE
112
130
  cell use `setTile` below, which does that correctly.
@@ -803,6 +803,44 @@ a `draft` that puts the waterline on it:
803
803
  For the water's own height anywhere (jetties, spawn heights, an AI deciding
804
804
  where the shore is), `Water3D.heightAt(x, z)` answers the same surface.
805
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
+
806
844
  ## Spawner
807
845
 
808
846
  Drip-feed clones of a TEMPLATE into the scene on a timer — enemy generators,
@@ -854,7 +892,17 @@ also frees timed spawns.
854
892
  | `waves` | `[]` | array of `{ prefab, count, interval, delayBefore }` (required) |
855
893
  | `autoStart` | `true` | begin the first wave at ready |
856
894
 
857
- 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
+ ```
858
906
 
859
907
  ```json
860
908
  { "name": "Waves", "type": "Node2D",
@@ -1290,6 +1338,20 @@ swap into a physics scene run unsimulated — imperceptible, and the alternative
1290
1338
  was a game that never simulated. `physics: false` stays off across swaps, and a
1291
1339
  scene with no bodies still pays for nothing.
1292
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
+
1293
1355
  ## DayNight (live 24-hour cycle)
1294
1356
 
1295
1357
  Drives the scene `environment` (needs an atmosphere sky): sun elevation/
@@ -259,6 +259,20 @@ widget use `UiButton`'s `pressed`.
259
259
  Setting `.value` from a behavior updates the control and does **NOT** re-emit
260
260
  `changed` — restoring a saved setting must not fire the handler that saved it.
261
261
 
262
+ **`choose(value)` is the other half**: set it as a PERSON would, and say so.
263
+ `UiButton` has always had `press()`; the value widgets had nothing, so a preset
264
+ button, a "reset to defaults", a tutorial that moves a slider for you, or a
265
+ harness checking its own options screen had no public route at all.
266
+
267
+ ```ts
268
+ (hud.getNode('%Volume') as UiSlider).choose(0.25); // clamped, emits changed
269
+ (hud.getNode('%Invert') as UiToggle).choose(true);
270
+ (hud.getNode('%Quality') as UiSelect).choose('high'); // an option it does not
271
+ // offer is refused
272
+ ```
273
+
274
+ Both are silent when nothing changes.
275
+
262
276
  **Every prop is live.** `color`, `size`, `width`, `background`, `label` and
263
277
  `anchor` are re-read each frame, so flashing the score red, growing a health bar
264
278
  or moving a widget to another corner all work from a behavior — and from the
@@ -656,7 +656,7 @@ _No props (structural fields only)._
656
656
 
657
657
  Methods: `addChild(child)` · `removeChild(child)` · `reparent(newParent)` · `findChild(name, recursive?)` · `getRoot()` · `getNodeByUid(uid)` · `getNodesByName(name)` · `getPath()` · `wiringPath()` · `getNode(path)` · `getNodeOrNull(path)` · `addToGroup(group)` · `removeFromGroup(group)` · `isInGroup(group)` · `declareSignal(name)` · `declaredSignalNames()` · `signal(name)` · `on(signal, fn, opts?)` · `off(signal, fn)` · `listenerCount(signal)` · `emit(signal, ...args)` · `queueFree()` · `free()`
658
658
 
659
- Reads: `uid: string | null` · `name: string` · `parent: Node | null` · `children: readonly Node[]` · `groups: ReadonlySet<string>` · `tree: SceneTree | null` · `isReady: boolean`
659
+ Reads: `uid: string | null` · `name: string` · `parent: Node | null` · `children: readonly Node[]` · `groups: ReadonlySet<string>` · `tree: SceneTree | null` · `isReady: boolean` · `freed: boolean`
660
660
 
661
661
  ## `Node2D` — `incanto/2d`
662
662
 
@@ -1009,6 +1009,7 @@ Methods: `heightAt(x, z)` · `resolvedLayers()` · `free()`
1009
1009
  | `columns` | `0` | number |
1010
1010
  | `cells` | `[]` | array |
1011
1011
  | `legend` | `{}` | object |
1012
+ | `colors` | `{}` | object |
1012
1013
  | `solid` | `[]` | array |
1013
1014
  | `opacity` | `1` | number |
1014
1015
 
@@ -1295,6 +1296,8 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `change
1295
1296
 
1296
1297
  Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1297
1298
 
1299
+ Methods: `choose(value)`
1300
+
1298
1301
  ## `UiSlider` — `incanto`
1299
1302
 
1300
1303
  | Prop | Default | Kind |
@@ -1314,6 +1317,8 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `change
1314
1317
 
1315
1318
  Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1316
1319
 
1320
+ Methods: `choose(value)`
1321
+
1317
1322
  ## `UiText` — `incanto`
1318
1323
 
1319
1324
  | Prop | Default | Kind |
@@ -1349,6 +1354,8 @@ Reads: `shown: string`
1349
1354
 
1350
1355
  Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1351
1356
 
1357
+ Methods: `choose(value)`
1358
+
1352
1359
  ## `UiVolumeSlider` — `incanto`
1353
1360
 
1354
1361
  | Prop | Default | Kind |
@@ -1712,6 +1719,15 @@ Signals: `reachedPoint(index)`
1712
1719
 
1713
1720
  Signals: `collected(value, other)`
1714
1721
 
1722
+ ### `PrefabShelf`
1723
+
1724
+ | Prop | Default | Kind |
1725
+ |---|---|---|
1726
+ | `enabled` | `true` | boolean |
1727
+ | `prefabs` | `""` | string |
1728
+
1729
+ Methods: `names()` · `make(name)`
1730
+
1715
1731
  ### `Projectile`
1716
1732
 
1717
1733
  | Prop | Default | Kind |
@@ -1789,7 +1805,7 @@ Reads: `liveCount: number`
1789
1805
  | `waves` | `[]` | array · **required** |
1790
1806
  | `autoStart` | `true` | boolean |
1791
1807
 
1792
- Signals: `waveStarted(index)` · `waveCleared(index)` · `allCleared`
1808
+ Signals: `spawned(node)` · `waveStarted(index)` · `waveCleared(index)` · `allCleared`
1793
1809
 
1794
1810
  Methods: `start()`
1795
1811
 
@@ -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 |
@@ -138,6 +156,22 @@ lifted out of the tree at load, so the check reads the scene FILE as well as the
138
156
  loaded tree, and a wave of forty enemies is reported as its one authored
139
157
  emitter, not forty.
140
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
+
141
175
  **`nothing here can hurt you`** is a design report, not an error. Sometimes it
142
176
  is exactly right (a walking simulator). Usually it means the hazards are not
143
177
  wired up.
@@ -292,6 +292,49 @@ explicitly when one file is entered more than one way (`"chapter-2-rescue"`).
292
292
  physics registers the new bodies before that scene's first frame — so a
293
293
  character walks in level two exactly as it did in level one.
294
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
+
295
338
  ## A title screen, in JSON
296
339
 
297
340
  A `SavePoint` can ASK without loading. `probeOnReady` fires on the first frame
@@ -318,7 +361,19 @@ The label leads because that is what a menu shows; an unlabelled slot falls back
318
361
  to its scene key, so the line is never blank.
319
362
 
320
363
  **The button's press is still yours**, and rightly: `pressed → your router`. See
321
- 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`.
322
377
 
323
378
  ## Several slots
324
379
 
@@ -685,6 +685,17 @@ then land on a game that never advances: the capture comes back UNCHANGED and
685
685
  looks perfectly healthy. `drove 0 frames` is refused with an error rather than
686
686
  handed over as a picture.
687
687
 
688
+ **A PLAIN capture works on a hidden page.** Only a DRIVE needs the window in
689
+ front. `captureFrame` waits ~120 ms for the game loop and then draws a frame
690
+ itself, exactly because a hidden tab gets none — so the pixels are current, the
691
+ world is simply paused. Measured, hiding every node from the console and
692
+ capturing again: 2D `luminance 0.09 → 0.00 BLACK SCREEN`, 3D `0.64 → 0.96,
693
+ subject 44.3% → 22.0%`.
694
+
695
+ That is the whole point for an agent: **your terminal is in front and the game
696
+ is behind it, and `draws` still answers.** The report says the world is paused,
697
+ and the `draws` rung carries that as a note under a pass.
698
+
688
699
  #### Look at it
689
700
 
690
701
  ```
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.70.0",
17
+ "incanto": "^0.71.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {