incanto 0.60.0 → 0.61.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 (64) hide show
  1. package/bin/incanto-check.mjs +13 -0
  2. package/dist/2d.d.ts +22 -3
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +5 -5
  5. package/dist/3d.js +5 -4
  6. package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
  7. package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
  8. package/dist/{create-game-ClnIb_M5.js → create-game-DH7JI5xx.js} +76 -11
  9. package/dist/{create-game-BCm38FJV.js → create-game-IX5lEH0P.js} +21 -294
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/debug.js +1 -1
  12. package/dist/{duplicate-DJQd44CD.js → duplicate-CGqAmK2h.js} +1 -1
  13. package/dist/editor.js +2 -1
  14. package/dist/{environment-presets-8cjF3t6w.js → environment-presets-CNxCuhZF.js} +48 -13
  15. package/dist/{gameplay-BBEjPFsR.js → gameplay-Dtzd2itW.js} +53 -11
  16. package/dist/gameplay.d.ts +1 -1
  17. package/dist/gameplay.js +1 -1
  18. package/dist/index.d.ts +6 -6
  19. package/dist/index.js +7 -7
  20. package/dist/{loader-D8n7TU8W.js → loader-D7jTvDQv.js} +797 -2
  21. package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +1 -1
  24. package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
  25. package/dist/{physics-2d-DqdVp1bt.js → physics-2d-CBnor8Zf.js} +2 -2
  26. package/dist/{physics-3d-BP0DZb_1.js → physics-3d-BTUfUSWO.js} +3 -3
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-BSu2dWGC.js → register-DL3izw8j.js} +46 -9
  30. package/dist/{register-Da3hXh2H.js → register-xuSRyD6b.js} +72 -16
  31. package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
  32. package/dist/{replay-BCMK_VRP.d.ts → replay-C5x2vPF5.d.ts} +2 -2
  33. package/dist/{replay-BlNuIDdg.js → replay-D7-yle3s.js} +44 -197
  34. package/dist/{split-screen-CL5Yvxse.js → split-screen-5Ban4q4n.js} +3 -3
  35. package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
  36. package/dist/{src-DFpXBMJN.js → src-DozXvyZS.js} +1 -1
  37. package/dist/{teardown-CTTwhWSe.js → teardown-B6rwJOyS.js} +1 -1
  38. package/dist/{test-BeZ95pqw.js → test-Dch_7VQD.js} +15 -14
  39. package/dist/test.d.ts +8 -4
  40. package/dist/test.js +2 -2
  41. package/dist/vite.js +2 -2
  42. package/editor/assets/{agent8-DSJries_.js → agent8-CF1JL2tR.js} +1 -1
  43. package/editor/assets/{debug-D15Wi5TO.js → debug-3QzYhOPA.js} +1 -1
  44. package/editor/assets/{index-BjC88k97.js → index-CAD2c5ug.js} +92 -92
  45. package/editor/index.html +1 -1
  46. package/package.json +1 -1
  47. package/schemas/scene.schema.json +4 -0
  48. package/skills/incanto-3d-models.md +1 -1
  49. package/skills/incanto-assets.md +10 -1
  50. package/skills/incanto-audio.md +21 -13
  51. package/skills/incanto-behaviors-and-scripts.md +7 -3
  52. package/skills/incanto-building-2d-games.md +12 -3
  53. package/skills/incanto-editor.md +1 -1
  54. package/skills/incanto-gameplay-behaviors.md +24 -1
  55. package/skills/incanto-hud.md +5 -3
  56. package/skills/incanto-localization.md +7 -0
  57. package/skills/incanto-node-reference.md +1 -0
  58. package/skills/incanto-scene-json-authoring.md +6 -1
  59. package/templates-app/beacon-isle-3d/package.json +1 -1
  60. package/templates-app/platformer-2d/package.json +1 -1
  61. package/templates-app/star-survivor/package.json +1 -1
  62. package/templates-app/tps-3d/package.json +1 -1
  63. package/templates-app/village-quest-3d/package.json +1 -1
  64. package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
