incanto 0.74.0 → 0.76.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 (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +861 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +29 -8
  54. package/skills/incanto-environment.md +257 -14
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +398 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -1,5 +1,5 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { B as HudLayer, F as resolveOrderGroups, G as UiText, H as TAPPABLE, I as InputMap, J as Node, N as ORDER_GROUP_BASE, U as UiBanner, V as HudWidgetBase, W as UiBar, Y as diagnose, a as restoreBehaviors, ct as Signal, i as captureBehaviors, j as createSaveStore, q as requireText, t as ENGINE_STATE_KEY, z as FITS } from "./save-slots-CEUGeLuB.js";
2
+ import { $ as Node, B as resolveOrderGroups, G as HudLayer, I as createSaveStore, J as UiBanner, K as HudWidgetBase, Q as requireText, R as ORDER_GROUP_BASE, V as InputMap, W as FITS, X as UiText, Y as UiBar, a as ENGINE_STATE_KEY, c as captureBehaviors, et as diagnose, i as worldPosition, l as restoreBehaviors, pt as Signal, q as TAPPABLE, r as worldDirection, t as hasPosition } from "./pose-ByFB_J3O.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-CDOMybym.js";
5
5
  import { p as registerNode } from "./touch-CioTZB-y.js";
@@ -2714,6 +2714,99 @@ var AudioPlayer = class extends Node {
2714
2714
  this._spatialPose = pose;
2715
2715
  }
2716
2716
  /**
2717
+ * Where this sound arrives from, for the record: gain, side and distance.
2718
+ *
2719
+ * The renderer feeds a pose every frame it draws; headless there is none, and
2720
+ * that is exactly where the question matters — a harness asking "was the bell
2721
+ * on my left" had nothing to read. So when no pose has been pushed, one is
2722
+ * computed HERE, from the scene's own current camera: the same listener the
2723
+ * panner would use, without a renderer to ask.
2724
+ *
2725
+ * Null when the sound is not spatial, or when the scene has no camera to hear
2726
+ * it — the record then says nothing rather than inventing a centred sound.
2727
+ */
2728
+ spatialAnswer() {
2729
+ if (!this.spatial) return null;
2730
+ const pose = this._spatialPose ?? this.poseFromScene();
2731
+ if (!pose) return null;
2732
+ const [sx, sy, sz] = pose.position;
2733
+ const [lx, ly, lz] = pose.listener.position;
2734
+ const distance = Math.hypot(sx - lx, sy - ly, sz - lz);
2735
+ return {
2736
+ gain: spatialGain(distance, {
2737
+ refDistance: this.refDistance,
2738
+ maxDistance: this.maxDistance,
2739
+ rolloff: this.rolloff
2740
+ }),
2741
+ pan: spatialPan(pose.position, pose.listener),
2742
+ distance
2743
+ };
2744
+ }
2745
+ /** The listener the scene implies: its current camera, posed from the tree. */
2746
+ poseFromScene() {
2747
+ const root = this.tree?.root;
2748
+ if (!root) return null;
2749
+ let camera = null;
2750
+ const walk = (node) => {
2751
+ if (camera) return;
2752
+ if ((node.constructor.typeName ?? "") === "Camera3D" && node.current === true) {
2753
+ camera = node;
2754
+ return;
2755
+ }
2756
+ for (const child of node.children) walk(child);
2757
+ };
2758
+ walk(root);
2759
+ if (!camera) return null;
2760
+ const cam = camera;
2761
+ const at = worldPosition(cam);
2762
+ if (at.length < 3) return null;
2763
+ const forward = worldDirection(cam, [
2764
+ 0,
2765
+ 0,
2766
+ -1
2767
+ ]);
2768
+ const up = worldDirection(cam, [
2769
+ 0,
2770
+ 1,
2771
+ 0
2772
+ ]);
2773
+ const emitter = this.emitterPosition();
2774
+ if (!emitter) return null;
2775
+ return {
2776
+ position: emitter,
2777
+ listener: {
2778
+ position: [
2779
+ at[0] ?? 0,
2780
+ at[1] ?? 0,
2781
+ at[2] ?? 0
2782
+ ],
2783
+ forward: [
2784
+ forward[0] ?? 0,
2785
+ forward[1] ?? 0,
2786
+ forward[2] ?? -1
2787
+ ],
2788
+ up: [
2789
+ up[0] ?? 0,
2790
+ up[1] ?? 1,
2791
+ up[2] ?? 0
2792
+ ]
2793
+ }
2794
+ };
2795
+ }
2796
+ /** Where this player SOUNDS from: its nearest spatial ancestor (or itself). */
2797
+ emitterPosition() {
2798
+ for (let at = this; at; at = at.parent) {
2799
+ if (!hasPosition(at)) continue;
2800
+ const world = worldPosition(at);
2801
+ if (world.length >= 3) return [
2802
+ world[0] ?? 0,
2803
+ world[1] ?? 0,
2804
+ world[2] ?? 0
2805
+ ];
2806
+ }
2807
+ return null;
2808
+ }
2809
+ /**
2717
2810
  * Write this sound into the engine's audio record.
2718
2811
  *
2719
2812
  * The CALL happens even where the backend does not (headless, the verify VM),
@@ -2722,6 +2815,7 @@ var AudioPlayer = class extends Node {
2722
2815
  */
2723
2816
  note(kind, name) {
2724
2817
  const engine = this.tree?.engine;
2818
+ const where = this.spatialAnswer();
2725
2819
  engine?.audio.record({
2726
2820
  kind,
2727
2821
  name,
@@ -2729,6 +2823,7 @@ var AudioPlayer = class extends Node {
2729
2823
  wiredAs: this.wiringPath(),
2730
2824
  bus: this.bus === "music" ? "music" : "sfx",
2731
2825
  at: engine.time,
2826
+ ...where ?? {},
2732
2827
  ...this.loop ? { loop: true } : {}
2733
2828
  });
2734
2829
  }
@@ -3034,11 +3129,22 @@ var UiDialogue = class extends HudWidgetBase {
3034
3129
  ]
3035
3130
  },
3036
3131
  charsPerSecond: { default: 40 },
3132
+ /**
3133
+ * A line moves on by itself this long after it is fully shown (0 = wait
3134
+ * for the click). Cutscenes have nobody clicking — the camera is on a rail
3135
+ * and the controller is off. A line with choices always waits.
3136
+ */
3137
+ autoAdvanceSeconds: {
3138
+ default: 0,
3139
+ range: { min: 0 }
3140
+ },
3037
3141
  width: { default: 520 }
3038
3142
  };
