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
@@ -1,4 +1,4 @@
1
- import { a as Rng, c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
1
+ import { a as SceneJson, c as JsonValue, o as JsonKind, s as JsonObject, t as Rng } from "./rng-Bb-IutXB.js";
2
2
 
3
3
  //#region src/core/signal.d.ts
4
4
  /**
@@ -85,6 +85,43 @@ declare class SceneTree {
85
85
  */
86
86
  private readonly _nameIndex;
87
87
  /**
88
+ * group → the ATTACHED nodes in it, maintained the way names are.
89
+ *
90
+ * `getNodesInGroup` was an un-indexed full-tree walk that allocated a fresh
91
+ * array per call, while the sibling name lookup was made O(1) for exactly
92
+ * this reason. The one engine behavior that calls it every frame —
93
+ * `FaceTarget`, the documented tower-defence turret — therefore cost
94
+ * turrets x TOTAL SCENE NODES per frame: a 40-turret map in a 5,000-node
95
+ * world walked 200,000 nodes a frame to find 40 of them, and allocated 40
96
+ * arrays doing it.
97
+ */
98
+ private readonly _groupIndex;
99
+ /**
100
+ * uid → node, maintained the way names are.
101
+ *
102
+ * `getNodeByUid` was a depth-first walk of the whole tree, and `restoreState`
103
+ * calls it once per saved uid plus once per freed uid — so loading a save
104
+ * was `nodes x saved-keys`. A 3,000-node world with 200 saved behaviors
105
+ * walked 600,000 nodes to hand back 200 values, on the frame the player is
106
+ * already waiting through a level load.
107
+ */
108
+ private readonly _uidIndex;
109
+ /**
110
+ * Templates a spawner has detached, by the path they were detached FROM.
111
+ *
112
+ * A spawner takes its prefab out of the live tree so it stops ticking. The
113
+ * second spawner pointing at the same template then found nothing and
114
+ * hard-failed the scene:
115
+ *
116
+ * Spawner on '/World/B': "prefab" '../Coin' does not resolve to a template
117
+ *
118
+ * One shared coin between two spawners is an ordinary thing to author, so the
119
+ * tree remembers where each template went. Scene-scoped, like everything
120
+ * else here — a new scene starts with none.
121
+ * @internal
122
+ */
123
+ readonly _templates: Map<string, Node>;
124
+ /**
88
125
  * Bumped whenever ANY node attaches or detaches — cheap "did the tree
89
126
  * change shape?" check for per-step body gathers and similar caches.
90
127
  */
@@ -127,6 +164,14 @@ declare class SceneTree {
127
164
  get frameId(): number;
128
165
  /** @internal Node attach hook. */
129
166
  _indexName(node: Node): void;
167
+ /** @internal A uid was assigned or changed on an ATTACHED node. */
168
+ _reindexUid(node: Node, oldUid: string | null): void;
169
+ /** @internal The node carrying this uid, or undefined. */
170
+ _nodeWithUid(uid: string): Node | undefined;
171
+ /** @internal A node joined a group, or arrived already in one. */
172
+ _indexGroup(node: Node, group: string): void;
173
+ /** @internal A node left a group, or left the tree. */
174
+ _unindexGroup(node: Node, group: string): void;
130
175
  /**
131
176
  * @internal A RENAME: same node, same place, different key in the name index.
132
177
  *
@@ -151,6 +196,14 @@ declare class SceneTree {
151
196
  /** @internal */
152
197
  _setEngine(engine: Engine | null): void;
153
198
  setRoot(node: Node): void;
199
+ /**
200
+ * Has the initial load finished?
201
+ *
202
+ * `false` through `setRoot`'s enter/ready pass — where a throw is a load
203
+ * error and must stay one — and `true` for everything after it.
204
+ * @internal
205
+ */
206
+ _live: boolean;
154
207
  update(dt: number): void;
155
208
  fixedUpdate(dt: number): void;
156
209
  getNodesInGroup(group: string): Node[];
@@ -180,12 +233,6 @@ interface NodeLifecycle {
180
233
  update(dt: number): void;
181
234
  fixedUpdate(dt: number): void;
182
235
  }
183
- /**
184
- * Base class of everything in an Incanto scene (Godot's Node model).
185
- *
186
- * Pure data + tree structure: no rendering, no DOM, no three.js — renderer
187
- * adapters subscribe from the outside.
188
- */
189
236
  declare class Node implements NodeLifecycle {
190
237
  static readonly typeName: string;
191
238
  /**
@@ -198,9 +245,49 @@ declare class Node implements NodeLifecycle {
198
245
  private _name;
199
246
  private _parent;
200
247
  private readonly _children;
248
+ /**
249
+ * child name → how many children carry it. Built on first use, then
250
+ * MAINTAINED — the whole reason this exists.
251
+ *
252
+ * `uniqueSiblingName` used to build a fresh `Set` of every existing sibling's
253
+ * name on every insert, BEFORE asking whether the requested name was even
254
+ * taken. Filling one parent therefore costs K(K+1)/2 Set operations, and a
255
+ * single attach into a horde of 8,000 cost about thirteen times what
256
+ * simulating that whole tree for a frame costs. A spawner-driven game gets
257
+ * slower the longer it is played, at the moment it is busiest.
258
+ *
259
+ * The three places `_children` changes are the three places this changes:
260
+ * `addChild`, `removeChild` and the `name` setter. `_children` is private to
261
+ * this file, so that list is the whole list.
262
+ */
263
+ private _childNames;
264
+ /**
265
+ * stem → the next trailing number worth TRYING. A hint, never the truth.
266
+ *
267
+ * The name index alone left a second quadratic in the same function: eight
268
+ * thousand children all called `Enemy` means eight thousand collisions, and
269
+ * `while (taken.has(stem + counter)) counter += 1` restarted at 2 every time.
270
+ * Measured, 4x the children still cost 16.7x the time. Starting the scan
271
+ * where the last one finished makes the whole fill linear; the `while` still
272
+ * runs, so a wrong hint costs a step and never a wrong name.
273
+ */
274
+ private _nameCounters;
201
275
  private readonly _groups;
202
276
  private readonly _signals;
277
+ /**
278
+ * Disconnects for connections this node made on OTHER nodes.
279
+ *
280
+ * Null until it makes one — most nodes never do, and an empty array per node
281
+ * is a real cost in a scene with thousands of them.
282
+ * @internal
283
+ */
284
+ _outgoing: Array<() => void> | null;
203
285
  private _declared;
286
+ /**
287
+ * signal → (the listener a caller gave us) → (the quarantining wrapper Signal
288
+ * actually holds). Null until this node has a listener, which most never do.
289
+ */
290
+ private _wrapped;
204
291
  private _tree;
205
292
  private _ready;
206
293
  /**
@@ -208,7 +295,9 @@ declare class Node implements NodeLifecycle {
208
295
  * among siblings) a uid is unique across the whole scene, so scripts and
209
296
  * tools can address a node no matter where it moves.
210
297
  */
211
- uid: string | null;
298
+ private _uid;
299
+ get uid(): string | null;
300
+ set uid(value: string | null);
212
301
  /** Free-form JSON identity for game logic (e.g. `{kind: 'ITEM', value: 10}`). */
213
302
  tags: Record<string, unknown>;
214
303
  /** Behavior attachment blob from scene JSON (resolved by the loader). */
@@ -227,6 +316,9 @@ declare class Node implements NodeLifecycle {
227
316
  get tree(): SceneTree | null;
228
317
  /** Whether onReady has run (it runs at most once per instance). */
229
318
  get isReady(): boolean;
319
+ /** The sibling-name index, built once per parent and then kept up to date. */
320
+ private childNames;
321
+ private nameCounters;
230
322
  addChild<T extends Node>(child: T): T;
231
323
  removeChild(child: Node): void;
232
324
  reparent(newParent: Node): void;
@@ -238,8 +330,33 @@ declare class Node implements NodeLifecycle {
238
330
  /** Every node in THIS subtree named `name` — names repeat, so a list. */
239
331
  getNodesByName(name: string): Node[];
240
332
  getPath(): string;
333
+ /**
334
+ * The path of the AUTHORED node this one stands for — `getPath()` with every
335
+ * cloned ancestor's auto-renamed name put back (`/Game/Foe37/Fx` →
336
+ * `/Game/Foe/Fx`).
337
+ *
338
+ * "Is this AudioPlayer wired?" is a question about the scene, and a wave of
339
+ * forty enemies is one emitter fired forty times, not forty emitters. The
340
+ * feedback sets ask it, and answering with the live path made a set the
341
+ * engine promised would "grow with the SCENE, not with the run" grow by one
342
+ * entry per spawn, forever, in every shipped game.
343
+ *
344
+ * The event LOGS keep the live path: a log names the node that acted.
345
+ */
346
+ wiringPath(): string;
241
347
  getNode(path: string): Node;
242
348
  getNodeOrNull(path: string): Node | null;
349
+ /**
350
+ * `parseNodePath`, but the error says WHERE it was written.
351
+ *
352
+ * Every other scene-load error appends the offending node — `NODE_NOT_FOUND`
353
+ * lists the children that do exist, an unknown type ends `(at '/Level')` —
354
+ * and the six `BAD_NODE_PATH` messages were the only family in the engine
355
+ * that named neither the node nor anything greppable. A bad path in a
356
+ * behavior prop printed `[BAD_NODE_PATH] Node path must not be empty.` and
357
+ * nothing else, so bisecting the scene was the only way to find it.
358
+ */
359
+ private parseFrom;
243
360
  private resolve;
244
361
  addToGroup(group: string): void;
245
362
  removeFromGroup(group: string): void;
@@ -254,9 +371,36 @@ declare class Node implements NodeLifecycle {
254
371
  signal(name: string): Signal<unknown[]>;
255
372
  on(signal: string, fn: SignalListener<unknown[]>, opts?: {
256
373
  once?: boolean;
374
+ owner?: Node;
257
375
  }): () => void;
258
376
  off(signal: string, fn: SignalListener<unknown[]>): void;
259
377
  /**
378
+ * Wrap a listener so ITS throw stays ITS problem.
379
+ *
380
+ * `Signal.emit` calls listeners directly, so an exception from one unwinds
381
+ * into whoever EMITTED. The node quarantine then blames and disables the
382
+ * emitter's script, and every OTHER listener on that signal is skipped for
383
+ * the rest of the emit. Measured on the same broken handler, two wirings:
384
+ *
385
+ * ```
386
+ * wired with node.on() emitter ticks 1/10 emitter script DISABLED
387
+ * wired with JSON connections emitter ticks 10/10 emitter script fine
388
+ * ```
389
+ *
390
+ * The JSON half has been quarantined since `quarantineWire` shipped, with a
391
+ * comment saying why: *"a behavior is your code; the node is the engine's. A
392
+ * typo in the first must not switch off the second."* `.on()` is the same
393
+ * category — it is the spelling every skill teaches and five shipped examples
394
+ * use, including `controller.on('movementStateChanged', …)`, where blaming
395
+ * the emitter stops the player moving entirely. That is verbatim the defect
396
+ * `quarantineWire` was written to prevent, reached through the other door.
397
+ *
398
+ * The owner node is NOT marked `errored`: its own update is fine, only this
399
+ * wire is off. That is exactly what `quarantineWire` does, and marking it
400
+ * would restore the over-punishment this removes.
401
+ */
402
+ private quarantined;
403
+ /**
260
404
  * How many listeners this node has on `signal` — 0 for one nobody watches.
261
405
  *
262
406
  * The engine's own leak test reads it, and so can a game: a handler connected
@@ -270,8 +414,42 @@ declare class Node implements NodeLifecycle {
270
414
  * SceneTree). Frees immediately when detached from any tree.
271
415
  */
272
416
  queueFree(): void;
417
+ /**
418
+ * Has this node been torn down?
419
+ *
420
+ * A scene swap frees the tree it replaces, so the answer is `true` for every
421
+ * node of the scene you just left — which is exactly the state a behavior is
422
+ * in when it runs one more line after `goToScene`. Kept as a flag rather than
423
+ * inferred from `parent`/`tree`, because a DETACHED node (a prefab on a
424
+ * shelf, a clone waiting to be added) has neither and is perfectly alive.
425
+ */
426
+ get freed(): boolean;
427
+ private _freed;
273
428
  /** Immediately detach and tear down this node and its children. */
274
429
  free(): void;
430
+ /**
431
+ * Run a structural lifecycle hook, quarantining it the way `update` is.
432
+ *
433
+ * Only `update`/`fixedUpdate` were ever quarantined, so a throw from a
434
+ * RUNTIME-created node's `onReady`/`onEnterTree` unwound into the `update()`
435
+ * of whatever created it. Measured on one Spawner scene, 120 steps:
436
+ *
437
+ * control A spawner alone nodes 20, errors 0
438
+ * control B clone throws in UPDATE nodes 20, errors 19, each line
439
+ * correctly named on the clone
440
+ * A + B clone throws in onReady nodes 2, errors 1,
441
+ * log: "behavior 'Spawner' on /World/Spawner threw in update"
442
+ *
443
+ * The innocent Spawner was quarantined — spawning stopped permanently — and
444
+ * the one headline named the wrong node, the wrong script and the wrong hook.
445
+ * An `onExitTree` throw on a runtime `queueFree` was worse still: it escaped
446
+ * `engine.step()` entirely, with `stats().errors: 0` and an EMPTY log,
447
+ * because the free-queue flush is outside behaviour propagation.
448
+ *
449
+ * During the initial load the throw is re-raised: `loadScene` is where an
450
+ * authoring mistake has to hard-fail, and that is stated design.
451
+ */
452
+ private _runHook;
275
453
  /** @internal */
276
454
  _propagateEnterTree(tree: SceneTree): void;
277
455
  /** @internal */
@@ -350,6 +528,12 @@ interface AudioEvent {
350
528
  name: string;
351
529
  /** Node path for a node-driven sound; `engine.music` / `engine.sfx` otherwise. */
352
530
  from: string;
531
+ /**
532
+ * The AUTHORED node this stands for — `from` with a clone's auto-renamed
533
+ * ancestors put back (`/Game/Foe37/Snd` → `/Game/Foe/Snd`). Absent = same as
534
+ * `from`. Only `sources()` reads it; the log keeps the live path.
535
+ */
536
+ wiredAs?: string;
353
537
  bus: BusName;
354
538
  /** `engine.time` when it fired, in seconds. */
355
539
  at: number;
@@ -379,10 +563,25 @@ declare class AudioBuses {
379
563
  * Bounded (200) — a game running for an hour must not grow a log.
380
564
  */
381
565
  private readonly events;
566
+ /**
567
+ * EVERY node that has sounded, ever — a set of paths, not a window.
568
+ *
569
+ * `recent()` is bounded at 200, which is right for a log and wrong for the
570
+ * question "is this AudioPlayer wired?". A game with one sound firing about
571
+ * three times a second overflows the window inside a minute, so a 60-second
572
+ * playtest read the tail and reported correctly-wired feedback as never
573
+ * fired. This grows with the SCENE, not with the run — which is true only
574
+ * because it records the AUTHORED path: a clone's own path is new every
575
+ * spawn, and recording those grew the set by one entry per enemy, forever,
576
+ * in every shipped game. See `Node.wiringPath`.
577
+ */
578
+ private readonly sourcePaths;
382
579
  /** @internal Called by the players; games read `recent()`. */
383
580
  record(event: AudioEvent): void;
384
581
  /** The last `limit` sounds, oldest first. */
385
582
  recent(limit?: number): AudioEvent[];
583
+ /** Every node path that has sounded since the last `clearLog()`. */
584
+ sources(): string[];
386
585
  /** How many times a preset/clip/track sounded — the assertion you want. */
387
586
  countOf(name: string): number;
388
587
  /** Forget what has sounded so far (between scenes, or between assertions). */
@@ -391,6 +590,7 @@ declare class AudioBuses {
391
590
  private _sfx;
392
591
  private _music;
393
592
  private _muted;
593
+ private _suspended;
394
594
  get master(): number;
395
595
  set master(v: number);
396
596
  get sfx(): number;
@@ -399,6 +599,21 @@ declare class AudioBuses {
399
599
  set music(v: number);
400
600
  get muted(): boolean;
401
601
  set muted(v: boolean);
602
+ /**
603
+ * Silence the game WITHOUT it counting as the player muting it.
604
+ *
605
+ * `pause-when-hidden` set `muted = true` when the tab went away, which fires
606
+ * `changed`, which `Settings.bindAudio` persists. Close the tab while it is
607
+ * hidden and `muted: true` is on disk: EVERY later session of that game boots
608
+ * silent, and no shipped starter has an in-game way back.
609
+ *
610
+ * A suspension is the engine's, not the player's. It never touches `muted`,
611
+ * so nothing persists it and unsuspending cannot turn the sound back on for
612
+ * someone who turned it off.
613
+ */
614
+ suspend(on: boolean): void;
615
+ /** Is the engine holding the sound off (hidden tab), as against the player? */
616
+ get suspended(): boolean;
402
617
  /** Final gain for a sound on `bus` with its own `sourceVolume` (all clamped). */
403
618
  effectiveVolume(bus: BusName, sourceVolume: number): number;
404
619
  /** Clamp + finite-guard a new value; emit only on a real change. */
@@ -434,6 +649,14 @@ interface MusicTrack {
434
649
  * Optional so an existing custom backend keeps compiling.
435
650
  */
436
651
  playhead?(): number | null;
652
+ /**
653
+ * Advance a track that has no clock of its own, by the engine's real dt.
654
+ *
655
+ * Optional, and the WebAudio backend does not implement it: an element's own
656
+ * `currentTime` is the authority whenever there is one. The silent backend
657
+ * does, so a game charted to a soundtrack has a playhead in the verify VM.
658
+ */
659
+ tick?(dt: number): void;
437
660
  }
438
661
  /** Backend that mints {@link MusicTrack}s — WebAudio-backed in the browser. */
439
662
  interface MusicBackend {
@@ -792,10 +1015,24 @@ declare class SfxEngine {
792
1015
  * The AUDIO clock, in seconds — the one a sound is actually scheduled
793
1016
  * against, and the only clock that does not drift relative to what you hear.
794
1017
  *
795
- * `0` headless and before the first sound, since there is no context yet.
796
1018
  * Schedule with a lead: `sfx.play(params, 1, { when: sfx.now + 0.08 })`.
1019
+ *
1020
+ * With no AudioContext — headless, and before the first sound — it is the
1021
+ * engine's own real elapsed time instead of a frozen `0`. A constant made the
1022
+ * documented lookahead scheduler queue the notes inside the first 80 ms and
1023
+ * then nothing ever again: measured on a 21-beat metronome in the verify VM,
1024
+ * **1 of 21 beats**, with no error and no warning. The silent clock runs so
1025
+ * the schedule is reachable; nothing sounds either way.
797
1026
  */
798
1027
  get now(): number;
1028
+ /** The clock `now` reports when there is no context to ask. */
1029
+ private silentNow;
1030
+ /**
1031
+ * @internal Advance the silent clock — the engine calls this every frame with
1032
+ * the REAL dt, beside `music.tick`, because the audio does not slow down when
1033
+ * the game does.
1034
+ */
1035
+ _tick(dt: number): void;
799
1036
  /**
800
1037
  * Play a preset at `gain` (0..1, already bus-multiplied by the caller). Each
801
1038
  * call spawns its own source so rapid repeats overlap instead of cutting off.
@@ -836,6 +1073,12 @@ interface EffectEvent {
836
1073
  name: string;
837
1074
  /** Node path, or `engine` for the screen-wide ones. */
838
1075
  from: string;
1076
+ /**
1077
+ * The AUTHORED node this stands for — `from` with a clone's auto-renamed
1078
+ * ancestors put back (`/Game/Foe37/Fx` → `/Game/Foe/Fx`). Absent = same as
1079
+ * `from`. Only `sources()` reads it; the log keeps the live path.
1080
+ */
1081
+ wiredAs?: string;
839
1082
  /** Particles in the burst, shake magnitude, seconds frozen — the amount. */
840
1083
  amount: number;
841
1084
  /** `engine.time` when it fired, in seconds. */
@@ -843,8 +1086,19 @@ interface EffectEvent {
843
1086
  }
844
1087
  declare class EffectLog {
845
1088
  private readonly events;
1089
+ /**
1090
+ * EVERY node that has fired, ever — a set of paths, not a window. See
1091
+ * `AudioBuses.sources`: 200 entries is a log, and "is this emitter wired?"
1092
+ * needs the whole run.
1093
+ *
1094
+ * AUTHORED paths, so it is bounded by the scene: a wave of forty enemies is
1095
+ * one emitter fired forty times. See `Node.wiringPath`.
1096
+ */
1097
+ private readonly sourcePaths;
846
1098
  /** @internal Called by the effects themselves; games read `recent()`. */
847
1099
  record(event: EffectEvent): void;
1100
+ /** Every node path that has fired an effect since the last `clearLog()`. */
1101
+ sources(): string[];
848
1102
  /** The last `limit` effects, oldest first. */
849
1103
  recent(limit?: number): EffectEvent[];
850
1104
  /** How many times an effect with this name fired (`explosion`, `#ff0000`). */
@@ -856,18 +1110,24 @@ declare class EffectLog {
856
1110
  }
857
1111
  //#endregion
858
1112
  //#region src/core/input.d.ts
859
- /**
860
- * Declarative input from scene JSON `input{}` — DOM-free logic (feed key state
861
- * via `handleKey`, or wire a browser with `attachKeyboard(window)`).
862
- *
863
- * Convention: vector2 follows the 2D y-down space (up = -y). `justPressed` /
864
- * `justReleased` are one-frame edges, settled by the Engine each tick.
865
- */
866
1113
  declare class InputMap {
867
1114
  private readonly actions;
868
1115
  private readonly down;
869
1116
  private readonly pressedEdge;
870
1117
  private readonly releasedEdge;
1118
+ private readonly pressedEdgeFixed;
1119
+ private readonly releasedEdgeFixed;
1120
+ /** Which pass is asking. Set by the Engine around each phase. */
1121
+ private phase;
1122
+ /**
1123
+ * Actions something has actually ASKED about since this scene loaded.
1124
+ *
1125
+ * Recorded in the four read methods only — never in `get`/`button`, because
1126
+ * `pressAction` goes through those, and the tool that reports this list is
1127
+ * the same tool that injects presses. Marking an action read because a
1128
+ * playtester pressed it would make the check answer its own question.
1129
+ */
1130
+ private readonly queried;
871
1131
  private detach;
872
1132
  /** Load (or extend with) scene-JSON action declarations. Hard-validates shape. */
873
1133
  declare(decls: Record<string, unknown>): void;
@@ -878,7 +1138,9 @@ declare class InputMap {
878
1138
  * This is what a click-driven game asks for. `pointerDelta()` answers "how far
879
1139
  * did the mouse move" — the mouse-look question — and for a whole class of
880
1140
  * genres that is the wrong question and the only one the engine could answer.
881
- * Pair it with `renderer.pick(x, y)` to get the node under the cursor.
1141
+ * Pair it with `engine.pickAt(x, y)` for the node under the cursor and
1142
+ * `engine.pointerWorld()` for the place — both reachable from a Behavior,
1143
+ * which `renderer.pick` / `renderer.worldFromScreen` are not.
882
1144
  */
883
1145
  pointerPosition(): {
884
1146
  x: number;
@@ -895,6 +1157,9 @@ declare class InputMap {
895
1157
  private readonly injectedDown;
896
1158
  private readonly injectedPressed;
897
1159
  private readonly injectedReleased;
1160
+ /** The fixed pass's view of the injected edges — see `pressedEdgeFixed`. */
1161
+ private readonly injectedPressedFixed;
1162
+ private readonly injectedReleasedFixed;
898
1163
  private readonly injectedPressedNext;
899
1164
  private readonly injectedReleasedNext;
900
1165
  /** True between the start of a frame's update pass and `endFrame()`. */
@@ -916,6 +1181,20 @@ declare class InputMap {
916
1181
  name: string;
917
1182
  type: "button" | "vector2";
918
1183
  }>;
1184
+ /**
1185
+ * Declared actions NOTHING has read — a control the scene promises and no
1186
+ * code implements.
1187
+ *
1188
+ * `village-quest-3d` declared `"restart": { "keys": ["KeyR"] }` and not one
1189
+ * line read it, so the game's own control list said R restarts and R did
1190
+ * nothing. The scene is where controls are DECLARED; whether anything
1191
+ * consumes one is invisible there, and a `grep` for the name cannot tell an
1192
+ * engine node's read from a dead string.
1193
+ *
1194
+ * The list is advisory, like `behaviorsWithoutSave`: an action polled only in
1195
+ * a state a run never reached is on it honestly. Read it, do not gate on it.
1196
+ */
1197
+ unreadActions(): string[];
919
1198
  pressAction(action: string): void;
920
1199
  /** Release an injected button action (yields one justReleased frame). */
921
1200
  releaseAction(action: string): void;
@@ -940,7 +1219,21 @@ declare class InputMap {
940
1219
  private dy;
941
1220
  private wheel;
942
1221
  /** Mouse buttons feed the same code space as keys: Mouse0/Mouse1/Mouse2. */
943
- handleMouseButton(button: number, isDown: boolean): void;
1222
+ /**
1223
+ * A mouse/pen/finger button.
1224
+ *
1225
+ * `kind` matters on the way UP: a finger that lifts CEASES TO EXIST, while a
1226
+ * cursor stays where you left it. Without it the last-tapped node kept
1227
+ * `hovering: true` for the rest of the session — a permanently highlighted
1228
+ * tile, mole or card on every phone, on a device with no hover at all.
1229
+ *
1230
+ * The position is cleared at `endFrame`, not here: `Clickable` fires on the
1231
+ * release over the node the press started on, so a pointer cleared in the
1232
+ * same frame takes the click with it.
1233
+ */
1234
+ handleMouseButton(button: number, isDown: boolean, kind?: "mouse" | "touch"): void;
1235
+ /** A finger came up this frame — see `endFrame`. */
1236
+ private touchLifted;
944
1237
  /** Accumulate look deltas (movementX/Y under pointer lock, else move deltas). */
945
1238
  handlePointerMove(dx: number, dy: number): void;
946
1239
  handleWheel(deltaY: number): void;
@@ -980,11 +1273,7 @@ declare class InputMap {
980
1273
  * Poll the browser Gamepad API every frame (the first connected pad).
981
1274
  * Wire once at boot: `engine.input.attachGamepad(engine)`. Headless no-op.
982
1275
  */
983
- attachGamepad(engine: {
984
- updated: {
985
- connect(fn: () => void): () => void;
986
- };
987
- }): () => void;
1276
+ attachGamepad(_engine?: unknown): () => void;
988
1277
  /**
989
1278
  * Pointer input from a mouse, a FINGER or a pen — one code path.
990
1279
  *
@@ -1050,9 +1339,38 @@ declare class InputMap {
1050
1339
  };
1051
1340
  /** Consume one-frame edges. The Engine calls this at the end of every tick. */
1052
1341
  endFrame(): void;
1053
- /** @internal The engine marks the update phase, so an edge injected inside
1054
- * it can be held for the next frame. */
1342
+ /**
1343
+ * @internal Start a tick: poll the devices that have no events of their own,
1344
+ * then open the frame.
1345
+ *
1346
+ * The gamepad used to be polled from an `engine.updated` handler — AFTER both
1347
+ * passes, and nine lines before `endFrame()` cleared what it had just
1348
+ * produced. So a pad's press edge was created and destroyed inside one tick
1349
+ * and `justPressed('jump')` was never true anywhere in the tree:
1350
+ *
1351
+ * PAD 8 taps -> update() saw justPressed 0, justReleased 0, isPressed 32
1352
+ * KEY 8 taps -> update() saw justPressed 8, justReleased 8
1353
+ *
1354
+ * A controller could hold but could never press: 0.00 px of jump on Pad0
1355
+ * against 74.33 px on Space, and `HudLayer.focusNavigation` — the built-in
1356
+ * controller-menu feature — moved focus zero times. Polling HERE puts a pad
1357
+ * where a keyboard already is: an edge that exists before the frame opens.
1358
+ */
1055
1359
  beginFrame(): void;
1360
+ /** Devices with no events of their own, polled at the start of every tick. */
1361
+ private readonly pollers;
1362
+ /**
1363
+ * @internal The fixed pass is starting. Edges it has not seen yet are live.
1364
+ */
1365
+ beginFixedStep(): void;
1366
+ /**
1367
+ * @internal The fixed pass is done for this tick — it has now seen the edges.
1368
+ *
1369
+ * Called after the FIRST fixed step only, so an edge reaches `fixedUpdate`
1370
+ * exactly once however many steps a tick runs. A tick that runs none leaves
1371
+ * this view untouched and the next one delivers it.
1372
+ */
1373
+ endFixedStep(): void;
1056
1374
  /**
1057
1375
  * Drop all action declarations and injected action state (physical key
1058
1376
  * state is kept). The Engine calls this on setScene so keybinds never
@@ -1190,6 +1508,19 @@ interface RayHit {
1190
1508
  distance: number;
1191
1509
  normal: number[];
1192
1510
  node: Node | null;
1511
+ /**
1512
+ * WHERE the ray landed, in world units — `origin + dir * distance`.
1513
+ *
1514
+ * The thing every caller actually wants: an impact effect, a decal, a tower
1515
+ * dropped on uneven ground, a waypoint. `incanto-web-integration.md` sends
1516
+ * you here for exactly that case — "`worldFromScreen` lands it on the ground
1517
+ * plane, and `rayFromScreen` hands you the ray when the ground is terrain or
1518
+ * a stack of crates" — and then the hit carried a distance and left the
1519
+ * arithmetic, and the normalization it depends on, to the caller.
1520
+ *
1521
+ * 2 numbers in 2D (pixels), 3 in 3D (metres), like every other position.
1522
+ */
1523
+ point: number[];
1193
1524
  }
1194
1525
  /**
1195
1526
  * The physics world, as core knows it — the QUERIES a behavior asks, with no
@@ -1208,6 +1539,10 @@ interface PhysicsQuery {
1208
1539
  /**
1209
1540
  * Fire a ray and report the first thing it hits, or `null`.
1210
1541
  *
1542
+ * `dir` may be any length — `target - eye` is the usual spelling — and is
1543
+ * normalized, so `distance` and `maxLen` are both in world units (px / m)
1544
+ * rather than multiples of `dir`. A zero-length direction returns `null`.
1545
+ *
1211
1546
  * Sensors never block a ray. Pass `exclude` for the body you are casting from
1212
1547
  * — a ray starting inside its own collider hits itself at distance 0.
1213
1548
  */
@@ -1215,16 +1550,36 @@ interface PhysicsQuery {
1215
1550
  staticOnly?: boolean;
1216
1551
  }): RayHit | null;
1217
1552
  /**
1218
- * A THICK ray (sphere sweep) 3D only, hence optional. Use it where skimming
1219
- * matters: a thin ray that passes 10 cm over a wall reports "clear" while the
1220
- * wall still fills the frame.
1553
+ * A THICK ray — a sphere sweep in 3D, a circle sweep in 2D.
1554
+ *
1555
+ * Use it where skimming matters: a thin ray that passes 10 cm over a wall
1556
+ * reports "clear" while the wall still fills the frame, and in 2D a shot can
1557
+ * thread a one-pixel gap between two floor tiles. Same filters and the same
1558
+ * direction contract as `castRay`; `distance` is the travel of the CENTRE.
1559
+ *
1560
+ * It was 3D-only, and therefore optional here, until 0.70.
1221
1561
  */
1222
- castSphere?(origin: readonly number[], dir: readonly number[], radius: number, maxLen: number, exclude?: Node, opts?: {
1562
+ castSphere(origin: readonly number[], dir: readonly number[], radius: number, maxLen: number, exclude?: Node, opts?: {
1223
1563
  staticOnly?: boolean;
1224
1564
  }): {
1225
1565
  distance: number;
1226
1566
  node: Node | null;
1227
1567
  } | null;
1568
+ /**
1569
+ * How fast this body is actually moving, from the SOLVER.
1570
+ *
1571
+ * 2D answers in px/s (y-down), 3D in m/s (y-up). `linearVelocity` on the node
1572
+ * is written back once per step, so a behavior reading it from inside
1573
+ * `fixedUpdate` — after a collision, before the write-back — sees the value
1574
+ * from BEFORE the impact, which is exactly when a game wants to know how hard
1575
+ * it hit something.
1576
+ */
1577
+ velocityOf(node: Node): number[];
1578
+ /**
1579
+ * Mass the solver actually uses — collider-derived unless the body overrides
1580
+ * it. Scale a push by this rather than by the authored `mass` prop.
1581
+ */
1582
+ massOf(node: Node): number;
1228
1583
  /** Tear the world down and detach from the engine. */
1229
1584
  dispose(): void;
1230
1585
  }
@@ -1246,6 +1601,26 @@ interface SaveStore {
1246
1601
  /** Wipe THIS namespace only. */
1247
1602
  clear(): void;
1248
1603
  /**
1604
+ * Every key this namespace holds.
1605
+ *
1606
+ * Optional, so a store a game injected itself keeps compiling. Without it,
1607
+ * the slot layer could only find saves through its own INDEX — and one
1608
+ * corrupt byte in that index made every intact save on the machine
1609
+ * unreachable and then orphaned them for good on the next write. A store
1610
+ * that can be enumerated can be rebuilt.
1611
+ */
1612
+ keys?(): string[];
1613
+ /**
1614
+ * Keys whose stored text is not JSON any more.
1615
+ *
1616
+ * `parse` swallows a syntax error and answers with the fallback — the
1617
+ * RECOVERY is deliberate ("the fallback beats a crash"), the silence was not.
1618
+ * A truncated slot from a power cut is indistinguishable from a slot that was
1619
+ * never written, and a load menu that shows one fewer save than the player
1620
+ * remembers is the worst thing a save system can do quietly.
1621
+ */
1622
+ corrupt?(): string[];
1623
+ /**
1249
1624
  * Does a write here survive the page closing?
1250
1625
  *
1251
1626
  * `false` means this store is a Map that lives as long as the tab does — a
@@ -1304,6 +1679,46 @@ interface RestoreReport {
1304
1679
  expected: number;
1305
1680
  /** Authored nodes the save says the run consumed, and that were freed again. */
1306
1681
  freed: number;
1682
+ /**
1683
+ * Behaviors whose `deserialize` THREW, and what it said.
1684
+ *
1685
+ * They are in `skipped` too, because they took nothing — but a save this
1686
+ * build cannot read and a node that has no `deserialize` at all are different
1687
+ * problems, and `skipped` alone said both.
1688
+ */
1689
+ refused: string[];
1690
+ /**
1691
+ * Behaviors that took their state and then threw out of `announce()`.
1692
+ *
1693
+ * The state IS restored; the SCREEN was not told. That is the difference
1694
+ * between a correct save and a HUD showing zeros over a 1400-coin run, and it
1695
+ * used to be a bare `catch {}` — `restored: 2, expected: 2`, no log, no
1696
+ * error.
1697
+ */
1698
+ unannounced: string[];
1699
+ /**
1700
+ * Authored nodes THIS tree already consumed that the save says should still
1701
+ * be there — the tell that the scene was PLAYED before the restore.
1702
+ *
1703
+ * `#freed` can take nodes away and nothing puts them back, so restoring into
1704
+ * a live scene (a pause-menu Load, a slot menu, the canonical
1705
+ * death→reload-checkpoint wiring) leaves this run's collectibles deleted and
1706
+ * reports fully green. Measured on 3 gems worth 10 with `scoreToWin: 30`:
1707
+ *
1708
+ * ```
1709
+ * CONTROL restartScene then restore → score 10, gems [Gem2,Gem3] → 30, won TRUE
1710
+ * death wired straight to restore → score 10, gems [Gem3] → 20, won FALSE
1711
+ * ```
1712
+ *
1713
+ * …and the next autosave writes the hybrid back to the slot, so the
1714
+ * unwinnable run survives a page reload. Every other field is identical
1715
+ * between the two, which is why nothing could tell them apart.
1716
+ *
1717
+ * A node that freed ITSELF on a timer between load and restore lands here
1718
+ * too, which is why this is reported rather than thrown: the caller knows
1719
+ * which of its nodes are transient and the engine does not.
1720
+ */
1721
+ stale: string[];
1307
1722
  }
1308
1723
  /**
1309
1724
  * Every behavior in the tree that has something to say, by uid.
@@ -1316,24 +1731,14 @@ interface RestoreReport {
1316
1731
  * cannot be given back. The editor assigns a uid to every node it touches; a
1317
1732
  * hand-written scene may not have.
1318
1733
  */
1319
- declare function captureBehaviors(root: Node, source?: JsonValue): {
1734
+ declare function captureBehaviors(root: Node, source?: JsonValue, authored?: ReadonlySet<string>): {
1320
1735
  state: BehaviorState; /** Paths of nodes that had state to save and no uid to save it under. */
1321
1736
  unaddressable: string[]; /** Behaviors in this tree that have something to save — addressable or not. */
1322
1737
  saveable: number; /** Authored nodes this run consumed that the `#freed` ledger cannot record. */
1323
- unrecordable: string[];
1738
+ unrecordable: string[]; /** Behaviors whose `serialize()` THREW — their state is not in `state`. */
1739
+ failed: string[];
1324
1740
  };
1325
- /**
1326
- * Hand each behavior its state back.
1327
- *
1328
- * Call this AFTER the scene has loaded and `onReady` has run: `onReady` is where
1329
- * a behavior sets its starting values, so restoring first would be overwritten
1330
- * by the fresh start.
1331
- *
1332
- * Never throws on a mismatch. A save from an older build of the game will name
1333
- * uids that no longer exist, and refusing to load it would mean a patch that
1334
- * moves one node deletes everyone's progress. It reports instead.
1335
- */
1336
- declare function restoreBehaviors(root: Node, state: BehaviorState): RestoreReport;
1741
+ declare function restoreBehaviors(root: Node, state: BehaviorState, authored?: ReadonlySet<string>): RestoreReport;
1337
1742
  /**
1338
1743
  * The slot layer: a named list of saves over the existing store.
1339
1744
  *
@@ -1352,8 +1757,32 @@ declare class SaveSlots {
1352
1757
  * be there tomorrow.
1353
1758
  */
1354
1759
  get persistent(): boolean;
1760
+ /**
1761
+ * The slot index — rebuilt from the store when it cannot be trusted.
1762
+ *
1763
+ * `store.get(INDEX_KEY, [])` falls back to an EMPTY LIST when the stored text
1764
+ * is not JSON, so one corrupt byte in a derived index emptied the whole load
1765
+ * menu while every save on the machine was still perfectly readable — and the
1766
+ * next `write()` then set the index to `[newId]`, orphaning them for good.
1767
+ * The index is a cache of what the store holds, and the store can be asked.
1768
+ */
1769
+ private ids;
1355
1770
  /** Slot ids that have a save, newest first. */
1356
1771
  list(): string[];
1772
+ /**
1773
+ * Slots that EXIST and cannot be read — a truncated write, or a save from a
1774
+ * build that predates this one.
1775
+ *
1776
+ * `read()` answers `null` for both of those and for "no such slot", and
1777
+ * `list()` filters them out, so a save the player made silently disappeared
1778
+ * from the menu. A load screen should render "1 save could not be read"
1779
+ * rather than one fewer row, which is why this is a list and not a warning.
1780
+ */
1781
+ problems(): Array<{
1782
+ id: string;
1783
+ why: "corrupt" | "unreadable";
1784
+ }>;
1785
+ private isCorrupt;
1357
1786
  /** Every slot with its contents — what a load menu actually renders. */
1358
1787
  all(): Array<SaveSlot & {
1359
1788
  id: string;
@@ -1432,8 +1861,32 @@ declare class Scene {
1432
1861
  /** The full source JSON this scene was loaded from — `restartScene` fuel. */
1433
1862
  readonly source: SceneJson;
1434
1863
  constructor(source: SceneJson, root: Node, tree: SceneTree);
1864
+ /**
1865
+ * Every uid this scene had the moment it finished loading.
1866
+ *
1867
+ * The `#freed` ledger's left-hand side: a uid in here that is not in the live
1868
+ * tree is a node this run consumed. Includes nodes inside expanded
1869
+ * `instance:` sub-scenes, which the scene FILE cannot show.
1870
+ */
1871
+ readonly authoredUids: ReadonlySet<string>;
1435
1872
  /** Lossless export. Note: expanded sub-scene instances serialize as full trees in M1. */
1436
1873
  toJSON(): SceneJson;
1874
+ /**
1875
+ * The connections whose BOTH ENDS still exist in the tree being written.
1876
+ *
1877
+ * `toJSON` is what `incanto-play`'s `capture` writes — "your screenshot", the
1878
+ * skills call it — and it wrote every connection the file declared, including
1879
+ * ones pointing at nodes the run had freed. The result was a scene the engine
1880
+ * REFUSES to load:
1881
+ *
1882
+ * DANGLING_CONNECTION: connection to 'Coin' resolves nowhere
1883
+ *
1884
+ * so a captured game with any collectible in it produced a file no tool could
1885
+ * read back. Dropping a wire whose node is gone is what the tree already did
1886
+ * when it freed the node; this makes the written file say the same thing.
1887
+ */
1888
+ private liveConnections;
1889
+ private resolves;
1437
1890
  }
1438
1891
  //#endregion
1439
1892
  //#region src/core/settings.d.ts
@@ -1817,6 +2270,53 @@ declare class Engine {
1817
2270
  * is a hitch, where the alternative was a game that never simulated at all.
1818
2271
  */
1819
2272
  ensurePhysics(): Promise<void>;
2273
+ /**
2274
+ * The seed this engine was built with — game logic draws from `rng`, and
2275
+ * anything DECORATIVE derives its own stream from this instead.
2276
+ *
2277
+ * A particle emitter used to draw from `rng`, and it spawns a number of
2278
+ * particles that depends on `dt`. Measured on the same scene and seed, the
2279
+ * same simulated two seconds, comparing the world's own draws by update
2280
+ * index:
2281
+ *
2282
+ * ```
2283
+ * particles off: 60fps and 30fps identical
2284
+ * particles ON : they diverge at update 2
2285
+ * ```
2286
+ *
2287
+ * So a purely visual effect changed the game's random decisions, and the
2288
+ * PLAYER'S FRAME RATE decided them. Derived streams stay reproducible for
2289
+ * replays and screenshots without spending the one the game reads.
2290
+ */
2291
+ readonly seed: number;
2292
+ /**
2293
+ * How to resolve an `instance:` sub-scene, remembered from the boot load.
2294
+ *
2295
+ * `loadScene(json, { engine, resolveScene })` records it here, so the paths
2296
+ * that RELOAD — `restartScene`, `goToScene`, a game-over restart key — can
2297
+ * pass it back. Without that a game using the documented sub-scenes booted
2298
+ * fine and threw `UNRESOLVED_INSTANCE` on its first restart.
2299
+ */
2300
+ resolveScene?: (path: string) => SceneJson;
2301
+ /**
2302
+ * How the current scene was LOADED, for the rebuilds that happen later.
2303
+ *
2304
+ * `duplicateNode` — every `Spawner`, every `NetworkSpawner`, every clone a
2305
+ * game makes — rebuilds a subtree from scene JSON long after `loadScene`
2306
+ * returned, and it did so with no options at all. Under
2307
+ * `stubMissingBehaviors` (how EVERY headless check runs a game whose scripts
2308
+ * are TypeScript it cannot import) the first spawn met the real registry and
2309
+ * threw `UNKNOWN_BEHAVIOR` out of `update`, disabling the spawner for the
2310
+ * rest of the run — a wave game reported with no waves and nothing saying
2311
+ * why.
2312
+ *
2313
+ * Recorded here for the same reason as `resolveScene` above: the engine
2314
+ * outlives the call, and the rebuild paths can reach it.
2315
+ */
2316
+ loadOptions: {
2317
+ stubMissingBehaviors?: boolean;
2318
+ stubAllBehaviors?: boolean;
2319
+ };
1820
2320
  /** Seeded randomness for game logic (deterministic when `seed` is set). */
1821
2321
  readonly rng: Rng;
1822
2322
  /** The engine log channel (debug overlay + headless harness tail this). */
@@ -1831,6 +2331,60 @@ declare class Engine {
1831
2331
  * and cleared on dispose; null when nothing renders (headless).
1832
2332
  */
1833
2333
  picker: ((x: number, y: number) => Node | null) | null;
2334
+ /**
2335
+ * Screen pixel → a point in the WORLD, when a renderer has offered one.
2336
+ *
2337
+ * `picker` answers "which node"; this answers "which place", and a mouse game
2338
+ * needs both — drag-and-launch, drop a tower on the map, steer a cursor unit,
2339
+ * draw the slingshot band. The conversion has always existed
2340
+ * (`Renderer2D.worldFromScreen` / `Renderer3D.worldFromScreen`) and lived
2341
+ * only on the renderer, so a Behavior — which holds a node and an engine and
2342
+ * nothing else — could not call it. Its two workarounds were both forbidden:
2343
+ * stash the renderer in a module global, or hand-roll the projection the 2D
2344
+ * skill says in so many words not to hand-roll.
2345
+ *
2346
+ * The answer is the scene's own units and shape: `[x, y]` in 2D (design
2347
+ * pixels), `[x, y, z]` on the ground plane in 3D — the same array a node's
2348
+ * `position` is, so it goes straight back into one.
2349
+ *
2350
+ * Instance-scoped, set by the renderer on construction and cleared on
2351
+ * dispose. Headless a harness may install a geometric one; null when nothing
2352
+ * has.
2353
+ */
2354
+ toWorld: ((x: number, y: number) => number[] | null) | null;
2355
+ /**
2356
+ * A world point → the screen pixel drawing it. The inverse of `toWorld`, for
2357
+ * pinning DOM to a node or placing a marker over one.
2358
+ *
2359
+ * `behind` is true when the point is behind the camera (3D); a 2D renderer
2360
+ * always reports false.
2361
+ */
2362
+ toScreen: ((world: readonly number[]) => {
2363
+ x: number;
2364
+ y: number;
2365
+ behind: boolean;
2366
+ } | null) | null;
2367
+ /**
2368
+ * Where the cursor is in the world, or null.
2369
+ *
2370
+ * The two-line dance every mouse game writes — read the pointer, convert it —
2371
+ * with the two ways it goes wrong already handled: a pointer nothing has
2372
+ * moved yet is null rather than the origin, and no converter is null rather
2373
+ * than a guess.
2374
+ */
2375
+ /**
2376
+ * Screen pixel → a ray into the world, when a renderer has offered one.
2377
+ *
2378
+ * `toWorld` lands on a plane; this is the general form, and it is what a shot
2379
+ * aimed at what the cursor is OVER needs: feed it straight to
2380
+ * `engine.physics.castRay(origin, dir, …)`. 3D only — a 2D renderer installs
2381
+ * nothing here, since a 2D world has no depth to cast into.
2382
+ */
2383
+ screenRay: ((x: number, y: number) => {
2384
+ origin: number[];
2385
+ dir: number[];
2386
+ } | null) | null;
2387
+ pointerWorld(): number[] | null;
1834
2388
  /** Frames the engine has stepped — monotonic, and the pick cache's key. */
1835
2389
  frameNumber: number;
1836
2390
  /** @internal frame number + answer, so N Clickables cost ONE raycast. */
@@ -1911,7 +2465,24 @@ declare class Engine {
1911
2465
  * (hit-stop / pause). Scales BOTH variable and fixed updates — physics,
1912
2466
  * timers, behaviors all breathe together. See `gameplay` `hitStop()`.
1913
2467
  */
1914
- timeScale: number;
2468
+ get timeScale(): number;
2469
+ set timeScale(v: number);
2470
+ private _timeScale;
2471
+ private _timeScaleWrites;
2472
+ /**
2473
+ * How many times anyone has written `timeScale` — the clock's ownership tag.
2474
+ *
2475
+ * A temporary freeze cannot tell its own 0 from somebody else's by VALUE.
2476
+ * `hitStop(engine)` then `flow.gameOver()` is the ordinary killing blow, and
2477
+ * both set 0; the freeze then wrote its captured scale back five frames later
2478
+ * and the world started moving again behind a sticky GAME OVER banner —
2479
+ * enemies walking, timers running, the player watching their corpse get shot.
2480
+ *
2481
+ * A counter answers the question the value cannot: *has anyone touched this
2482
+ * since I did?* Cheap (one increment per write, and writes are rare) and it
2483
+ * needs no registry of who holds what.
2484
+ */
2485
+ get timeScaleWrites(): number;
1915
2486
  /**
1916
2487
  * The node the dev overlay has selected (renderers draw its bounding box
1917
2488
  * as an orange outline in the game view so you can SEE what you picked).
@@ -1930,6 +2501,29 @@ declare class Engine {
1930
2501
  /** Elapsed REAL time in seconds since the scene started (ignores timeScale) —
1931
2502
  * UI animations that must keep moving during slow-mo/pause read this. */
1932
2503
  get unscaledTime(): number;
2504
+ /**
2505
+ * THIS frame's dt in real seconds — the one a paused game still gets.
2506
+ *
2507
+ * `update(dt)` receives the SCALED dt, which is right for anything that is
2508
+ * part of the simulation and wrong for everything that is a presentation of
2509
+ * it. At `timeScale = 0` — which is what `GameFlow.pause()` and, by default,
2510
+ * `gameOver()` set — a scaled countdown does not count:
2511
+ *
2512
+ * a timed UiBanner still up when the player dies never expires, so the
2513
+ * flow's own sticky GAME OVER banner is queued behind it and NEVER
2514
+ * PAINTED — the ending screen shows the last wave toast, forever
2515
+ * a live CameraShake never settles: `t -= dt` is a no-op while the rest of
2516
+ * update() re-rolls a full-magnitude offset every rendered frame, and
2517
+ * with FollowCamera's smoothing that is an unbounded random walk
2518
+ * a remote player's interpolation stops, so everyone else is a statue while
2519
+ * their real positions keep arriving
2520
+ *
2521
+ * `hitStop`, `AudioPlayer` and `music.tick` already use the real clock for
2522
+ * exactly this reason. This is the same clock, per frame, so a countdown can
2523
+ * use it without reading `unscaledTime` twice and subtracting.
2524
+ */
2525
+ get unscaledDelta(): number;
2526
+ private _unscaledDelta;
1933
2527
  constructor(opts?: EngineOptions);
1934
2528
  get scene(): Scene | null;
1935
2529
  /** Replace the active scene. The previous scene's root is freed. */
@@ -1970,6 +2564,22 @@ declare class Engine {
1970
2564
  private reportFrameError;
1971
2565
  /** @internal A node quarantined itself; count it for `stats().errors`. */
1972
2566
  _nodeErrored(_node: unknown): void;
2567
+ /**
2568
+ * @internal Something OUTSIDE a frame failed — a replication send that
2569
+ * rejected, a transport that refused. Count it and say it once.
2570
+ *
2571
+ * `stats().errors` is what every harness in the ecosystem ends with, and it
2572
+ * only ever counted things that threw INSIDE a frame. A multiplayer client
2573
+ * whose socket died kept running perfectly on its own screen while every
2574
+ * other player watched it frozen, and `errors: 0` with an empty log said the
2575
+ * game was fine. Repeats are counted and not re-printed, for the reason
2576
+ * `reportFrameError` gives: sixty identical traces a second is how you lose
2577
+ * the first one.
2578
+ */
2579
+ _asyncFailed(what: string, error: unknown): void;
2580
+ private lastAsyncError;
2581
+ /** Re-entrancy guard for `setScene` — an exit hook can now reach the engine. */
2582
+ private swapping;
1973
2583
  private errorCount;
1974
2584
  private lastFrameError;
1975
2585
  /**
@@ -1993,6 +2603,14 @@ declare class Engine {
1993
2603
  * core/save-slots.ts for why a save carries state rather than a tree
1994
2604
  * snapshot.
1995
2605
  */
2606
+ /**
2607
+ * Behaviors whose `serialize()` threw during the LAST `captureState()`.
2608
+ *
2609
+ * A write path that cannot see part of the run must not overwrite the slot
2610
+ * that still can — `SavePoint.save()` reads this and refuses.
2611
+ */
2612
+ get lastCaptureFailures(): readonly string[];
2613
+ private _captureFailures;
1996
2614
  captureState(): BehaviorState;
1997
2615
  /** Give it back, after the scene is loaded and onReady has run. */
1998
2616
  restoreState(state: BehaviorState): RestoreReport;
@@ -2078,6 +2696,14 @@ interface PropRange {
2078
2696
  /** `min` is a bound the value must EXCEED, not reach (`targetHeight > 0`). */
2079
2697
  exclusiveMin?: boolean;
2080
2698
  /**
2699
+ * `max` is a bound the value must stay UNDER, not reach.
2700
+ *
2701
+ * `Camera3D.fov` is the case: 180 is a legal number and a degenerate
2702
+ * frustum — the whole world collapses to a point at screen centre and the
2703
+ * framing instrument still calls it on screen.
2704
+ */
2705
+ exclusiveMax?: boolean;
2706
+ /**
2081
2707
  * Values outside the range that mean something anyway.
2082
2708
  *
2083
2709
  * `metalness: -1` is "keep whatever the GLB authored" — a real value with a
@@ -2094,6 +2720,23 @@ interface PropDef {
2094
2720
  * dropdowns and the generated JSON-Schema enum — keep it exhaustive. */
2095
2721
  options?: readonly string[];
2096
2722
  /**
2723
+ * Every JSON kind this prop accepts, when it accepts more than the default's.
2724
+ * MUST contain the default's own kind — `registerNode` refuses a list that
2725
+ * does not, because that default would be unwritable in a scene.
2726
+ *
2727
+ * A prop typed `boolean | JsonObject` in TypeScript is one kind to the loader:
2728
+ * whatever the default happens to be. `Water3D.underwater` (default `true`)
2729
+ * and `Flowers3D.density` (default `'sparse'`) both wrote a `validateJson`
2730
+ * branch for the other half — checking `underwater.visibility`, budgeting
2731
+ * plants/m² — and neither branch could be reached from a scene, because the
2732
+ * kind gate runs first. Both halves were in the skills; only one loaded.
2733
+ *
2734
+ * When a value's kind is not the default's, the kind gate is all this file
2735
+ * checks: `options`, `range` and the array template describe the DEFAULT's
2736
+ * kind, so the node's own `validateJson` owns the other half's shape.
2737
+ */
2738
+ kinds?: readonly JsonKind[];
2739
+ /**
2097
2740
  * This prop holds a NODE PATH (`%Unique`, `/Absolute/From/Root`, or one
2098
2741
  * relative to the node that owns the prop) resolved with `getNodeOrNull`.
2099
2742
  *
@@ -2135,7 +2778,14 @@ interface PropDef {
2135
2778
  */
2136
2779
  range?: PropRange;
2137
2780
  asset?: {
2138
- /** The `assets{}` entry `type` a `$ref` here must point at. */kind: "texture" | "spritesheet" | "model" | "animation";
2781
+ /** The `assets{}` entry `type` a `$ref` here must point at. */
2782
+ /**
2783
+ * `audio` has no `assets{}` entry type — `AudioPlayer.src` takes a url and
2784
+ * only a url. It is declared anyway so the prop is VISIBLE to everything
2785
+ * that walks asset props (the editor's picker, `incanto-check`'s
2786
+ * missing-file warning), which is the whole point of the spec.
2787
+ */
2788
+ kind: "texture" | "spritesheet" | "model" | "animation" | "audio";
2139
2789
  /**
2140
2790
  * A value that is not a `$ref` is an error.
2141
2791
  *
@@ -2144,6 +2794,26 @@ interface PropDef {
2144
2794
  * GLB (`ModelInstance3D.animation`).
2145
2795
  */
2146
2796
  refOnly?: boolean;
2797
+ /**
2798
+ * A `$ref` here is an error — this prop takes a raw URL.
2799
+ *
2800
+ * The 3D texture props declared no `asset` spec at all, so
2801
+ * `validateAssetRefs` never looked at them: `"texture": "$hero"` — the
2802
+ * spelling the 2D sibling's own error message teaches — was certified valid
2803
+ * at author time and then fetched as a relative path, 404ing at runtime
2804
+ * with a sprite that simply never appears.
2805
+ */
2806
+ urlOnly?: boolean;
2807
+ /**
2808
+ * The prop is an OBJECT whose values are the references, not a string.
2809
+ *
2810
+ * `CharacterController3D.animations` is the zero-TypeScript way to animate
2811
+ * a character — `{ "idle": "$idle", "run": "$run" }` — and every one of
2812
+ * those values is an asset ref that nothing validated. A typo there is a
2813
+ * character that never animates, with no error anywhere, which is the exact
2814
+ * class of silence the hard-error rule exists to prevent.
2815
+ */
2816
+ mapValues?: boolean;
2147
2817
  };
2148
2818
  /**
2149
2819
  * For an OBJECT-valued prop shaped as a tagged union — `collider`, whose
@@ -2246,7 +2916,17 @@ declare abstract class Behavior {
2246
2916
  static signals?: readonly string[];
2247
2917
  /** The node this behavior is attached to (assigned by the loader). */
2248
2918
  node: Node;
2249
- /** The running engine, reached through the scene tree. */
2919
+ /**
2920
+ * The running engine, reached through the scene tree.
2921
+ *
2922
+ * The message names the FIX, because there is one and it is one word. Reading
2923
+ * this in `onReady` is the documented pattern — `Wander` draws its first
2924
+ * heading there — and `onReady` runs during `loadScene`, before the tree
2925
+ * belongs to anyone. Every boot path in the engine passes `{ engine }` for
2926
+ * exactly that reason; the two-step boot the skills print did not, so a scene
2927
+ * with one wandering critter in it threw at load and the error said only that
2928
+ * something was not attached.
2929
+ */
2250
2930
  get engine(): Engine;
2251
2931
  /** Shortcut for `this.engine.input`. */
2252
2932
  get input(): InputMap;
@@ -2310,6 +2990,35 @@ declare abstract class Behavior {
2310
2990
  * quietly ignoring a field you renamed is usually the better answer.
2311
2991
  */
2312
2992
  deserialize?(data: JsonValue): void;
2993
+ /**
2994
+ * Tell the world what you were just handed — the seventh hook, and the one
2995
+ * that made "Continue" show the right numbers.
2996
+ *
2997
+ * `deserialize` writes fields. A HUD is wired to SIGNALS (`scoreChanged`,
2998
+ * `healthChanged`, `changed`), because that is what `incanto-hud.md` teaches
2999
+ * and what the editor can wire. Nothing emitted them after a restore, so a
3000
+ * Continue boot measured like this:
3001
+ *
3002
+ * ```
3003
+ * HUD score="0" hp=100/100 gems="0"
3004
+ * TRUTH score=1400 hp=38 gems=7
3005
+ * ```
3006
+ *
3007
+ * with `errors: 0`, an empty log, and a restore report of
3008
+ * `{missing:[], skipped:[], restored:2, expected:2}` — every instrument
3009
+ * green over a screen showing a fresh start. Health was the worst of the
3010
+ * three: its setter is private and early-returns when unchanged, so nothing
3011
+ * a game could call would repaint the bar; it read 100 at 38 HP until the
3012
+ * next hit teleported it to 37.
3013
+ *
3014
+ * `restoreBehaviors` calls this once, after every `deserialize` in the pass,
3015
+ * so a handler that reads a sibling sees restored values there too.
3016
+ *
3017
+ * Emit only what DISPLAYS. Not `died`, not `won`, not `levelUp` — a save is
3018
+ * being read, nothing just happened, and re-firing an outcome signal on load
3019
+ * is how a Continue lands straight on the game-over screen.
3020
+ */
3021
+ announce?(): void;
2313
3022
  }
2314
3023
  /** Explicit registration — never an import side effect (tree-shaking safety). */
2315
3024
  declare function registerBehavior(name: string, ctor: BehaviorCtor, opts?: {