incanto 0.66.0 → 0.68.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 (96) hide show
  1. package/bin/_behaviors-loader.mjs +87 -0
  2. package/bin/_pick-server.mjs +48 -0
  3. package/bin/incanto-check.mjs +16 -2
  4. package/bin/incanto-editor.mjs +39 -2
  5. package/bin/incanto-feel.mjs +4 -1
  6. package/bin/incanto-frame.mjs +37 -3
  7. package/bin/incanto-logs.mjs +23 -3
  8. package/bin/incanto-multiplay.mjs +50 -1
  9. package/bin/incanto-play.mjs +3 -1
  10. package/bin/incanto-playtest.mjs +3 -25
  11. package/bin/incanto-verify.mjs +15 -2
  12. package/dist/2d.d.ts +44 -7
  13. package/dist/2d.js +6 -4
  14. package/dist/3d.d.ts +112 -16
  15. package/dist/3d.js +8 -6
  16. package/dist/{audio-player-_UAcHxnC.d.ts → audio-player-DOrq7sP-.d.ts} +62 -4
  17. package/dist/{behavior-BXNLfIJk.d.ts → behavior-DoFPYrgo.d.ts} +117 -2
  18. package/dist/boot-failure-CKYrEcGF.js +47 -0
  19. package/dist/{create-game-DYJCIzO0.js → create-game-DbWtVTxD.js} +91 -29
  20. package/dist/{create-game-Dd7H4bJV.js → create-game-IZIydDwI.js} +16 -10
  21. package/dist/debug.d.ts +1 -1
  22. package/dist/debug.js +3 -2
  23. package/dist/{duplicate-CqSAtdrh.js → duplicate-IWIqk0HJ.js} +1 -1
  24. package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
  25. package/dist/editor.js +18 -7
  26. package/dist/env.d.ts +1 -1
  27. package/dist/{environment-presets-BlPsEmq6.js → environment-presets-CybQXNqS.js} +100 -11
  28. package/dist/{gameplay-D1RADWu3.js → gameplay-DM1eu_cV.js} +46 -5
  29. package/dist/gameplay.d.ts +75 -6
  30. package/dist/gameplay.js +2 -2
  31. package/dist/index.d.ts +52 -8
  32. package/dist/index.js +9 -7
  33. package/dist/{loader-lQDCwNag.js → loader-BC4PNtJX.js} +173 -1
  34. package/dist/{loader-Cga7FVP4.d.ts → loader-CcB533FR.d.ts} +2 -2
  35. package/dist/net.d.ts +2 -2
  36. package/dist/net.js +1 -1
  37. package/dist/{physics-2d-Cji5A6sX.js → physics-2d-B7Y6dPZO.js} +29 -2
  38. package/dist/{physics-3d-BkHjwJgI.js → physics-3d-bG3n70Ky.js} +30 -3
  39. package/dist/react.d.ts +2 -2
  40. package/dist/react.js +1 -1
  41. package/dist/{register-BmuqYTiY.js → register-3ta-2Xig.js} +148 -22
  42. package/dist/{register-D0CxCveZ.js → register-ibCjm-wH.js} +3 -3
  43. package/dist/{replay-IsZbNu6d.js → replay-CYvhVHHN.js} +28 -4
  44. package/dist/{replay-BHoB6fCU.d.ts → replay-Dvn8aeBd.d.ts} +38 -4
  45. package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
  46. package/dist/{split-screen-D_i7GRcY.js → split-screen-CYwDkbLF.js} +2 -2
  47. package/dist/{split-screen-eJUFjhi1.d.ts → split-screen-DhrSzZIB.d.ts} +3 -3
  48. package/dist/{src-BTLbXFPZ.js → src-D7RIqXYF.js} +1 -1
  49. package/dist/{teardown-byR9USax.js → teardown-D2NEmxPB.js} +2 -2
  50. package/dist/{test-DgrD0jHD.js → test-8hoHeRmo.js} +140 -32
  51. package/dist/test.d.ts +9 -7
  52. package/dist/test.js +3 -3
  53. package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
  54. package/dist/vite.d.ts +0 -7
  55. package/dist/vite.js +13 -3
  56. package/dist/webgl-unavailable-N9nQqesw.js +228 -0
  57. package/editor/assets/{agent8-9N-Pd_YS.js → agent8-BrrHOjMJ.js} +1 -1
  58. package/editor/assets/debug-BBhKuBNV.js +3 -0
  59. package/editor/assets/{index-CeDhIPTC.js → index-eVd0BToA.js} +92 -92
  60. package/editor/index.html +1 -1
  61. package/package.json +1 -1
  62. package/schemas/scene.schema.json +5 -1
  63. package/skills/incanto-3d-character.md +1 -0
  64. package/skills/incanto-audio.md +49 -0
  65. package/skills/incanto-building-2d-games.md +37 -0
  66. package/skills/incanto-building-3d-games.md +56 -0
  67. package/skills/incanto-editor.md +5 -1
  68. package/skills/incanto-environment.md +2 -2
  69. package/skills/incanto-gameplay-behaviors.md +29 -2
  70. package/skills/incanto-hud.md +14 -1
  71. package/skills/incanto-multiplayer.md +14 -0
  72. package/skills/incanto-node-reference.md +2 -1
  73. package/skills/incanto-performance.md +35 -0
  74. package/skills/incanto-physics-and-input.md +23 -0
  75. package/skills/incanto-playtesting.md +15 -1
  76. package/skills/incanto-save-slots.md +21 -1
  77. package/skills/incanto-scene-json-authoring.md +12 -1
  78. package/skills/incanto-verifying-your-game.md +19 -0
  79. package/skills/incanto-your-first-game.md +4 -4
  80. package/templates-app/beacon-isle-3d/package.json +1 -1
  81. package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
  82. package/templates-app/beacon-isle-3d/src/main.ts +8 -1
  83. package/templates-app/platformer-2d/package.json +1 -1
  84. package/templates-app/platformer-2d/src/game.scene.json +99 -53
  85. package/templates-app/platformer-2d/src/main.ts +8 -1
  86. package/templates-app/star-survivor/package.json +1 -1
  87. package/templates-app/star-survivor/src/game.scene.json +31 -17
  88. package/templates-app/star-survivor/src/main.ts +8 -1
  89. package/templates-app/tps-3d/package.json +1 -1
  90. package/templates-app/tps-3d/src/game.scene.json +14 -8
  91. package/templates-app/tps-3d/src/main.ts +8 -1
  92. package/templates-app/village-quest-3d/package.json +1 -1
  93. package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
  94. package/templates-app/village-quest-3d/src/main.ts +8 -1
  95. package/templates-app/village-quest-3d/src/village.scene.json +10 -7
  96. package/editor/assets/debug-CkbJICYp.js +0 -3