3039
3143
  anchor = "bottom";
3040
3144
  /** Typewriter speed; 0 = instant lines. */
3041
3145
  charsPerSecond = 40;
3146
+ autoAdvanceSeconds = 0;
3147
+ shownFor = 0;
3042
3148
  width = 520;
3043
3149
  queue = [];
3044
3150
  current = null;
@@ -3190,6 +3296,7 @@ var UiDialogue = class extends HudWidgetBase {
3190
3296
  this.current = this.queue.shift();
3191
3297
  this.resolveCurrent();
3192
3298
  this.revealed = this.charsPerSecond <= 0 ? this.current.text.length : 0;
3299
+ this.shownFor = 0;
3193
3300
  this.emit("lineShown", this.current.text);
3194
3301
  if (this._element) {
3195
3302
  if (this.speakerEl) this.speakerEl.textContent = this.current.speaker;
@@ -3220,6 +3327,10 @@ var UiDialogue = class extends HudWidgetBase {
3220
3327
  });
3221
3328
  this.choicesEl?.appendChild(b);
3222
3329
  });
3330
+ if (this.autoAdvanceSeconds > 0 && this.current && !this.current.choices && this.revealed >= this.current.text.length) {
3331
+ this.shownFor += dt;
3332
+ if (this.shownFor >= this.autoAdvanceSeconds) this.advance();
3333
+ }
3223
3334
  }
3224
3335
  };
3225
3336
  //#endregion
@@ -4206,6 +4317,487 @@ var UiMuteToggle = class extends UiToggle {
4206
4317
  }
4207
4318
  };
4208
4319
  //#endregion