@@ -1,4 +1,4 @@
1
- import { P as Scene, b as Engine } from "./behavior-DWKTUzKI.js";
1
+ import { P as Scene, b as Engine } from "./behavior-DsgayMsH.js";
2
2
  import { i as SceneJson } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/core/scene/loader.d.ts
@@ -6,6 +6,18 @@ interface LoadSceneOptions {
6
6
  /** Resolve a sub-scene path referenced by `{instance: '<path>'}`. */
7
7
  resolveScene?: (path: string) => SceneJson;
8
8
  /**
9
+ * Assets declared ELSEWHERE in the project, for `$ref` validation.
10
+ *
11
+ * A scene a `NetworkSpawner` spawns resolves its refs against the HOST's
12
+ * `assets{}` — `remote-player.scene.json` names `$characters/base` and
13
+ * declares nothing itself, which is correct and cannot be checked from that
14
+ * one file. A tool with the whole project passes the union in; loading the
15
+ * file alone still fails, because alone it really cannot resolve.
16
+ */
17
+ declaredAssets?: Record<string, {
18
+ type?: unknown;
19
+ }>;
20
+ /**
9
21
  * Tool/preview mode (the editor's ▶ play strips behaviors — and their
10
22
  * `static signals` with them): declare each connection's signal on its
11
23
  * from-node instead of hard-failing with UNKNOWN_SIGNAL. Games should
package/dist/net.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { At as Node, d as PropSchema } from "./behavior-DWKTUzKI.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-BQ3tAsf-.js";
1
+ import { d as PropSchema, jt as Node } from "./behavior-DsgayMsH.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-D7OopelJ.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-CL5Yvxse.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-5Ban4q4n.js";
3
3
  export { LocalGameServer, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, NetworkSpawner, applySyncPatch, createAgent8Server, createLocalGameServer, createSplitScreen, registerNodesNet };
@@ -0,0 +1,28 @@
1
+ //#region src/core/pathfinding.d.ts
2
+ /**
3
+ * Grid A* — the pathfinding every top-down/RPG/tower-defense asks for and
4
+ * agents keep re-inventing badly. Pure and dimension-free: cells in, cells
5
+ * out; the caller maps cells to world units (px, meters, tiles).
6
+ */
7
+ interface PathGrid {
8
+ width: number;
9
+ height: number;
10
+ /** True = impassable. Out-of-bounds is always solid. */
11
+ solid(x: number, y: number): boolean;
12
+ }
13
+ interface FindPathOptions {
14
+ /** Allow diagonal steps (blocked from cutting corners). Default true. */
15
+ diagonal?: boolean;
16
+ /** Abort guard for huge/impossible searches. Default 20000 nodes. */
17
+ maxExpansions?: number;
18
+ }
19
+ /**
20
+ * A* from `from` to `to` (inclusive cell coords). Returns the cell path
21
+ * INCLUDING both endpoints, or null when unreachable. Straight steps cost
22
+ * 1, diagonals √2; the heuristic is octile (admissible for both modes).
23
+ */
24
+ declare function findPath(grid: PathGrid, from: [number, number], to: [number, number], opts?: FindPathOptions): [number, number][] | null;
25
+ /** Build a PathGrid from row-strings ('#' = solid) — tests and tile games. */
26
+ declare function gridFromRows(rows: readonly string[]): PathGrid;
27
+ //#endregion
28
+ export { gridFromRows as i, PathGrid as n, findPath as r, FindPathOptions as t };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { w as diagnose } from "./loader-D8n7TU8W.js";
2
+ import { k as diagnose } from "./loader-D7jTvDQv.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-BSu2dWGC.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-DL3izw8j.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { t as withoutRapierInitNoise } from "./quiet-rapier-BAJ4K94N.js";
7
7
  //#region src/2d/physics/physics-2d.ts
@@ -1,10 +1,10 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { w as diagnose } from "./loader-D8n7TU8W.js";
2
+ import { k as diagnose } from "./loader-D7jTvDQv.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { H as validateCollider3D, I as Area3D, L as CharacterBody3D, R as PhysicsBody3D, V as Node3D, z as RigidBody3D } from "./gameplay-BBEjPFsR.js";
4
+ import { H as validateCollider3D, I as Area3D, L as CharacterBody3D, R as PhysicsBody3D, V as Node3D, z as RigidBody3D } from "./gameplay-Dtzd2itW.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { t as withoutRapierInitNoise } from "./quiet-rapier-BAJ4K94N.js";
7
- import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-8cjF3t6w.js";
7
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-CNxCuhZF.js";
8
8
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
9
9
  //#region src/3d/physics/collider-lines.ts
10
10
  /**
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-DWKTUzKI.js";
1
+ import { P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-DsgayMsH.js";
2
2
  import { c as JsonValue } from "./schema-CFeioQRE.js";
3
3
  import { CSSProperties, ReactNode } from "react";
4
4
 
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-BCm38FJV.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-ClnIb_M5.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-IX5lEH0P.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-DH7JI5xx.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,8 +1,8 @@
1
- import { C as Node, b as effectiveOrder, w as diagnose } from "./loader-D8n7TU8W.js";
2
- import { I as translateOn, t as registerCoreNodes } from "./register-Da3hXh2H.js";
1
+ import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-D7jTvDQv.js";
2
+ import { I as translateOn, t as registerCoreNodes } from "./register-xuSRyD6b.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { i as getNodeSchema, l as registerNode } from "./registry-WWcQcfMr.js";
5
+ import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
6
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
7
7
  import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
8
8
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
@@ -302,7 +302,8 @@ var Joint2D = class extends Node2D {
302
302
  },
303
303
  target: {
304
304
  default: "",
305
- nodePath: true
305
+ nodePath: true,
306
+ required: true
306
307
  },
307
308
  anchor: { default: [0, 0] },
308
309
  targetAnchor: { default: [0, 0] },
@@ -344,7 +345,13 @@ const UNIT_PLANE$2 = new PlaneGeometry(1, 1);
344
345
  var Sprite2D = class extends Node2D {
345
346
  static typeName = "Sprite2D";
346
347
  static props = {
347
- texture: { default: "" },
348
+ texture: {
349
+ default: "",
350
+ asset: {
351
+ kind: "texture",
352
+ refOnly: true
353
+ }
354
+ },
348
355
  anchor: { default: [.5, .5] },
349
356
  flipX: { default: false },
350
357
  flipY: { default: false },
@@ -422,7 +429,13 @@ var AnimatedSprite2D = class extends Sprite2D {
422
429
  static typeName = "AnimatedSprite2D";
423
430
  static signals = ["animationFinished"];
424
431
  static props = {
425
- sheet: { default: "" },
432
+ sheet: {
433
+ default: "",
434
+ asset: {
435
+ kind: "spritesheet",
436
+ refOnly: true
437
+ }
438
+ },
426
439
  animations: { default: {} },
427
440
  autoplay: { default: "" }
428
441
  };
@@ -828,7 +841,14 @@ var Label = class extends Node2D {
828
841
  fontSize: { default: 16 },
829
842
  color: { default: "#ffffff" },
830
843
  font: { default: "monospace" },
831
- align: { default: "left" }
844
+ align: { default: "left" },
845
+ opacity: {
846
+ default: 1,
847
+ range: {
848
+ min: 0,
849
+ max: 1
850
+ }
851
+ }
832
852
  };
833
853
  text = "";
834
854
  fontSize = 16;
@@ -836,6 +856,16 @@ var Label = class extends Node2D {
836
856
  font = "monospace";
837
857
  /** 'left' | 'center' | 'right' — anchor of the text block on the node origin. */
838
858
  align = "left";
859
+ /**
860
+ * 0..1, like `Sprite2D` and `ColorRect2D`.
861
+ *
862
+ * Label was the one 2D drawable without it, and the gap showed up through
863
+ * `FloatAway` — whose own doc names "a 2D `Label`" as a node it works on.
864
+ * `FloatAway` writes `opacity` when the node has one, so on a Label the
865
+ * number rose to full height at FULL opacity and then vanished in a single
866
+ * frame: the pop that behavior exists to avoid.
867
+ */
868
+ opacity = 1;
839
869
  quadMesh = null;
840
870
  canvas = null;
841
871
  lastKey = "";
@@ -863,7 +893,8 @@ var Label = class extends Node2D {
863
893
  this.lastKey = key;
864
894
  this.rasterize();
865
895
  }
866
- quad.visible = true;
896
+ quad.visible = this.opacity > 0;
897
+ quad.material.opacity = this.opacity;
867
898
  quad.renderOrder = this.effectiveRenderOrder;
868
899
  }