@@ -1,5 +1,5 @@
1
- import { b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, t as Behavior } from "./behavior-BXNLfIJk.js";
2
- import { c as JsonValue, s as JsonObject } from "./schema-CFeioQRE.js";
1
+ import { b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, t as Behavior } from "./behavior-DoFPYrgo.js";
2
+ import { c as JsonValue, s as JsonObject } from "./schema-B6ugCV1Q.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
5
5
  /**
@@ -143,16 +143,37 @@ declare class Health extends Behavior {
143
143
  * unlosable. Every checker was green on it, and `incanto-playtest` even
144
144
  * reported `lost in 19/20` because a single `died` reads as a loss.
145
145
  *
146
- * Wire it where the life is spent:
146
+ * **Wire `reviveFull`, not this, to `lifeLost`.** `ScoreKeeper.loseLife()`
147
+ * emits `lifeLost` WITH the remaining life count, and this method's first
148
+ * parameter is the HP to come back at — so the documented recipe revived at
149
+ * 2 HP, then 1, then 0, and nothing warned (the wire-arity guard fires only
150
+ * when a handler is UNDER-fed, and `hp` is optional):
147
151
  *
148
- * ```json
149
- * { "signal": "lifeLost", "from": "Score", "to": "Player", "handler": "revive" }
152
+ * ```
153
+ * death 1: lives=2 hp=2
154
+ * death 2: lives=1 hp=1
155
+ * death 3: lives=0 hp=0 dead=false <- alive at zero HP
150
156
  * ```
151
157
  *
158
+ * Reach for `revive(hp)` when your game genuinely wants a partial comeback.
159
+ *
152
160
  * A no-op on the living — reviving someone who never died would silently
153
161
  * refill their health, which is a different feature and not this one.
154
162
  */
155
163
  revive(hp?: number): void;
164
+ /**
165
+ * Bring a dead one back at FULL health — the handler a life-lost wire wants.
166
+ *
167
+ * It exists because `revive` takes an optional `hp` and `lifeLost` carries the
168
+ * life count, so wiring the two silently made "you have 2 lives left" mean
169
+ * "come back at 2 HP". This takes no arguments, so no signal's payload can
170
+ * ever change what it means.
171
+ *
172
+ * ```json
173
+ * { "signal": "lifeLost", "from": "Score", "to": "Player", "handler": "reviveFull" }
174
+ * ```
175
+ */
176
+ reviveFull(): void;
156
177
  /** Drop to 0 and die immediately (ignores i-frames). */
157
178
  kill(): void;
158
179
  /**
@@ -537,6 +558,20 @@ declare class SavePoint extends Behavior {
537
558
  private elapsed;
538
559
  private slots;
539
560
  private store;
561
+ /**
562
+ * Has this run already said the saves are going nowhere?
563
+ *
564
+ * `saved(slot)` is the engine's own instrument for "it saved", and it fired
565
+ * identically in a private window where the write landed in a Map that dies
566
+ * with the tab. In-session everything looked healthy — the slot read back,
567
+ * `slotScene()` answered — and across the reload the score went 5 to 0 and
568
+ * `list()` went `["1"]` to `[]`, with `engine.log.entries().length = 0` and
569
+ * `stats().errors = 0` in every phase. Byte-identical to a healthy control.
570
+ *
571
+ * So it goes through `engine.log`, not just the console: that is what
572
+ * `incanto-logs` reads and what the `says` rung reports.
573
+ */
574
+ private warnedVolatile;
540
575
  private pendingRestore;
541
576
  private pendingProbe;
542
577
  override onReady(): void;
@@ -1050,6 +1085,40 @@ declare class PathFollow extends Behavior {
1050
1085
  override update(dt: number): void;
1051
1086
  }
1052
1087
  //#endregion
1088
+ //#region src/gameplay/spatial.d.ts
1089
+ /**
1090
+ * A node's position with every spatial ancestor's added in.
1091
+ *
1092
+ * The tree composes transforms for DRAWING — three does it for free — and every
1093
+ * piece of engine logic that reads `node.position` gets the LOCAL number
1094
+ * instead. Physics already knew (`worldPosition2D`/`worldPosition3D` in the
1095
+ * adapters); the camera did not, and neither did anything that chases a target.
1096
+ *
1097
+ * A camera parented to the player is the Godot and Phaser idiom, and the
1098
+ * authoring skill explicitly permits it — *"EVERY node type can hold children …
1099
+ * any node works as a grouping container"*. Measured on exactly that scene:
1100
+ *
1101
+ * ```
1102
+ * player world position : [1400, 900]
1103
+ * renderer view centre : {"x":0,"y":0}
1104
+ * framing : camera /Level/Player/Cam centred [1400, 900]
1105
+ * 1 in view, 0 outside it
1106
+ * ```
1107
+ *
1108
+ * The renderer drew empty world 1600 px from the player while `framing` — the
1109
+ * instrument the skills tell you to trust before handing a game back —
1110
+ * certified the player on screen.
1111
+ *
1112
+ * Arity comes from the node itself: a 2D node has two components, a 3D node
1113
+ * three, and an ancestor that is neither contributes nothing.
1114
+ */
1115
+ declare function worldPosition(node: Node): number[];
1116
+ /**
1117
+ * Turn a world position into the local one `node.position` has to hold for the
1118
+ * node to END UP there — the inverse of `worldPosition` for its parent chain.
1119
+ */
1120
+ declare function localFromWorld(node: Node, world: readonly number[]): number[];
1121
+ //#endregion
1053
1122
  //#region src/gameplay/index.d.ts
1054
1123
  /** Every built-in gameplay behavior, keyed by its registration name. */
