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.
- package/bin/incanto-multiplay.mjs +52 -4
- package/dist/2d.d.ts +83 -7
- package/dist/2d.js +4 -3
- package/dist/3d.d.ts +861 -167
- package/dist/3d.js +6 -6
- package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
- package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
- package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
- package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
- package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
- package/dist/debug.d.ts +1 -1
- package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
- package/dist/editor.js +1600 -1529
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
- package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
- package/dist/gameplay.d.ts +573 -31
- package/dist/gameplay.js +3 -2
- package/dist/index.d.ts +150 -6
- package/dist/index.js +6 -6
- package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
- package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
- package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
- package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
- package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
- package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
- package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
- package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
- package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
- package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
- package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
- package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
- package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
- package/dist/test.d.ts +90 -12
- package/dist/test.js +2 -2
- package/dist/vite.js +34 -5
- package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
- package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
- package/editor/assets/index-Db74LZY-.js +11579 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +1639 -280
- package/skills/incanto-3d-character.md +383 -20
- package/skills/incanto-audio.md +24 -2
- package/skills/incanto-building-2d-games.md +21 -2
- package/skills/incanto-building-3d-games.md +38 -3
- package/skills/incanto-editor.md +29 -8
- package/skills/incanto-environment.md +257 -14
- package/skills/incanto-game-feel.md +60 -1
- package/skills/incanto-gameplay-behaviors.md +668 -20
- package/skills/incanto-hud.md +116 -0
- package/skills/incanto-multiplayer.md +141 -2
- package/skills/incanto-node-reference.md +398 -16
- package/skills/incanto-performance.md +32 -0
- package/skills/incanto-physics-and-input.md +241 -13
- package/skills/incanto-playtesting.md +2 -2
- package/skills/incanto-save-slots.md +61 -2
- package/skills/incanto-scene-json-authoring.md +13 -0
- package/skills/incanto-verifying-your-game.md +86 -3
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
- package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/molehill-2d/src/game.scene.json +129 -2
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +40 -26
- package/templates-app/platformer-2d/src/game.scene.json +144 -2
- package/templates-app/platformer-2d/verify.ts +18 -1
- package/templates-app/star-survivor/PROJECT/Status.md +7 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/behaviors.ts +9 -8
- package/templates-app/star-survivor/src/game.scene.json +142 -4
- package/templates-app/star-survivor/verify.ts +13 -0
- package/templates-app/tps-3d/PROJECT/Status.md +6 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +71 -10
- package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
- package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
- package/templates-app/village-quest-3d/src/village.scene.json +126 -2
- package/templates-app/village-quest-3d/verify.ts +52 -2
- package/editor/assets/index-CrUCQoaB.js +0 -11046
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { o as __exportAll } from "./json-CfTjpvW8.js";
|
|
2
|
-
import {
|
|
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 {
|
|
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 };
|