4320
+ //#region src/core/nodes/ui-minimap.ts
4321
+ const DEG = Math.PI / 180;
4322
+ /** How often the tree is re-scanned for the followed node and the groups (frames). */
4323
+ const SCAN_FRAMES = 30;
4324
+ /**
4325
+ * A minimap — the world around one node, top-down, as dots on a disc.
4326
+ *
4327
+ * ```json
4328
+ * { "name": "Map", "type": "UiMinimap",
4329
+ * "props": { "anchor": "topRight", "size": 160, "radius": 40,
4330
+ * "dots": { "enemy": "#ff5050", "pickup": "#ffd54f", "goal": "#3aa0ff" } } }
4331
+ * ```
4332
+ *
4333
+ * It follows the first node in the `player` group unless `follow` names
4334
+ * one, shows every node in the groups listed in `dots` within `radius` world units
4335
+ * (metres in 3D, px in 2D — the plane is x/z in 3D and x/y in 2D), and can
4336
+ * turn with a `heading` node so "ahead" is up. Everything it draws is a
4337
+ * number first: `markers()` is the same list headless, so a harness can ask
4338
+ * what the map shows.
4339
+ */
4340
+ var UiMinimap = class extends HudWidgetBase {
4341
+ static typeName = "UiMinimap";
4342
+ static props = {
4343
+ ...HudWidgetBase.props,
4344
+ /** Diameter (px). */
4345
+ size: { default: 160 },
4346
+ /** World units shown from the centre to the rim (m in 3D, px in 2D). */
4347
+ radius: { default: 40 },
4348
+ /** The node at the centre; '' = the first node in the `player` group. */
4349
+ follow: {
4350
+ default: "",
4351
+ nodePath: true
4352
+ },
4353
+ /** Group → colour: every node in a listed group is a dot. (Not `groups` — that is the node's own membership.) */
4354
+ dots: { default: {} },
4355
+ /** A node whose yaw turns the map so its "ahead" is up (+Z-forward); '' = north (−z) up. */
4356
+ heading: {
4357
+ default: "",
4358
+ nodePath: true
4359
+ },
4360
+ /** Colour of the followed node's own marker. */
4361
+ self: { default: "#ffffff" },
4362
+ /** Marker diameter (px). */
4363
+ dotSize: { default: 6 },
4364
+ background: { default: "rgba(0,0,0,0.45)" },
4365
+ shape: {
4366
+ default: "circle",
4367
+ options: ["circle", "square"]
4368
+ }
4369
+ };
4370
+ size = 160;
4371
+ radius = 40;
4372
+ follow = "";
4373
+ dots = {};
4374
+ heading = "";
4375
+ self = "#ffffff";
4376
+ dotSize = 6;
4377
+ background = "rgba(0,0,0,0.45)";
4378
+ shape = "circle";
4379
+ canvas = null;
4380
+ centreNode = null;
4381
+ members = /* @__PURE__ */ new Map();
4382
+ scanIn = 0;
4383
+ /**
4384
+ * Loader hook: `dots` is a name map — its KEYS are the author's group names,
4385
+ * so any key is right — and every VALUE must be a colour string. A number or
4386
+ * an object where a colour should be drew nothing, silently.
4387
+ */
4388
+ static validateJson(node) {
4389
+ const dots = node.dots;
4390
+ if (typeof dots !== "object" || dots === null || Array.isArray(dots)) throw new IncantoError("PROP_TYPE_MISMATCH", `UiMinimap '${node.name}': "dots" must be an object of group → colour, got ${JSON.stringify(dots)}.`, { prop: "dots" });
4391
+ for (const [group, color] of Object.entries(dots)) if (typeof color !== "string" || color === "") throw new IncantoError("PROP_TYPE_MISMATCH", `UiMinimap '${node.name}': dots.${group} must be a colour string (like "#ff5050"), got ${JSON.stringify(color)}.`, { prop: "dots" });
4392
+ }
4393
+ /** The followed node, or null when nothing is in the `player` group either. */
4394
+ centre() {
4395
+ this.rescan(false);
4396
+ return this.centreNode;
4397
+ }
4398
+ /** Every dot on the map right now, in canvas px — the followed node is not among them. */
4399
+ markers() {
4400
+ this.rescan(false);
4401
+ const centre = this.centreNode;
4402
+ if (!centre) return [];
4403
+ const c = worldOf$1(centre);
4404
+ const is3d = c.length >= 3;
4405
+ const half = this.size / 2;
4406
+ const scale = this.radius > 0 ? half / this.radius : 0;
4407
+ const yaw = this.headingYaw();
4408
+ const out = [];
4409
+ for (const [group, color] of Object.entries(this.dots)) for (const node of this.members.get(group) ?? []) {
4410
+ if (node === centre || node.tree === null) continue;
4411
+ const p = worldOf$1(node);
4412
+ const dx = (p[0] ?? 0) - (c[0] ?? 0);
4413
+ const d2 = is3d ? (p[2] ?? 0) - (c[2] ?? 0) : (p[1] ?? 0) - (c[1] ?? 0);
4414
+ if (Math.hypot(dx, d2) > this.radius) continue;
4415
+ let x;
4416
+ let y;
4417
+ if (yaw === null) {
4418
+ x = half + dx * scale;
4419
+ y = half + d2 * scale;
4420
+ } else {
4421
+ const ux = Math.sin(yaw);
4422
+ const uz = Math.cos(yaw);
4423
+ const rx = -uz;
4424
+ const rz = ux;
4425
+ x = half + (dx * rx + d2 * rz) * scale;
4426
+ y = half - (dx * ux + d2 * uz) * scale;
4427
+ }
4428
+ out.push({
4429
+ x,
4430
+ y,
4431
+ color: String(color),
4432
+ group,
4433
+ node
4434
+ });
4435
+ }
4436
+ return out;
4437
+ }
4438
+ /** The heading's yaw in radians, or null for north-up. */
4439
+ headingYaw() {
4440
+ if (this.heading === "") return null;
4441
+ const h = this.getNodeOrNull(this.heading);
4442
+ if (!h) return null;
4443
+ const r = h.rotation;
4444
+ if (typeof r === "number") return r * DEG;
4445
+ return (r?.[1] ?? 0) * DEG;
4446
+ }
4447
+ rescan(force) {
4448
+ if (!force && this.scanIn > 0 && this.centreNode && this.centreNode.tree !== null) {
4449
+ this.scanIn -= 1;
4450
+ return;
4451
+ }
4452
+ this.scanIn = SCAN_FRAMES;
4453
+ const root = this.getRoot();
4454
+ const tree = this.tree;
4455
+ this.centreNode = this.follow !== "" ? this.getNodeOrNull(this.follow) : tree?.getNodesInGroup("player")[0] ?? null;
4456
+ this.members.clear();
4457
+ for (const group of Object.keys(this.dots)) this.members.set(group, tree ? tree.getNodesInGroup(group) : collectGroup(root, group));
4458
+ }
4459
+ _build() {
4460
+ const canvas = document.createElement("canvas");
4461
+ canvas.width = this.size;
4462
+ canvas.height = this.size;
4463
+ canvas.style.cssText = `width:${this.size}px;height:${this.size}px;display:block;`;
4464
+ this.canvas = canvas;
4465
+ return canvas;
4466
+ }
4467
+ _sync() {
4468
+ const canvas = this.canvas;
4469
+ if (!canvas) return;
4470
+ if (canvas.width !== this.size) {
4471
+ canvas.width = this.size;
4472
+ canvas.height = this.size;
4473
+ canvas.style.width = `${this.size}px`;
4474
+ canvas.style.height = `${this.size}px`;
4475
+ }
4476
+ const ctx = canvas.getContext("2d");
4477
+ if (!ctx) return;
4478
+ const s = this.size;
4479
+ const half = s / 2;
4480
+ ctx.clearRect(0, 0, s, s);
4481
+ ctx.save();
4482
+ if (this.shape === "circle") {
4483
+ ctx.beginPath();
4484
+ ctx.arc(half, half, half, 0, Math.PI * 2);
4485
+ ctx.clip();
4486
+ }
4487
+ ctx.fillStyle = this.background;
4488
+ ctx.fillRect(0, 0, s, s);
4489
+ ctx.strokeStyle = "rgba(255,255,255,0.25)";
4490
+ ctx.lineWidth = 1;
4491
+ ctx.beginPath();
4492
+ ctx.moveTo(half, 0);
4493
+ ctx.lineTo(half, s);
4494
+ ctx.moveTo(0, half);
4495
+ ctx.lineTo(s, half);
4496
+ ctx.stroke();
4497
+ const r = Math.max(1, this.dotSize / 2);
4498
+ for (const m of this.markers()) {
4499
+ ctx.fillStyle = m.color;
4500
+ ctx.beginPath();
4501
+ ctx.arc(m.x, m.y, r, 0, Math.PI * 2);
4502
+ ctx.fill();
4503
+ }
4504
+ if (this.centreNode) {
4505
+ ctx.fillStyle = this.self;
4506
+ ctx.beginPath();
4507
+ ctx.moveTo(half, half - r * 1.6);
4508
+ ctx.lineTo(half + r * 1.1, half + r * 1.1);
4509
+ ctx.lineTo(half - r * 1.1, half + r * 1.1);
4510
+ ctx.closePath();
4511
+ ctx.fill();
4512
+ }
4513
+ ctx.restore();
4514
+ if (this.shape === "circle") {
4515
+ ctx.strokeStyle = "rgba(255,255,255,0.5)";
4516
+ ctx.lineWidth = 2;
4517
+ ctx.beginPath();
4518
+ ctx.arc(half, half, half - 1, 0, Math.PI * 2);
4519
+ ctx.stroke();
4520
+ }
4521
+ }
4522
+ };
4523
+ /** Ancestor-summed position — a node parented to a moving thing is where it is drawn. */
4524
+ function worldOf$1(node) {
4525
+ let x = 0;
4526
+ let y = 0;
4527
+ let z = 0;
4528
+ let dims = 0;
4529
+ for (let n = node; n; n = n.parent) {
4530
+ const p = n.position;
4531
+ if (!Array.isArray(p)) continue;
4532
+ dims = Math.max(dims, p.length);
4533
+ x += p[0] ?? 0;
4534
+ y += p[1] ?? 0;
4535
+ z += p[2] ?? 0;
4536
+ }
4537
+ return dims >= 3 ? [
4538
+ x,
4539
+ y,
4540
+ z
4541
+ ] : [x, y];
4542
+ }
4543
+ function collectGroup(node, group, out = []) {
4544
+ if (node.isInGroup(group)) out.push(node);
4545
+ for (const c of node.children) collectGroup(c, group, out);
4546
+ return out;
4547
+ }
4548
+ //#endregion
4549
+ //#region src/core/nodes/ui-waypoint.ts
4550
+ /**
4551
+ * An objective marker — a diamond over the target, an arrow at the screen's
4552
+ * edge when it is off screen or behind you, the metres left under it.
4553
+ *
4554
+ * ```json
4555
+ * { "name": "Mark", "type": "UiWaypoint",
4556
+ * "props": { "target": "/root/Shop1", "label": "bakery", "hideWithin": 4, "arriveWithin": 3 } }
4557
+ * ```
4558
+ *
4559
+ * It floats over `target` through `engine.toScreen` (the renderer's own
4560
+ * projection), so `anchor` means nothing here. Off screen it slides to the
4561
+ * `margin` rectangle along the line from the centre and turns its arrow that
4562
+ * way; behind the camera it is mirrored through the centre first, so it
4563
+ * still points the way to turn. `distance` is measured from `from` — the
4564
+ * first node in the `player` group by default. `hideWithin` hides it when
4565
+ * you are that close; `arrived` fires once when `from` comes within
4566
+ * `arriveWithin`, and again after leaving and coming back.
4567
+ *
4568
+ * `screen()` is the same answer headless, from whatever `toScreen` a test
4569
+ * installs — a harness can ask where the marker is and which way it points.
4570
+ */
4571
+ var UiWaypoint = class extends HudWidgetBase {
4572
+ static typeName = "UiWaypoint";
4573
+ static props = {
4574
+ ...HudWidgetBase.props,
4575
+ /** The node the marker floats over. */
4576
+ target: {
4577
+ default: "",
4578
+ nodePath: true
4579
+ },
4580
+ /** Text under the diamond ('' = none). */
4581
+ label: { default: "" },
4582
+ color: { default: "#ffd54f" },
4583
+ /** The diamond's size, px. */
4584
+ size: { default: 22 },
4585
+ /** Show the metres left. */
4586
+ distance: { default: true },
4587
+ /** Keep it on screen at the edge when the target is off screen or behind. */
4588
+ clamp: { default: true },
4589
+ /** How far in from the edge it stops, px. */
4590
+ margin: { default: 40 },
4591
+ /** Hide when `from` is this close (m); 0 = never. */
4592
+ hideWithin: {
4593
+ default: 0,
4594
+ range: { min: 0 }
4595
+ },
4596
+ /** `arrived` fires when `from` comes this close (m); 0 = never. */
4597
+ arriveWithin: {
4598
+ default: 0,
4599
+ range: { min: 0 }
4600
+ },
4601
+ /** Who the distance is measured from; '' = the first node in the `player` group. */
4602
+ from: {
4603
+ default: "",
4604
+ nodePath: true
4605
+ }
4606
+ };
4607
+ static signals = [...HudWidgetBase.signals, "arrived"];
4608
+ target = "";
4609
+ label = "";
4610
+ color = "#ffd54f";
4611
+ size = 22;
4612
+ distance = true;
4613
+ clamp = true;
4614
+ margin = 40;
4615
+ hideWithin = 0;
4616
+ arriveWithin = 0;
4617
+ from = "";
4618
+ inside = false;
4619
+ box = null;
4620
+ arrow = null;
4621
+ diamond = null;
4622
+ text = null;
4623
+ /** Metres from `from` to the target, or null when either is missing. */
4624
+ distanceNow() {
4625
+ const t = this.targetNode();
4626
+ const f = this.fromNode();
4627
+ if (!t || !f) return null;
4628
+ const a = worldOf(t);
4629
+ const b = worldOf(f);
4630
+ return Math.hypot((a[0] ?? 0) - (b[0] ?? 0), (a[1] ?? 0) - (b[1] ?? 0), (a[2] ?? 0) - (b[2] ?? 0));
4631
+ }
4632
+ /**
4633
+ * Where the marker is, in canvas px, for a view this big (the canvas, in the
4634
+ * browser). Null when there is no target or no renderer has offered a
4635
+ * projection.
4636
+ */
4637
+ screen(view = this.viewport()) {
4638
+ const to = this.tree?.engine?.toScreen;
4639
+ const t = this.targetNode();
4640
+ if (!to || !t) return null;
4641
+ const p = to(worldOf(t));
4642
+ if (!p) return null;
4643
+ const w = view.width;
4644
+ const h = view.height;
4645
+ const cx = w / 2;
4646
+ const cy = h / 2;
4647
+ let x = p.x;
4648
+ let y = p.y;
4649
+ const behind = p.behind;
4650
+ if (behind) {
4651
+ x = w - x;
4652
+ y = h - y;
4653
+ }
4654
+ const onScreen = !behind && x >= 0 && x <= w && y >= 0 && y <= h;
4655
+ const m = this.margin;
4656
+ const insideMargin = x >= m && x <= w - m && y >= m && y <= h - m;
4657
+ let dx = x - cx;
4658
+ let dy = y - cy;
4659
+ if (Math.abs(dx) < 1e-6 && Math.abs(dy) < 1e-6) dy = 1;
4660
+ const angle = Math.atan2(dy, dx);
4661
+ let clamped = false;
4662
+ if (this.clamp && (behind || !insideMargin)) {
4663
+ const halfW = Math.max(1, cx - m);
4664
+ const halfH = Math.max(1, cy - m);
4665
+ const tx = Math.abs(dx) > 1e-6 ? halfW / Math.abs(dx) : Number.POSITIVE_INFINITY;
4666
+ const ty = Math.abs(dy) > 1e-6 ? halfH / Math.abs(dy) : Number.POSITIVE_INFINITY;
4667
+ const s = Math.min(tx, ty);
4668
+ if (s < 1 || behind) {
4669
+ dx *= s;
4670
+ dy *= s;
4671
+ x = cx + dx;
4672
+ y = cy + dy;
4673
+ clamped = true;
4674
+ }
4675
+ }
4676
+ const d = this.distanceNow() ?? 0;
4677
+ const visible = this.visible && !(this.hideWithin > 0 && d < this.hideWithin);
4678
+ return {
4679
+ x,
4680
+ y,
4681
+ angle,
4682
+ behind,
4683
+ onScreen,
4684
+ clamped,
4685
+ distance: d,
4686
+ visible
4687
+ };
4688
+ }
4689
+ update(dt) {
4690
+ if (this.arriveWithin > 0) {
4691
+ const d = this.distanceNow();
4692
+ const now = d !== null && d <= this.arriveWithin;
4693
+ if (now && !this.inside) this.emit("arrived");
4694
+ this.inside = now;
4695
+ }
4696
+ super.update(dt);
4697
+ }
4698
+ _build() {
4699
+ const box = document.createElement("div");
4700
+ box.style.cssText = "position:fixed;left:0;top:0;display:flex;flex-direction:column;align-items:center;gap:2px;pointer-events:none;transform:translate(-50%,-50%);will-change:transform;";
4701
+ const arrow = document.createElement("div");
4702
+ arrow.style.cssText = `width:0;height:0;border-top:7px solid transparent;border-bottom:7px solid transparent;border-left:12px solid ${this.color};transform-origin:50% 50%;display:none;`;
4703
+ const diamond = document.createElement("div");
4704
+ diamond.style.cssText = `width:${this.size}px;height:${this.size}px;background:${this.color};transform:rotate(45deg);box-shadow:0 0 6px rgba(0,0,0,0.5);`;
4705
+ const text = document.createElement("div");
4706
+ text.style.cssText = "font:600 13px/1.2 system-ui,sans-serif;color:#fff;text-shadow:0 1px 3px #000;white-space:nowrap;text-align:center;";
4707
+ box.append(diamond, text, arrow);
4708
+ this.box = box;
4709
+ this.arrow = arrow;
4710
+ this.diamond = diamond;
4711
+ this.text = text;
4712
+ return box;
4713
+ }
4714
+ _sync() {
4715
+ const box = this.box;
4716
+ if (!box) return;
4717
+ const s = this.screen();
4718
+ if (!s || !s.visible) {
4719
+ box.style.display = "none";
4720
+ return;
4721
+ }
4722
+ box.style.display = "flex";
4723
+ const view = this.viewport();
4724
+ const bw = box.offsetWidth || 0;
4725
+ const bh = box.offsetHeight || 0;
4726
+ const x = Math.min(Math.max(s.x, bw / 2), view.width - bw / 2);
4727
+ const y = Math.min(Math.max(s.y, bh / 2), view.height - bh / 2);
4728
+ box.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`;
4729
+ if (this.diamond) {
4730
+ this.diamond.style.background = this.color;
4731
+ this.diamond.style.width = `${this.size}px`;
4732
+ this.diamond.style.height = `${this.size}px`;
4733
+ this.diamond.style.display = s.clamped ? "none" : "";
4734
+ }
4735
+ if (this.arrow) {
4736
+ this.arrow.style.display = s.clamped ? "" : "none";
4737
+ this.arrow.style.borderLeftColor = this.color;
4738
+ this.arrow.style.transform = `rotate(${s.angle}rad)`;
4739
+ }
4740
+ if (this.text) {
4741
+ const parts = [];
4742
+ if (this.label !== "") parts.push(this.label);
4743
+ if (this.distance) parts.push(`${Math.round(s.distance)} m`);
4744
+ this.text.textContent = parts.join(" · ");
4745
+ this.text.style.display = parts.length ? "" : "none";
4746
+ }
4747
+ }
4748
+ /** The canvas's size in the browser; a stand-in headless. */
4749
+ viewport() {
4750
+ if (typeof document !== "undefined") {
4751
+ const canvas = document.querySelector("canvas");
4752
+ if (canvas) {
4753
+ const r = canvas.getBoundingClientRect();
4754
+ if (r.width > 0 && r.height > 0) return {
4755
+ width: r.width,
4756
+ height: r.height
4757
+ };
4758
+ }
4759
+ if (typeof window !== "undefined" && window.innerWidth > 0) return {
4760
+ width: window.innerWidth,
4761
+ height: window.innerHeight
4762
+ };
4763
+ }
4764
+ return {
4765
+ width: 1280,
4766
+ height: 720
4767
+ };
4768
+ }
4769
+ targetNode() {
4770
+ if (this.target === "") return null;
4771
+ const t = this.getNodeOrNull(this.target);
4772
+ return t && t.tree !== null ? t : null;
4773
+ }
4774
+ fromNode() {
4775
+ if (this.from !== "") {
4776
+ const f = this.getNodeOrNull(this.from);
4777
+ return f && f.tree !== null ? f : null;
4778
+ }
4779
+ return (this.tree?.getNodesInGroup("player") ?? [])[0] ?? null;
4780
+ }
4781
+ };
4782
+ /** Translation-only world position, 2D or 3D. */
4783
+ function worldOf(node) {
4784
+ let x = 0;
4785
+ let y = 0;
4786
+ let z = 0;
4787
+ for (let n = node; n; n = n.parent) {
4788
+ const p = n.position;
4789
+ if (!Array.isArray(p)) continue;
4790
+ x += p[0] ?? 0;
4791
+ y += p[1] ?? 0;
4792
+ z += p[2] ?? 0;
4793
+ }
4794
+ return [
4795
+ x,
4796
+ y,
4797
+ z
4798
+ ];
4799
+ }
4800
+ //#endregion
4209
4801
  //#region src/core/register.ts
4210
4802
  /**
4211
4803
  * Register the core node types. Call once in your game entry before loading
@@ -4225,6 +4817,8 @@ function registerCoreNodes() {
4225
4817
  registerNode(UiDialogue);
4226
4818
  registerNode(UiPanel);
4227
4819
  registerNode(UiImage);
4820
+ registerNode(UiMinimap);
4821
+ registerNode(UiWaypoint);
4228
4822
  registerNode(UiSlider);
4229
4823
  registerNode(UiToggle);
4230
4824
  registerNode(UiSelect);
@@ -4236,4 +4830,4 @@ function registerCoreNodes() {
4236
4830
  registerNode(UiMuteToggle);
4237
4831
  }
4238
4832
  //#endregion
4239
- export { T_PREFIX as A, SFX_PRESET_NAMES as B, qualityRendering as C, EffectLog as D, LogManager as E, isAudioContextAvailable as F, crossfadeGains as G, synthSfx as H, ROLLOFF_MODELS as I, fadeGain as K, spatialGain as L, translateOn as M, translationKey as N, BASE_LOCALE as O, SfxEngine as P, spatialPan as R, qualityEnvironment as S, suggestQuality as T, MusicManager as U, sfxDuration as V, WebAudioMusicBackend as W, AudioPlayer as _, UiMuteToggle as a, Settings as b, UiRenderScaleSelect as c, UiToggle as d, UiVolumeSlider as f, UiDialogue as g, UiButton as h, UiLanguageSelect as i, suggestLocale as j, Localization as k, UiSelect as l, Respawn as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, AudioBuses as q, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, Engine as v, readDeviceHints as w, qualityCaps as x, engine_exports as y, SFX_PRESETS as z };
4833
+ export { BASE_LOCALE as A, spatialPan as B, qualityCaps as C, suggestQuality as D, readDeviceHints as E, translationKey as F, MusicManager as G, SFX_PRESET_NAMES as H, SfxEngine as I, fadeGain as J, WebAudioMusicBackend as K, isAudioContextAvailable as L, T_PREFIX as M, suggestLocale as N, LogManager as O, translateOn as P, ROLLOFF_MODELS as R, Settings as S, qualityRendering as T, sfxDuration as U, SFX_PRESETS as V, synthSfx as W, AudioBuses as Y, UiButton as _, UiImage as a, Engine as b, UiPanel as c, UiSelect as d, UiSlider as f, Respawn as g, Timer as h, UiFrameCapSelect as i, Localization as j, EffectLog as k, UiQualitySelect as l, UiVolumeSlider as m, UiWaypoint as n, UiLanguageSelect as o, UiToggle as p, crossfadeGains as q, UiMinimap as r, UiMuteToggle as s, registerCoreNodes as t, UiRenderScaleSelect as u, UiDialogue as v, qualityEnvironment as w, engine_exports as x, AudioPlayer as y, spatialGain as z };