1055
1124
  declare const GAMEPLAY_BEHAVIORS: Readonly<Record<string, BehaviorCtor>>;
@@ -1062,4 +1131,4 @@ declare function registerGameplayBehaviors(opts?: {
1062
1131
  replace?: boolean;
1063
1132
  }): void;
1064
1133
  //#endregion
1065
- export { CameraShake, Chase, Collector, Cooldown, Currency, DamageOnContact, DayNight, type DayPhase, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
1134
+ export { CameraShake, Chase, Collector, Cooldown, Currency, DamageOnContact, DayNight, type DayPhase, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, localFromWorld, phaseOf, registerGameplayBehaviors, restartScene, screenFlash, worldPosition };
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as DamageOnContact, C as restartScene, D as FaceTarget, E as FloatAway, M as Currency, N as Collector, O as DayNight, P as Chase, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Health, k as phaseOf, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-D1RADWu3.js";
2
- export { CameraShake, Chase, Collector, Cooldown, Currency, DamageOnContact, DayNight, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
1
+ import { A as DamageOnContact, C as restartScene, D as FaceTarget, E as FloatAway, F as localFromWorld, I as worldPosition, M as Currency, N as Collector, O as DayNight, P as Chase, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Health, k as phaseOf, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-DM1eu_cV.js";
2
+ export { CameraShake, Chase, Collector, Cooldown, Currency, DamageOnContact, DayNight, FaceTarget, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, localFromWorld, phaseOf, registerGameplayBehaviors, restartScene, screenFlash, worldPosition };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { $ as T_PREFIX, A as SettingsValues, At as BusName, B as captureBehaviors, C as EngineStats, Ct as sfxDuration, Dt as MusicTrack, E as DeviceHints, Et as MusicManager, F as BehaviorState, Ft as LogLevel, G as ORDER_GROUP_BASE, H as savesWithoutUid, I as RestoreReport, It as LogManager, J as effectiveOrder, K as OrderGroup, L as SaveSlot, Lt as Signal, M as readDeviceHints, Mt as NodeLifecycle, N as suggestQuality, Nt as SceneTree, O as QualityTier, Ot as PlayMusicOptions, P as Scene, Pt as LogEntry, Q as Localization, R as SaveSlots, Rt as SignalListener, S as Scheduler, St as SynthOptions, T as RendererStats, Tt as MusicBackend, U as SaveStore, V as restoreBehaviors, W as createSaveStore, X as BASE_LOCALE, Y as resolveOrderGroups, Z as LocaleTables, _ as mergeStaticSignals, _t as spatialPan, a as clearBehaviors, at as EffectLog, b as Engine, bt as SfxParams, c as registeredBehaviors, ct as isAudioContextAvailable, d as PropSchema, dt as Listener, et as suggestLocale, f as clearRegistry, ft as ROLLOFF_MODELS, g as getNodeType, gt as spatialGain, h as getNodeSignals, ht as Vec3, i as behaviorSignals, it as EffectKind, j as qualityEnvironment, jt as Node, k as Settings, kt as AudioBuses, l as NodeCtor, lt as Voice, m as getNodeSchema, mt as SpatialParams, n as BehaviorCtor, nt as InputMap, o as getBehavior, ot as SfxEngine, p as createNode, pt as RolloffModel, q as OrderGroupTable, r as behaviorSchema, rt as EffectEvent, s as registerBehavior, st as SfxPlayOptions, t as Behavior, tt as translationKey, u as PropDef, ut as VoicePreset, v as registerNode, vt as SFX_PRESETS, w as GameStats, wt as synthSfx, x as EngineOptions, xt as SfxWave, y as registeredTypes, yt as SFX_PRESET_NAMES, z as behaviorsWithoutSave } from "./behavior-BXNLfIJk.js";
2
- import { a as Rng, c as JsonValue, d as jsonKind, i as SceneJson, l as jsonClone, n as NodeJson, o as JsonKind, r as SCENE_FORMAT, s as JsonObject, t as ConnectionJson, u as jsonEquals } from "./schema-CFeioQRE.js";
3
- import { n as loadScene, t as LoadSceneOptions } from "./loader-Cga7FVP4.js";
1
+ import { a as ParticleSimConfig, c as showBootFailure, i as ParticleSim, n as AudioPlayer, o as ParticleView, s as isWebGLAvailable, t as AudioElementLike } from "./audio-player-DOrq7sP-.js";
2
+ import { $ as T_PREFIX, A as SettingsValues, At as BusName, B as captureBehaviors, C as EngineStats, Ct as sfxDuration, Dt as MusicTrack, E as DeviceHints, Et as MusicManager, F as BehaviorState, Ft as LogLevel, G as ORDER_GROUP_BASE, H as savesWithoutUid, I as RestoreReport, It as LogManager, J as effectiveOrder, K as OrderGroup, L as SaveSlot, Lt as Signal, M as readDeviceHints, Mt as NodeLifecycle, N as suggestQuality, Nt as SceneTree, O as QualityTier, Ot as PlayMusicOptions, P as Scene, Pt as LogEntry, Q as Localization, R as SaveSlots, Rt as SignalListener, S as Scheduler, St as SynthOptions, T as RendererStats, Tt as MusicBackend, U as SaveStore, V as restoreBehaviors, W as createSaveStore, X as BASE_LOCALE, Y as resolveOrderGroups, Z as LocaleTables, _ as mergeStaticSignals, _t as spatialPan, a as clearBehaviors, at as EffectLog, b as Engine, bt as SfxParams, c as registeredBehaviors, ct as isAudioContextAvailable, d as PropSchema, dt as Listener, et as suggestLocale, f as clearRegistry, ft as ROLLOFF_MODELS, g as getNodeType, gt as spatialGain, h as getNodeSignals, ht as Vec3, i as behaviorSignals, it as EffectKind, j as qualityEnvironment, jt as Node, k as Settings, kt as AudioBuses, l as NodeCtor, lt as Voice, m as getNodeSchema, mt as SpatialParams, n as BehaviorCtor, nt as InputMap, o as getBehavior, ot as SfxEngine, p as createNode, pt as RolloffModel, q as OrderGroupTable, r as behaviorSchema, rt as EffectEvent, s as registerBehavior, st as SfxPlayOptions, t as Behavior, tt as translationKey, u as PropDef, ut as VoicePreset, v as registerNode, vt as SFX_PRESETS, w as GameStats, wt as synthSfx, x as EngineOptions, xt as SfxWave, y as registeredTypes, yt as SFX_PRESET_NAMES, z as behaviorsWithoutSave } from "./behavior-DoFPYrgo.js";
3
+ import { a as Rng, c as JsonValue, d as jsonKind, i as SceneJson, l as jsonClone, n as NodeJson, o as JsonKind, r as SCENE_FORMAT, s as JsonObject, t as ConnectionJson, u as jsonEquals } from "./schema-B6ugCV1Q.js";
4
+ import { n as loadScene, t as LoadSceneOptions } from "./loader-CcB533FR.js";
4
5
  import { i as resolveFrames, n as AnimationEntry, r as resolveAnimation, t as AnimationDef } from "./sprite-animation-CMr6f1K2.js";
5
- import { a as ParticleSimConfig, i as ParticleSim, n as AudioPlayer, o as ParticleView, t as AudioElementLike } from "./audio-player-_UAcHxnC.js";
6
6
  import { i as gridFromRows, n as PathGrid, r as findPath, t as FindPathOptions } from "./pathfinding-_fGrCFmH.js";
7
- import { a as startRecording, c as IncantoErrorDetails, i as replay, l as auditScene, n as ReplayEvent, o as IncantoError, r as ReplayJson, s as IncantoErrorCode, t as Recorder } from "./replay-BHoB6fCU.js";
7
+ import { a as startRecording, c as IncantoErrorDetails, i as replay, l as auditScene, n as ReplayEvent, o as IncantoError, r as ReplayJson, s as IncantoErrorCode, t as Recorder } from "./replay-Dvn8aeBd.js";
8
8
 
9
9
  //#region src/core/audio/crossfade.d.ts
10
10
  /**
@@ -313,8 +313,24 @@ declare class HudLayer extends Node {
313
313
  * play resumes.
314
314
  */
315
315
  private stepFocus;
316
- /** Focusable widgets under this layer, in tree order. */
317
- private focusables;
316
+ /**
317
+ * Focusable widgets under this layer, in tree order — **on screen only**.
318
+ *
319
+ * This filtered on each widget's OWN `visible` and recursed into hidden
320
+ * containers anyway, so arrow-keys walked into closed panels and Enter really
321
+ * pressed what it found. Measured on a title screen: 23 focus stops, 21 of
322
+ * them under an ancestor with `visible = false`, and
323
+ *
324
+ * gold before = 150 -> Enter on the hidden Shop/Row0/Buy -> gold 120,
325
+ * screen still 'title'
326
+ *
327
+ * The layer's own visibility was not a gate either. It is now: a HUD nobody
328
+ * can see has no ring, and a subtree nobody can see is not walked.
329
+ *
330
+ * PUBLIC, because a game that wants to know what its ring contains could not
331
+ * ask — and "why is focus on that" is exactly the question this answers.
332
+ */
333
+ focusables(): HudWidgetBase[];
318
334
  /** Move focus to a widget by hand — opening a menu should start somewhere. */
319
335
  focus(widget: HudWidgetBase | null): void;
320
336
  /** @internal Widgets mount into per-anchor flex columns. */
@@ -377,6 +393,11 @@ declare abstract class HudWidgetBase extends Node {
377
393
  private host;
378
394
  override onExitTree(): void;
379
395
  override update(_dt: number): void;
396
+ /**
397
+ * The display this widget's own css asks for while shown — `grid` for a grid
398
+ * panel, `flex` for a row, `''` for a widget that never says.
399
+ */
400
+ private _shownDisplay;
380
401
  /** Which anchor slot this widget was mounted into. */
381
402
  private mountedAnchor;
382
403
  /**
@@ -607,6 +628,29 @@ declare class UiDialogue extends HudWidgetBase {
607
628
  /** Queue a line; optional `choices` renders buttons after the line types out. */
608
629
  say(speaker: string, text: string, choices?: string[]): void;
609
630
  /**
631
+ * A dialogue takes focus while it is up.
632
+ *
633
+ * It paints a focus ring and the generated reference advertises the prop, and
634
+ * it was inert: choices are raw DOM buttons inside the widget, so
635
+ * `HudLayer.focusables()` can never see them, and this class — unlike
636
+ * `UiButton` a hundred lines above it — never overrode `_activate`. Eleven
637
+ * keys and eight pad codes did nothing to a waiting choice while the same
638
+ * Enter pressed a sibling button. Native Tab worked, and is closed by any
639
+ * game that binds Tab; a pad has no Tab at all.
640
+ */
641
+ override focusable: boolean;
642
+ /** Which choice the keyboard/pad is on. Reset by every new line. */
643
+ private cursor;
644
+ /**
645
+ * Enter / A, and left / right between the options.
646
+ *
647
+ * `dir` is 0 for "activate" and ±1 for a nudge, the same shape `UiSelect`
648
+ * uses — so one key layout drives a menu, a slider and a conversation.
649
+ */
650
+ override _activate(dir: number): void;
651
+ /** Show which option the keyboard is on — a ring nobody can see is not one. */
652
+ private paintCursor;
653
+ /**
610
654
  * Player pressed "next": mid-typing reveals the whole line; a fully-typed
611
655
  * line without choices advances (choice lines wait for `choose`).
612
656
  */
@@ -1265,4 +1309,4 @@ declare function computeViewport(canvasW: number, canvasH: number, viewport: {
1265
1309
  /** Engine version. Kept in sync with package.json by the release pipeline. */
1266
1310
  declare const VERSION: string;
1267
1311
  //#endregion
1268
- export { type AnimationDef, type AnimationEntry, type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, type EffectEvent, type EffectKind, EffectLog, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RefNode, type RefProblem, type RefResult, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, Respawn, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
1312
+ export { type AnimationDef, type AnimationEntry, type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, type EffectEvent, type EffectKind, EffectLog, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RefNode, type RefProblem, type RefResult, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, Respawn, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, isWebGLAvailable, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, showBootFailure, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
- import { A as describeRefProblem, B as registeredBehaviors, C as ORDER_GROUP_BASE, E as InputMap, F as behaviorSchema, I as behaviorSignals, L as clearBehaviors, M as resolveRefInJson, N as parseNodePath, O as Node, P as Behavior, R as getBehavior, S as createSaveStore, T as resolveOrderGroups, V as Signal, _ as SaveSlots, a as serializeNode, b as restoreBehaviors, d as computeViewport, f as resolveViewport, g as resolveConstants, h as isConstRef, i as Scene, j as nodeRefWarnings, m as CONST_REF_KEY, n as loadScene, o as SCENE_FORMAT, p as SceneTree, v as behaviorsWithoutSave, w as effectiveOrder, x as savesWithoutUid, y as captureBehaviors, z as registerBehavior } from "./loader-lQDCwNag.js";
2
- import { A as UiBanner, B as isAudioContextAvailable, D as EffectLog, E as LogManager, F as T_PREFIX, G as SFX_PRESET_NAMES, H as spatialGain, I as suggestLocale, J as MusicManager, K as sfxDuration, M as UiText, N as BASE_LOCALE, O as HudLayer, P as Localization, Q as AudioBuses, R as translationKey, S as qualityEnvironment, T as suggestQuality, U as spatialPan, V as ROLLOFF_MODELS, W as SFX_PRESETS, X as crossfadeGains, Y as WebAudioMusicBackend, Z as fadeGain, _ as AudioPlayer, a as UiMuteToggle, b as Settings, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as UiDialogue, h as UiButton, i as UiLanguageSelect, j as UiBar, l as UiSelect, m as Respawn, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as synthSfx, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, v as Engine, w as readDeviceHints, z as SfxEngine } from "./register-BmuqYTiY.js";
1
+ import { A as describeRefProblem, B as registeredBehaviors, C as ORDER_GROUP_BASE, E as InputMap, F as behaviorSchema, I as behaviorSignals, L as clearBehaviors, M as resolveRefInJson, N as parseNodePath, O as Node, P as Behavior, R as getBehavior, S as createSaveStore, T as resolveOrderGroups, V as Signal, _ as SaveSlots, a as serializeNode, b as restoreBehaviors, d as computeViewport, f as resolveViewport, g as resolveConstants, h as isConstRef, i as Scene, j as nodeRefWarnings, m as CONST_REF_KEY, n as loadScene, o as SCENE_FORMAT, p as SceneTree, v as behaviorsWithoutSave, w as effectiveOrder, x as savesWithoutUid, y as captureBehaviors, z as registerBehavior } from "./loader-BC4PNtJX.js";
2
+ import { A as UiBanner, B as isAudioContextAvailable, D as EffectLog, E as LogManager, F as T_PREFIX, G as SFX_PRESET_NAMES, H as spatialGain, I as suggestLocale, J as MusicManager, K as sfxDuration, M as UiText, N as BASE_LOCALE, O as HudLayer, P as Localization, Q as AudioBuses, R as translationKey, S as qualityEnvironment, T as suggestQuality, U as spatialPan, V as ROLLOFF_MODELS, W as SFX_PRESETS, X as crossfadeGains, Y as WebAudioMusicBackend, Z as fadeGain, _ as AudioPlayer, a as UiMuteToggle, b as Settings, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as UiDialogue, h as UiButton, i as UiLanguageSelect, j as UiBar, l as UiSelect, m as Respawn, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as synthSfx, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, v as Engine, w as readDeviceHints, z as SfxEngine } from "./register-3ta-2Xig.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-CwwhxQgb.js";
6
6
  import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-CF70EArN.js";
7
- import { n as startRecording, r as auditScene, t as replay } from "./replay-IsZbNu6d.js";
8
- import { a as logReport, i as resolveRendering, n as attachTouchControls, o as logText, r as joystickVector, s as parseDrive, t as TouchControls } from "./touch-DESwnpOc.js";
7
+ import { i as joystickVector, n as attachTouchControls, t as TouchControls } from "./touch-DEAmqGdf.js";
8
+ import { n as startRecording, r as auditScene, t as replay } from "./replay-CYvhVHHN.js";
9
+ import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
10
+ import { a as logText, i as logReport, o as parseDrive, r as resolveRendering, t as isWebGLAvailable } from "./webgl-unavailable-N9nQqesw.js";
9
11
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
10
12
  import { a as PARTICLE_PRESETS, i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
11
- import { a as preloadUrls, i as preloadSceneAssets, n as newUid, o as findPath, r as assetUrls, s as gridFromRows, t as VERSION } from "./src-BTLbXFPZ.js";
12
- import { t as duplicateNode } from "./duplicate-CqSAtdrh.js";
13
- export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, EffectLog, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Respawn, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
13
+ import { a as preloadUrls, i as preloadSceneAssets, n as newUid, o as findPath, r as assetUrls, s as gridFromRows, t as VERSION } from "./src-D7RIqXYF.js";
14
+ import { t as duplicateNode } from "./duplicate-IWIqk0HJ.js";
15
+ export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, EffectLog, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Respawn, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, isWebGLAvailable, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, showBootFailure, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -20,6 +20,17 @@ var Signal = class {
20
20
  disconnectAll() {
21
21
  this.connections.length = 0;
22
22
  }
23
+ /**
24
+ * How many listeners are attached.
25
+ *
26
+ * A leak is a SLOPE, and a game had no way to see one: every check in this
27
+ * engine is a snapshot. A handler connected per frame and never disconnected,
28
+ * or a `once` that armed and never fired, accumulates invisibly and is
29
+ * perfectly healthy for the first thirty seconds of every playthrough.
30
+ */
31
+ get count() {
32
+ return this.connections.length;
33
+ }
23
34
  emit(...args) {
24
35
  const snapshot = [...this.connections];
25
36
  for (const conn of snapshot) {
@@ -652,6 +663,16 @@ var Node = class {
652
663
  off(signal, fn) {
653
664
  this._signals.get(signal)?.disconnect(fn);
654
665
  }
666
+ /**
667
+ * How many listeners this node has on `signal` — 0 for one nobody watches.
668
+ *
669
+ * The engine's own leak test reads it, and so can a game: a handler connected
670
+ * every frame and never disconnected is invisible to every other check here,
671
+ * because every other check is a snapshot and a leak is a slope.
672
+ */
673
+ listenerCount(signal) {
674
+ return this._signals.get(signal)?.count ?? 0;
675
+ }
655
676
  emit(signal, ...args) {
656
677
  this.assertDeclared(signal);
657
678
  this._signals.get(signal)?.emit(...args);
@@ -1549,10 +1570,16 @@ function effectiveOrder(group, renderOrder, table = ORDER_GROUP_BASE) {
1549
1570
  //#endregion
1550
1571
  //#region src/core/save.ts
1551
1572
  const memoryStores = /* @__PURE__ */ new Map();
1573
+ /** Namespaces already warned about, so a per-frame save cannot spam the console. */
1574
+ const warnedVolatile = /* @__PURE__ */ new Set();
1552
1575
  function createSaveStore(namespace) {
1553
1576
  const prefix = `incanto:${namespace}:`;
1554
1577
  const local = storageOrNull();
1555
1578
  if (!local) {
1579
+ if (typeof localStorage !== "undefined" && !warnedVolatile.has(namespace)) {
1580
+ warnedVolatile.add(namespace);
1581
+ console.warn(`[incanto] this browser refused localStorage, so saves for '${namespace}' live in memory and are lost when the page closes (private window, storage disabled, or quota exhausted). Check \`store.persistent\` and tell the player.`);
1582
+ }
1556
1583
  let mem = memoryStores.get(namespace);
1557
1584
  if (!mem) {
1558
1585
  mem = /* @__PURE__ */ new Map();
@@ -1560,6 +1587,7 @@ function createSaveStore(namespace) {
1560
1587
  }
1561
1588
  const backing = mem;
1562
1589
  return {
1590
+ persistent: false,
1563
1591
  get: (key, fallback) => parse(backing.get(key), fallback),
1564
1592
  set: (key, value) => void backing.set(key, JSON.stringify(value)),
1565
1593
  remove: (key) => void backing.delete(key),
@@ -1567,6 +1595,7 @@ function createSaveStore(namespace) {
1567
1595
  };
1568
1596
  }
1569
1597
  return {
1598
+ persistent: true,
1570
1599
  get: (key, fallback) => parse(local.getItem(prefix + key) ?? void 0, fallback),
1571
1600
  set: (key, value) => local.setItem(prefix + key, JSON.stringify(value)),
1572
1601
  remove: (key) => local.removeItem(prefix + key),
@@ -1674,6 +1703,7 @@ const FREED_KEY = "#freed";
1674
1703
  function captureBehaviors(root, source) {
1675
1704
  const state = {};
1676
1705
  const unaddressable = [];
1706
+ const unrecordable = [];
1677
1707
  let saveable = 0;
1678
1708
  const walk = (node) => {
1679
1709
  const behavior = node.behavior;
@@ -1698,13 +1728,46 @@ function captureBehaviors(root, source) {
1698
1728
  mark(root);
1699
1729
  const freed = authoredUids(source).filter((uid) => !alive.has(uid));
1700
1730
  if (freed.length > 0) state[FREED_KEY] = freed;
1731
+ unrecordable.push(...goneWithoutUid(source, root));
1701
1732
  }
1702
1733
  return {
1703
1734
  state,
1704
1735
  unaddressable,
1705
- saveable
1736
+ saveable,
1737
+ unrecordable
1706
1738
  };
1707
1739
  }
1740
+ /**
1741
+ * Authored nodes this run CONSUMED that the ledger cannot record.
1742
+ *
1743
+ * `#freed` is authored uids minus alive uids, so a node with no uid can never
1744
+ * enter it — it is silently omitted, and on load it comes back. Every collected
1745
+ * coin in a scene without uids returns, while the score that counted it does
1746
+ * not, and nothing says a word.
1747
+ *
1748
+ * A uid is the only stable handle (it survives a rename and a reparent), so the
1749
+ * engine must not guess a replacement — a saved PATH would point at whatever
1750
+ * node moved into that slot after the next edit. What it can do is notice, by
1751
+ * walking the authored tree and asking the live one whether each node is still
1752
+ * there. Reported, not repaired.
1753
+ */
1754
+ function goneWithoutUid(source, root) {
1755
+ const out = [];
1756
+ const walk = (node, path) => {
1757
+ if (typeof node !== "object" || node === null) return;
1758
+ const n = node;
1759
+ const name = typeof n.name === "string" ? n.name : "";
1760
+ const here = path === "" ? name : `${path}/${name}`;
1761
+ if (!(typeof n.uid === "string" && n.uid !== "") && here !== "" && path !== "") {
1762
+ const live = root.getNodeOrNull(here.slice(here.indexOf("/") + 1));
1763
+ const leaving = live ? live.tree?._isQueuedFree(live) ?? false : false;
1764
+ if (!live || leaving) out.push(here);
1765
+ }
1766
+ if (Array.isArray(n.children)) for (const child of n.children) walk(child, here);
1767
+ };
1768
+ walk(source.root, "");
1769
+ return out;
1770
+ }
1708
1771
  /** Every uid the scene FILE declares, in document order. */
1709
1772
  function authoredUids(source) {
1710
1773
  const out = [];
@@ -1779,6 +1842,17 @@ var SaveSlots = class {
1779
1842
  constructor(namespace = "saves", store) {
1780
1843
  this.store = store ?? createSaveStore(namespace);
1781
1844
  }
1845
+ /**
1846
+ * Will these slots survive the page closing?
1847
+ *
1848
+ * `false` in a private window, with storage disabled, or headless — every
1849
+ * write still succeeds and every read still answers, for as long as the tab
1850
+ * lives. A load menu should say so instead of offering a slot that will not
1851
+ * be there tomorrow.
1852
+ */
1853
+ get persistent() {
1854
+ return this.store.persistent !== false;
1855
+ }
1782
1856
  /** Slot ids that have a save, newest first. */
1783
1857
  list() {
1784
1858
  const rows = this.store.get(INDEX_KEY, []).map((id) => ({
@@ -2795,6 +2869,8 @@ var Scene = class {
2795
2869
  orderGroups;
2796
2870
  /** Physics config (gravity etc. — interpreted by the physics modules). */
2797
2871
  physics;
2872
+ /** Declared piece of another scene — see `SceneJson.fragment`. */
2873
+ fragment;
2798
2874
  /** Multiplayer config (room etc. — interpreted by incanto/net). */
2799
2875
  multiplayer;
2800
2876
  /** Design-resolution viewport (consumed by renderers via resolveViewport). */
@@ -2814,6 +2890,7 @@ var Scene = class {
2814
2890
  this.orderGroups = source.orderGroups ? { ...source.orderGroups } : void 0;
2815
2891
  this.strings = source.strings ? jsonClone(source.strings) : void 0;
2816
2892
  this.physics = source.physics ? jsonClone(source.physics) : void 0;
2893
+ this.fragment = source.fragment === true ? true : void 0;
2817
2894
  this.multiplayer = source.multiplayer ? jsonClone(source.multiplayer) : void 0;
2818
2895
  this.environment = source.environment ? jsonClone(source.environment) : void 0;
2819
2896
  this.viewport = source.viewport ? jsonClone(source.viewport) : void 0;
@@ -2832,6 +2909,7 @@ var Scene = class {
2832
2909
  ...this.orderGroups ? { orderGroups: { ...this.orderGroups } } : {},
2833
2910
  ...this.strings ? { strings: jsonClone(this.strings) } : {},
2834
2911
  ...this.physics ? { physics: jsonClone(this.physics) } : {},
2912
+ ...this.fragment ? { fragment: true } : {},
2835
2913
  ...this.multiplayer ? { multiplayer: jsonClone(this.multiplayer) } : {},
2836
2914
  ...this.environment ? { environment: jsonClone(this.environment) } : {},
2837
2915
  ...this.viewport ? { viewport: jsonClone(this.viewport) } : {},
@@ -3004,9 +3082,11 @@ function buildNodeJson(json, opts) {
3004
3082
  return buildNode(json, opts, [], "", void 0);
3005
3083
  }
3006
3084
  function validateHeader(json) {
3085
+ rejectUnknownKeys(json, SCENE_KEYS, "the scene", "");
3007
3086
  if (json.format !== 1) throw new IncantoError("BAD_FORMAT", `Unsupported scene format ${JSON.stringify(json.format)}; this engine reads format 1.`);
3008
3087
  if (json.type !== "scene") throw new IncantoError("BAD_FORMAT", `Expected "type": "scene", got ${JSON.stringify(json.type)}.`);
3009
3088
  if (typeof json.name !== "string" || json.name === "") throw new IncantoError("BAD_FORMAT", "Scene \"name\" must be a non-empty string.");
3089
+ if (json.fragment !== void 0 && typeof json.fragment !== "boolean") throw new IncantoError("BAD_FORMAT", `Scene "fragment" must be true or false, got ${JSON.stringify(json.fragment)}.`);
3010
3090
  if (json.dimension !== void 0 && json.dimension !== "2d" && json.dimension !== "3d") throw new IncantoError("BAD_FORMAT", `Scene "dimension" must be "2d" or "3d", got ${JSON.stringify(json.dimension)}.`);
3011
3091
  if (typeof json.root !== "object" || json.root === null) throw new IncantoError("BAD_FORMAT", "Scene \"root\" must be a node object.");
3012
3092
  resolveViewport(json.viewport);
@@ -3042,9 +3122,101 @@ function withNodeContext(e, path, nj) {
3042
3122
  });
3043
3123
  return e;
3044
3124
  }
3125
+ /**
3126
+ * Every key a node object may carry.
3127
+ *
3128
+ * The node object was an OPEN dictionary sitting between a dozen closed ones —
3129
+ * `props`, `environment`, `material`, `network` and the environment's own
3130
+ * sub-blocks all reject an unknown key with a did-you-mean. A node accepted
3131
+ * anything, and the one that costs an author a day is `connections`:
3132
+ *
3133
+ * ```jsonc
3134
+ * { "name": "Room", "type": "Node3D",
3135
+ * "connections": [ … ], // <- silently nothing
3136
+ * "children": [ … ] }
3137
+ * ```
3138
+ *
3139
+ * `wireConnections` is reached from the scene header and from a sub-scene's own
3140
+ * header, and NOTHING reads `nodeJson.connections`. Measured on two scenes
3141
+ * differing only in which brace the same wire sits inside:
3142
+ *
3143
+ * ```
3144
+ * inside Dst.n=0 validateScene.ok=true auditWarnings=0
3145
+ * beside Dst.n=1 validateScene.ok=true auditWarnings=0
3146
+ * none Dst.n=0 validateScene.ok=true auditWarnings=0
3147
+ * ```
3148
+ *
3149
+ * The misplaced wire is bit-for-bit indistinguishable from deleting it. A
3150
+ * builder moved 28 of them into `root`, got `1/1 scene(s) valid` with zero
3151
+ * warnings, and found out sixty seconds later from `incanto-playtest`.
3152
+ */
3153
+ const NODE_KEYS = [
3154
+ "name",
3155
+ "uid",
3156
+ "type",
3157
+ "instance",
3158
+ "overrides",
3159
+ "groups",
3160
+ "tags",
3161
+ "props",
3162
+ "script",
3163
+ "network",
3164
+ "children"
3165
+ ];
3166
+ /** Every key the scene HEADER may carry — closed for the same reason. */
3167
+ const SCENE_KEYS = [
3168
+ "format",
3169
+ "type",
3170
+ "name",
3171
+ "dimension",
3172
+ "viewport",
3173
+ "assets",
3174
+ "constants",
3175
+ "orderGroups",
3176
+ "input",
3177
+ "multiplayer",
3178
+ "environment",
3179
+ "strings",
3180
+ "physics",
3181
+ "fragment",
3182
+ "root",
3183
+ "connections"
3184
+ ];
3185
+ /** Closest known key by edit distance — only a genuinely near one. */
3186
+ function nearestKey(key, known) {
3187
+ const lower = key.toLowerCase();
3188
+ let best = null;
3189
+ let bestScore = 3;
3190
+ for (const candidate of known) {
3191
+ let d = 0;
3192
+ const a = lower;
3193
+ const b = candidate.toLowerCase();
3194
+ const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
3195
+ for (let j = 0; j <= b.length; j++) rows[0][j] = j;
3196
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
3197
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3198
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
3199
+ }
3200
+ d = rows[a.length][b.length];
3201
+ if (d < bestScore) {
3202
+ bestScore = d;
3203
+ best = candidate;
3204
+ }
3205
+ }
3206
+ return best;
3207
+ }
3208
+ /** Reject a key the shape does not have, the way `props` already does. */
3209
+ function rejectUnknownKeys(obj, known, what, where) {
3210
+ for (const key of Object.keys(obj)) {
3211
+ if (known.includes(key)) continue;
3212
+ const guess = nearestKey(key, known);
3213
+ throw new IncantoError("BAD_FORMAT", `Unknown key "${key}" on ${what}${where ? ` '${where}'` : ""}.` + (guess ? ` Did you mean "${guess}"?` : "") + ` Valid keys: [${known.join(", ")}].` + (key === "connections" ? " Connections live on the SCENE, beside \"root\" — a node cannot carry them, and one placed here does exactly nothing." : ""), { validOptions: [...known] });
3214
+ }
3215
+ }
3045
3216
  function buildNode(nj, opts, stack, parentPath, constants, bands = []) {
3046
3217
  const path = `${parentPath}/${nj.name}`;
3047
3218
  try {
3219
+ rejectUnknownKeys(nj, NODE_KEYS, "node", nj.name ?? "");
3048
3220
  if (nj.instance !== void 0 && nj.type !== void 0) throw new IncantoError("BAD_FORMAT", `Node '${nj.name}' declares both "type" and "instance" — they are mutually exclusive.`);
3049
3221
  if (nj.instance !== void 0) return buildInstance(nj, opts, stack, path, constants, bands);
3050
3222
  if (nj.type === void 0) throw new IncantoError("BAD_FORMAT", `Node '${nj.name}' must declare either "type" or "instance".`);
@@ -1,5 +1,5 @@
1
- import { P as Scene, b as Engine } from "./behavior-BXNLfIJk.js";
2
- import { i as SceneJson } from "./schema-CFeioQRE.js";
1
+ import { P as Scene, b as Engine } from "./behavior-DoFPYrgo.js";
2
+ import { i as SceneJson } from "./schema-B6ugCV1Q.js";
3
3
 
4
4
  //#region src/core/scene/loader.d.ts
5
5
  interface LoadSceneOptions {
package/dist/net.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { d as PropSchema, jt as Node } from "./behavior-BXNLfIJk.js";
2
- import { a as NetworkManagerOptions, c as LocalGameServer, d as ServerClass, f as createLocalGameServer, g as Unsubscribe, h as NetworkTransport, i as NetworkManager, l as LocalGameServerOptions, m as LoopbackTransport, n as SplitScreenPlayer, o as applySyncPatch, p as LoopbackHub, r as createSplitScreen, s as CollectionOptions, t as SplitScreenOptions, u as LocalGameServerTransport } from "./split-screen-eJUFjhi1.js";
1
+ import { d as PropSchema, jt as Node } from "./behavior-DoFPYrgo.js";
2
+ import { a as NetworkManagerOptions, c as LocalGameServer, d as ServerClass, f as createLocalGameServer, g as Unsubscribe, h as NetworkTransport, i as NetworkManager, l as LocalGameServerOptions, m as LoopbackTransport, n as SplitScreenPlayer, o as applySyncPatch, p as LoopbackHub, r as createSplitScreen, s as CollectionOptions, t as SplitScreenOptions, u as LocalGameServerTransport } from "./split-screen-DhrSzZIB.js";
3
3
 
4
4
  //#region src/net/agent8.d.ts
5
5
  /**
package/dist/net.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
2
- import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-D_i7GRcY.js";
2
+ import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-CYwDkbLF.js";
3
3
  export { LocalGameServer, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, NetworkSpawner, applySyncPatch, createAgent8Server, createLocalGameServer, createSplitScreen, registerNodesNet };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { k as diagnose } from "./loader-lQDCwNag.js";
2
+ import { k as diagnose } from "./loader-BC4PNtJX.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-D0CxCveZ.js";
4
+ import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-ibCjm-wH.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { n as jointContacts, t as withoutRapierInitNoise } from "./quiet-rapier-C6fW4zcW.js";
7
7
  //#region src/2d/physics/physics-2d.ts
@@ -66,6 +66,7 @@ var Physics2D = class {
66
66
  this.unregisterDebug = registerDebugSource(this);
67
67
  this.disconnect = engine.fixedUpdated.connect((dt) => this.step(dt));
68
68
  this.disconnectScene = engine.sceneChanged.connect(() => {
69
+ this.pruneOverlaps();
69
70
  if (engine.scene) this.syncBodies();
70
71
  });
71
72
  engine.physics = this;
@@ -306,6 +307,31 @@ var Physics2D = class {
306
307
  if (set.size === 0) this.overlaps.delete(self);
307
308
  }
308
309
  }
310
+ /**
311
+ * Forget every overlap whose node has left the tree.
312
+ *
313
+ * The lazy drop inside `overlapping()` only cleans the SETS of a key someone
314
+ * asks about, and only when they ask. This clears the KEYS, which is where the
315
+ * dead nodes were pinned.
316
+ */
317
+ /** Drop one node from the overlap map, both as a key and as a member. */
318
+ forgetOverlaps(node) {
319
+ this.overlaps.delete(node);
320
+ for (const [self, others] of this.overlaps) {
321
+ others.delete(node);
322
+ if (others.size === 0) this.overlaps.delete(self);
323
+ }
324
+ }
325
+ pruneOverlaps() {
326
+ for (const [self, others] of this.overlaps) {
327
+ if (self.tree === null) {
328
+ this.overlaps.delete(self);
329
+ continue;
330
+ }
331
+ for (const other of others) if (other.tree === null) others.delete(other);
332
+ if (others.size === 0) this.overlaps.delete(self);
333
+ }
334
+ }
309
335
  /** The bodies currently overlapping `node`. Freed nodes are dropped on read. */
310
336
  overlapping(node) {
311
337
  const set = this.overlaps.get(node);
@@ -426,6 +452,7 @@ var Physics2D = class {
426
452
  this.byColliderHandle.delete(e.collider.handle);
427
453
  this.world.removeRigidBody(e.body);
428
454
  this.entries.delete(node);
455
+ this.forgetOverlaps(node);
429
456
  node._physics = null;
430
457
  }
431
458
  }