869
900
  rasterize() {
@@ -1306,7 +1337,13 @@ var TileMap2D = class extends Node2D {
1306
1337
  static typeName = "TileMap2D";
1307
1338
  static props = {
1308
1339
  /** Atlas texture asset ref; tiles are tileSize×tileSize squares in it. */
1309
- texture: { default: "" },
1340
+ texture: {
1341
+ default: "",
1342
+ asset: {
1343
+ kind: "texture",
1344
+ refOnly: true
1345
+ }
1346
+ },
1310
1347
  tileSize: { default: 32 },
1311
1348
  /** Atlas columns; 0 = derive from the texture width. */
1312
1349
  columns: { default: 0 },
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { C as Node, N as Signal, S as InputMap, g as restoreBehaviors, h as captureBehaviors, v as createSaveStore, w as diagnose, x as resolveOrderGroups, y as ORDER_GROUP_BASE } from "./loader-D8n7TU8W.js";
2
+ import { C as ORDER_GROUP_BASE, E as InputMap, O as Node, S as createSaveStore, T as resolveOrderGroups, V as Signal, b as restoreBehaviors, k as diagnose, y as captureBehaviors } from "./loader-D7jTvDQv.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { l as registerNode } from "./registry-WWcQcfMr.js";
5
+ import { l as registerNode } from "./registry-CF70EArN.js";
6
6
  //#region src/core/audio/buses.ts
7
7
  const AUDIO_LOG_LIMIT = 200;
8
8
  var AudioBuses = class {
@@ -612,6 +612,16 @@ const SFX_PRESETS = {
612
612
  }
613
613
  };
614
614
  const SFX_PRESET_NAMES = Object.keys(SFX_PRESETS);
615
+ /**
616
+ * How long a preset lasts, in seconds — the same `attack + sustain + decay`
617
+ * `synthSfx` uses to size its buffer.
618
+ *
619
+ * `pitch` deliberately does not enter into it: it is a frequency multiplier,
620
+ * not a playback rate, so a preset played an octave up is the same length.
621
+ */
622
+ function sfxDuration(params) {
623
+ return params.attack + params.sustain + params.decay;
624
+ }
615
625
  const TWO_PI = Math.PI * 2;
616
626
  /**
617
627
  * Render a preset to mono PCM in [-1, 1]. PURE: identical inputs → identical
@@ -1369,6 +1379,14 @@ var HudLayer = class extends Node {
1369
1379
  * widget (an icon inside a slot), so the walk upward is the point.
1370
1380
  */
1371
1381
  const widgetOf = /* @__PURE__ */ new WeakMap();
1382
+ /** The nearest ancestor widget that actually accepts a drop. */
1383
+ function dropTargetFromElement(el) {
1384
+ for (let n = el; n; n = n.parentElement) {
1385
+ const found = widgetOf.get(n);
1386
+ if (found?.dropTarget) return found;
1387
+ }
1388
+ return null;
1389
+ }
1372
1390
  function widgetFromElement(el) {
1373
1391
  for (let n = el; n; n = n.parentElement) {
1374
1392
  const found = widgetOf.get(n);
@@ -1378,6 +1396,8 @@ function widgetFromElement(el) {
1378
1396
  }
1379
1397
  /** The drag in flight, if any. One pointer, one drag. */
1380
1398
  let dragging = null;
1399
+ /** The drop target the dragged widget came OUT of — putting it back is a no-op. */
1400
+ let draggingFrom = null;
1381
1401
  /** Shared plumbing: mount into the parent HudLayer's anchor slot. */
1382
1402
  var HudWidgetBase = class extends Node {
1383
1403
  /**
@@ -1495,6 +1515,7 @@ var HudWidgetBase = class extends Node {
1495
1515
  event.preventDefault();
1496
1516
  el.setPointerCapture?.(event.pointerId);
1497
1517
  dragging = this;
1518
+ draggingFrom = dropTargetFromElement(el.parentElement);
1498
1519
  el.style.opacity = "0.6";
1499
1520
  this.emit("dragStarted", this);
1500
1521
  });
@@ -1502,9 +1523,12 @@ var HudWidgetBase = class extends Node {
1502
1523
  if (dragging !== this) return;
1503
1524
  el.style.opacity = "";
1504
1525
  el.releasePointerCapture?.(event.pointerId);
1505
- const target = widgetFromElement(document.elementFromPoint?.(event.clientX, event.clientY) ?? null);
1526
+ const under = document.elementFromPoint?.(event.clientX, event.clientY) ?? null;
1527
+ const target = dropTargetFromElement(under) ?? widgetFromElement(under);
1528
+ const origin = draggingFrom;
1506
1529
  dragging = null;
1507
- if (target && target !== this && target.dropTarget) {
1530
+ draggingFrom = null;
1531
+ if (target && target !== this && target !== origin && target.dropTarget) {
1508
1532
  this.emit("droppedOn", target);
1509
1533
  target.emit("dropped", this);
1510
1534
  } else this.emit("dragCancelled", this);
@@ -2463,6 +2487,7 @@ var Engine = class {
2463
2487
  this.input.clear();
2464
2488
  this._time = 0;
2465
2489
  this._unscaledTime = 0;
2490
+ this.timeScale = 1;
2466
2491
  if (scene.input) this.input.declare(scene.input);
2467
2492
  if (scene.strings) this.locale.load(scene.strings);
2468
2493
  this.orderGroups = resolveOrderGroups(scene.orderGroups);
@@ -2744,7 +2769,7 @@ function createElement() {
2744
2769
  }
2745
2770
  /**
2746
2771
  * One sound. `play()` (or `autoplay: true`) starts it; `finished` fires when
2747
- * it ends. Browsers block audio before the first user gesture — a blocked
2772
+ * it ends — for a procedural preset as well as a `src` clip. Browsers block audio before the first user gesture — a blocked
2748
2773
  * play marks `pendingGesture`; `createGame` retries pending players on the
2749
2774
  * first pointer/key gesture automatically.
2750
2775
  *
@@ -2801,10 +2826,12 @@ var AudioPlayer = class extends Node {
2801
2826
  /** Start on the first frame in the tree (subject to the gesture policy). */
2802
2827
  autoplay = false;
2803
2828
  /**
2804
- * 3D positional audio: in a 3D scene the sound pans + attenuates by the
2805
- * emitter's world position relative to the active Camera3D (the listener).
2806
- * Default false → identical non-spatial behavior (back-compat). In a 2D scene
2807
- * spatial is currently ignored (the adapter feeds no pose) — see incanto-audio.
2829
+ * Positional audio: the sound pans + attenuates by the emitter's world
2830
+ * position relative to the active camera (the listener). Default false →
2831
+ * identical non-spatial behavior.
2832
+ *
2833
+ * Both dimensions. A 2D scene measures in PIXELS, so the distances below —
2834
+ * metre-shaped defaults — need setting there; `auditScene` says so.
2808
2835
  */
2809
2836
  spatial = false;
2810
2837
  /** Distance at which spatial gain is full; closer never gets louder. */
@@ -2838,6 +2865,21 @@ var AudioPlayer = class extends Node {
2838
2865
  loadError = null;
2839
2866
  element = null;
2840
2867
  _playing = false;
2868
+ /**
2869
+ * `engine.unscaledTime` at which the preset currently sounding ends.
2870
+ *
2871
+ * A preset used to be fire-and-forget: `playing` stayed false through a sound
2872
+ * that was audibly playing, and `finished` — a signal this node DECLARES, so
2873
+ * a connection to it loads clean — never fired at all. Wiring "when the coin
2874
+ * chime ends, free the pickup" to a preset player produced a wire that is
2875
+ * dead for the life of the game and says nothing.
2876
+ *
2877
+ * The end is known exactly: `attack + sustain + decay` is the length
2878
+ * `synthSfx` sizes its buffer to. Measured on the UNSCALED clock because a
2879
+ * sound does not slow down when the game does, and a paused game still hears
2880
+ * the tail of the hit that paused it.
2881
+ */
2882
+ presetEndsAt = null;
2841
2883
  autoplayed = false;
2842
2884
  /** Last spatial pose pushed by the 3D adapter (null until/unless spatial). */
2843
2885
  _spatialPose = null;
@@ -2940,15 +2982,18 @@ var AudioPlayer = class extends Node {
2940
2982
  }
2941
2983
  /**
2942
2984
  * Procedural-SFX path: synthesize the preset and fire it through WebAudio
2943
- * (low-latency, overlap-friendly). Fire-and-forget — no `finished`/`playing`
2944
- * tracking (these are short one-shots, not the element-driven `src` clip).
2945
- * Headless (no AudioContext) → silent no-op. WebAudio also needs a gesture;
2946
- * a suspended context is resumed by the same unlock listener as `src` players.
2985
+ * (low-latency, overlap-friendly). Headless (no AudioContext) plays nothing —
2986
+ * but still reports `playing` and still emits `finished` on schedule, because
2987
+ * the length is arithmetic, not a device. WebAudio needs a gesture; a
2988
+ * suspended context is resumed by the same unlock listener as `src` players.
2947
2989
  */
2948
2990
  playPreset() {
2949
2991
  const params = SFX_PRESETS[this.preset];
2950
2992
  if (!params) return;
2951
- const sfx = this.tree?.engine?.sfx;
2993
+ const engine = this.tree?.engine;
2994
+ this._playing = true;
2995
+ this.presetEndsAt = (engine?.unscaledTime ?? 0) + sfxDuration(params);
2996
+ const sfx = engine?.sfx;
2952
2997
  if (!sfx) return;
2953
2998
  sfx.play(params, this.gain(), {
2954
2999
  pitch: this.pitch,
@@ -2974,6 +3019,7 @@ var AudioPlayer = class extends Node {
2974
3019
  this.element.pause();
2975
3020
  this.element.currentTime = 0;
2976
3021
  }
3022
+ this.presetEndsAt = null;
2977
3023
  this._playing = false;
2978
3024
  this.pendingGesture = false;
2979
3025
  }
@@ -2986,6 +3032,13 @@ var AudioPlayer = class extends Node {
2986
3032
  this.autoplayed = true;
2987
3033
  this.play();
2988
3034
  }
3035
+ if (this.presetEndsAt !== null) {
3036
+ if ((this.tree?.engine?.unscaledTime ?? 0) >= this.presetEndsAt) {
3037
+ this.presetEndsAt = null;
3038
+ this._playing = false;
3039
+ this.emit("finished");
3040
+ }
3041
+ }
2989
3042
  if (this.element && this._playing) {
2990
3043
  this.element.volume = this.gain() * this.spatialElementGain();
2991
3044
  this.element.loop = this.loop;
@@ -3372,7 +3425,10 @@ var UiImage = class extends HudWidgetBase {
3372
3425
  static typeName = "UiImage";
3373
3426
  static props = {
3374
3427
  ...HudWidgetBase.props,
3375
- src: { default: "" },
3428
+ src: {
3429
+ default: "",
3430
+ asset: { kind: "texture" }
3431
+ },
3376
3432
  width: { default: 48 },
3377
3433
  height: { default: 48 },
3378
3434
  /** How the picture fills its box — `contain` never crops. */
@@ -3985,4 +4041,4 @@ function registerCoreNodes() {
3985
4041
  registerNode(UiMuteToggle);
3986
4042
  }
3987
4043
  //#endregion
3988
- export { UiBar as A, ROLLOFF_MODELS as B, readDeviceHints as C, HudLayer as D, EffectLog as E, suggestLocale as F, synthSfx as G, spatialPan as H, translateOn as I, crossfadeGains as J, MusicManager as K, translationKey as L, BASE_LOCALE as M, Localization as N, HudWidgetBase as O, T_PREFIX as P, SfxEngine as R, qualityRendering as S, LogManager as T, SFX_PRESETS as U, spatialGain as V, SFX_PRESET_NAMES as W, AudioBuses as X, fadeGain as Y, Engine as _, UiMuteToggle as a, qualityCaps as b, UiRenderScaleSelect as c, UiToggle as d, UiVolumeSlider as f, AudioPlayer as g, UiDialogue as h, UiLanguageSelect as i, UiText as j, UiBanner as k, UiSelect as l, UiButton as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, WebAudioMusicBackend as q, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, engine_exports as v, suggestQuality as w, qualityEnvironment as x, Settings as y, isAudioContextAvailable as z };
4044
+ export { UiBar as A, ROLLOFF_MODELS as B, readDeviceHints as C, HudLayer as D, EffectLog as E, suggestLocale as F, sfxDuration as G, spatialPan as H, translateOn as I, WebAudioMusicBackend as J, synthSfx as K, translationKey as L, BASE_LOCALE as M, Localization as N, HudWidgetBase as O, T_PREFIX as P, SfxEngine as R, qualityRendering as S, LogManager as T, SFX_PRESETS as U, spatialGain as V, SFX_PRESET_NAMES as W, fadeGain as X, crossfadeGains as Y, AudioBuses as Z, Engine as _, UiMuteToggle as a, qualityCaps as b, UiRenderScaleSelect as c, UiToggle as d, UiVolumeSlider as f, AudioPlayer as g, UiDialogue as h, UiLanguageSelect as i, UiText as j, UiBanner as k, UiSelect as l, UiButton as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, MusicManager as q, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, engine_exports as v, suggestQuality as w, qualityEnvironment as x, Settings as y, isAudioContextAvailable as z };
@@ -1,5 +1,35 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
2
  import { r as jsonKind, t as jsonClone } from "./json-CwwhxQgb.js";
3
+ //#region src/core/prop-range.ts
4
+ function describe(range) {
5
+ const { min, max, exclusiveMin, allow } = range;
6
+ let body;
7
+ if (min !== void 0 && max !== void 0) body = `${min}..${max}`;
8
+ else if (min !== void 0) body = exclusiveMin ? `greater than ${min}` : `at least ${min}`;
9
+ else if (max !== void 0) body = `at most ${max}`;
10
+ else body = "a finite number";
11
+ return allow && allow.length > 0 ? `${body} (or ${allow.join(", ")})` : body;
12
+ }
13
+ /**
14
+ * Throws `PROP_TYPE_MISMATCH` when a number falls outside its documented range.
15
+ *
16
+ * A non-number is ignored: the kind check has already run and reporting the
17
+ * same value twice, under two codes, tells its reader nothing new.
18
+ *
19
+ * @param value the authored value
20
+ * @param label `Type.prop`, for the message
21
+ * @param range what the prop documents
22
+ */
23
+ function validatePropRange(value, label, range) {
24
+ if (typeof value !== "number") return;
25
+ if (range.allow?.includes(value)) return;
26
+ if (!Number.isFinite(value)) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${label}' must be a finite number, got ${value}.`, { prop: label });
27
+ const belowMin = range.min !== void 0 && (range.exclusiveMin ? value <= range.min : value < range.min);
28
+ const aboveMax = range.max !== void 0 && value > range.max;
29
+ if (!belowMin && !aboveMax) return;
30
+ throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${label}' must be ${describe(range)}, got ${value}.`, { prop: label });
31
+ }
32
+ //#endregion
3
33
  //#region src/core/registry.ts
4
34
  /**
5
35
  * Validate a prop value against its schema default:
@@ -120,7 +150,10 @@ function mergeStaticAliases(ctor) {
120
150
  */
121
151
  function applySchemaProps(target, schema, props, label, aliases = {}, extraOptions = {}) {
122
152
  for (const [key, def] of Object.entries(schema)) target[key] = jsonClone(def.default);
123
- if (!props) return;
153
+ if (props) applyGivenProps(target, schema, props, label, aliases, extraOptions);
154
+ requireNonEmpty(target, schema, label);
155
+ }
156
+ function applyGivenProps(target, schema, props, label, aliases, extraOptions) {
124
157
  for (const [rawKey, value] of Object.entries(props)) {
125
158
  const key = schema[rawKey] ? rawKey : aliases[rawKey] ?? rawKey;
126
159
  const def = schema[key];
@@ -135,18 +168,37 @@ function applySchemaProps(target, schema, props, label, aliases = {}, extraOptio
135
168
  const extra = extraOptions[key];
136
169
  const options = def.options && extra && extra.length > 0 ? [...def.options, ...extra] : def.options;
137
170
  validatePropValue(value, def.default, `${label}.${key}`, options);
171
+ if (def.range) validatePropRange(value, `${label}.${key}`, def.range);
138
172
  target[key] = jsonClone(value);
139
173
  }
174
+ }
175
+ function requireNonEmpty(target, schema, label) {
140
176
  for (const [key, def] of Object.entries(schema)) {
141
177
  if (!def.required) continue;
142
- const value = target[key];
143
- if (value === "" || value === void 0 || value === null) throw new IncantoError("PROP_TYPE_MISMATCH", `'${label}' needs a "${key}" — it is empty.`, {
178
+ if (!isEmptyValue(target[key])) continue;
179
+ throw new IncantoError("PROP_TYPE_MISMATCH", `'${label}' needs a "${key}" — it is empty.`, {
144
180
  prop: key,
145
181
  nodeType: label
146
182
  });
147
183
  }
148
184
  }
149
185
  /**
186
+ * "Not a configuration" — what `required` refuses.
187
+ *
188
+ * The first cut only knew about `''`, which is what the two props that
189
+ * introduced the flag hold. Three of the ones declared since default to an
190
+ * ARRAY — `MoveTo.to`, `Patrol.points`, `WaveSpawner.waves` — and for those the
191
+ * flag was inert: a `MoveTo` with no `to` loaded clean and moved nowhere, which
192
+ * is exactly the failure the flag was added to stop. Found by running the
193
+ * shipped package as a consumer instead of trusting the declaration.
194
+ */
195
+ function isEmptyValue(value) {
196
+ if (value === "" || value === void 0 || value === null) return true;
197
+ if (Array.isArray(value)) return value.length === 0;
198
+ if (typeof value === "object") return Object.keys(value).length === 0;
199
+ return false;
200
+ }
201
+ /**
150
202
  * Instantiate a registered type, assign schema defaults (cloned), then the
151
203
  * given props — validating every key and its JSON kind against the default.
152
204
  */
@@ -1,4 +1,4 @@
1
- import { b as Engine } from "./behavior-DWKTUzKI.js";
1
+ import { b as Engine } from "./behavior-DsgayMsH.js";
2
2
  import { s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/core/audit.d.ts
@@ -25,7 +25,7 @@ declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[];
25
25
  * Incanto fails loudly with machine-readable codes and messages that list valid
26
26
  * alternatives — AI agents self-correct on hard failures, not warnings.
27
27
  */
28
- type IncantoErrorCode = "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE";
28
+ type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE";
29
29
  /**
30
30
  * Structured context mirrored from the prose message — tools and agents parse
31
31
  * THESE instead of regexing the message (which is for humans and may reword).