incanto 0.32.0 → 0.33.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 (49) hide show
  1. package/dist/2d.d.ts +59 -2
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +8 -3
  4. package/dist/3d.js +4 -4
  5. package/dist/{behavior-D_jMpFh8.d.ts → behavior-CKwTCjfR.d.ts} +140 -1
  6. package/dist/{create-game-bKHgHcsZ.js → create-game-D2QU5x7S.js} +13 -6
  7. package/dist/{create-game-gXI7PYl0.js → create-game-sFuTLqjD.js} +11 -5
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/{duplicate-KRPtUtzl.js → duplicate-BgnG1Lqz.js} +1 -1
  10. package/dist/editor.js +35 -0
  11. package/dist/{environment-presets--DigHNg4.js → environment-presets-CQtEGogB.js} +2 -2
  12. package/dist/{gameplay-BftxM_It.js → gameplay-BpQCbABv.js} +90 -2
  13. package/dist/gameplay.d.ts +1 -1
  14. package/dist/gameplay.js +1 -1
  15. package/dist/index.d.ts +125 -23
  16. package/dist/index.js +5 -58
  17. package/dist/{loader-BlaRQGaA.js → loader-Buk8Bu1h.js} +53 -0
  18. package/dist/{loader-BYBrqTxP.d.ts → loader-COn5fS0o.d.ts} +1 -1
  19. package/dist/net.d.ts +1 -1
  20. package/dist/net.js +3 -3
  21. package/dist/{pathfinding-BwhqPD3i.d.ts → pathfinding-DUw9mir9.d.ts} +1 -1
  22. package/dist/{physics-2d-D9wquBvK.js → physics-2d-DjXR5DMu.js} +12 -2
  23. package/dist/{physics-3d-CnPygVGo.js → physics-3d-DF8npb1O.js} +55 -9
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-Dzkd6-os.js → register-CscIzJEO.js} +551 -13
  27. package/dist/{register-CUY284Is.js → register-CtI-itec.js} +2 -2
  28. package/dist/{register-tkR_8tWg.js → register-FIJtNbub.js} +145 -8
  29. package/dist/{test-WwRIlXsK.js → test-BRxLd2jH.js} +10 -10
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +1 -1
  32. package/dist/vite.js +1 -1
  33. package/editor/assets/{agent8-C5k1nTCH.js → agent8-BdDP3xKW.js} +1 -1
  34. package/editor/assets/{debug-BT_0mjk-.js → debug-DbjTyTlC.js} +1 -1
  35. package/editor/assets/{index-gfyrByWw.js → index-BWCudoz1.js} +90 -90
  36. package/editor/index.html +1 -1
  37. package/package.json +1 -1
  38. package/schemas/scene.schema.json +520 -0
  39. package/skills/incanto-audio.md +43 -0
  40. package/skills/incanto-building-2d-games.md +36 -0
  41. package/skills/incanto-hud.md +41 -0
  42. package/skills/incanto-node-reference.md +89 -0
  43. package/skills/incanto-physics-and-input.md +50 -0
  44. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +40 -19
  45. package/templates-app/beacon-isle-3d/package.json +1 -1
  46. package/templates-app/tps-3d/docs/project-3d-rules.md +41 -19
  47. package/templates-app/tps-3d/package.json +1 -1
  48. package/templates-app/village-quest-3d/docs/project-3d-rules.md +40 -19
  49. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { f as Node, h as InputMap, x as Signal } from "./loader-BlaRQGaA.js";
1
+ import { f as Node, h as InputMap, x as Signal } from "./loader-Buk8Bu1h.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { l as registerNode } from "./registry-CJdGpT2V.js";
@@ -28,19 +28,19 @@ var AudioBuses = class {
28
28
  return this._master;
29
29
  }
30
30
  set master(v) {
31
- this._master = this.assign(this._master, v);
31
+ this.assign("_master", v);
32
32
  }
33
33
  get sfx() {
34
34
  return this._sfx;
35
35
  }
36
36
  set sfx(v) {
37
- this._sfx = this.assign(this._sfx, v);
37
+ this.assign("_sfx", v);
38
38
  }
39
39
  get music() {
40
40
  return this._music;
41
41
  }
42
42
  set music(v) {
43
- this._music = this.assign(this._music, v);
43
+ this.assign("_music", v);
44
44
  }
45
45
  get muted() {
46
46
  return this._muted;
@@ -57,11 +57,20 @@ var AudioBuses = class {
57
57
  return this._master * busGain * clamp01$3(sourceVolume);
58
58
  }
59
59
  /** Clamp + finite-guard a new value; emit only on a real change. */
60
- assign(current, next) {
61
- if (!Number.isFinite(next)) return current;
60
+ /**
61
+ * Write THEN emit.
62
+ *
63
+ * This used to emit from inside the expression assigning the field, so every
64
+ * listener of `changed` read the OLD value — including the thing the signal
65
+ * exists for ("live re-gain of playing sounds"), which therefore re-gained to
66
+ * the volume you just left.
67
+ */
68
+ assign(key, next) {
69
+ if (!Number.isFinite(next)) return;
62
70
  const clamped = clamp01$3(next);
63
- if (clamped !== current) this.changed.emit();
64
- return clamped;
71
+ if (clamped === this[key]) return;
72
+ this[key] = clamped;
73
+ this.changed.emit();
65
74
  }
66
75
  };
67
76
  function clamp01$3(v) {
@@ -1009,6 +1018,196 @@ var LogManager = class {
1009
1018
  }
1010
1019
  };
1011
1020
  //#endregion
1021
+ //#region src/core/save.ts
1022
+ const memoryStores = /* @__PURE__ */ new Map();
1023
+ function createSaveStore(namespace) {
1024
+ const prefix = `incanto:${namespace}:`;
1025
+ const local = storageOrNull();
1026
+ if (!local) {
1027
+ let mem = memoryStores.get(namespace);
1028
+ if (!mem) {
1029
+ mem = /* @__PURE__ */ new Map();
1030
+ memoryStores.set(namespace, mem);
1031
+ }
1032
+ const backing = mem;
1033
+ return {
1034
+ get: (key, fallback) => parse(backing.get(key), fallback),
1035
+ set: (key, value) => void backing.set(key, JSON.stringify(value)),
1036
+ remove: (key) => void backing.delete(key),
1037
+ clear: () => backing.clear()
1038
+ };
1039
+ }
1040
+ return {
1041
+ get: (key, fallback) => parse(local.getItem(prefix + key) ?? void 0, fallback),
1042
+ set: (key, value) => local.setItem(prefix + key, JSON.stringify(value)),
1043
+ remove: (key) => local.removeItem(prefix + key),
1044
+ clear: () => {
1045
+ const doomed = [];
1046
+ for (let i = 0; i < local.length; i++) {
1047
+ const k = local.key(i);
1048
+ if (k?.startsWith(prefix)) doomed.push(k);
1049
+ }
1050
+ for (const k of doomed) local.removeItem(k);
1051
+ }
1052
+ };
1053
+ }
1054
+ function parse(raw, fallback) {
1055
+ if (raw === void 0 || raw === null) return fallback;
1056
+ try {
1057
+ return JSON.parse(raw);
1058
+ } catch {
1059
+ return fallback;
1060
+ }
1061
+ }
1062
+ function storageOrNull() {
1063
+ try {
1064
+ if (typeof localStorage === "undefined") return null;
1065
+ const probe = "__incanto_probe__";
1066
+ localStorage.setItem(probe, "1");
1067
+ localStorage.removeItem(probe);
1068
+ return localStorage;
1069
+ } catch {
1070
+ return null;
1071
+ }
1072
+ }
1073
+ //#endregion
1074
+ //#region src/core/settings.ts
1075
+ const DEFAULTS = {
1076
+ master: 1,
1077
+ sfx: 1,
1078
+ music: 1,
1079
+ muted: false,
1080
+ quality: "high",
1081
+ sensitivity: 1,
1082
+ invertY: false,
1083
+ reduceMotion: false
1084
+ };
1085
+ const TIERS = [
1086
+ "low",
1087
+ "medium",
1088
+ "high"
1089
+ ];
1090
+ /**
1091
+ * A default tier from what the browser will admit to.
1092
+ *
1093
+ * Deliberately crude: this picks a STARTING point, and the options menu is the
1094
+ * real answer. A phone that reports four cores and a coarse pointer should not
1095
+ * open on the setting tuned for a desktop GPU.
1096
+ */
1097
+ function suggestQuality(hints) {
1098
+ const cores = hints.hardwareConcurrency ?? 8;
1099
+ const memory = hints.deviceMemory ?? 8;
1100
+ if (hints.coarsePointer && (cores <= 6 || memory <= 4)) return "low";
1101
+ if (cores <= 4 || memory <= 4 || (hints.maxTextureSize ?? 8192) < 8192) return "low";
1102
+ if (cores <= 8 || memory <= 8) return "medium";
1103
+ return "high";
1104
+ }
1105
+ /** Read what this environment will tell us, safely, without touching WebGL. */
1106
+ function readDeviceHints() {
1107
+ const nav = globalThis.navigator;
1108
+ const mm = globalThis.matchMedia;
1109
+ const out = {};
1110
+ if (typeof nav?.hardwareConcurrency === "number") out.hardwareConcurrency = nav.hardwareConcurrency;
1111
+ if (typeof nav?.deviceMemory === "number") out.deviceMemory = nav.deviceMemory;
1112
+ try {
1113
+ if (mm) out.coarsePointer = mm("(pointer: coarse)").matches;
1114
+ } catch {}
1115
+ return out;
1116
+ }
1117
+ var Settings = class {
1118
+ /** Fires after any change, with the key that changed. */
1119
+ changed = new Signal();
1120
+ store;
1121
+ values;
1122
+ constructor(namespace = "settings", store) {
1123
+ this.store = store ?? createSaveStore(namespace);
1124
+ this.values = { ...DEFAULTS };
1125
+ for (const key of Object.keys(DEFAULTS)) {
1126
+ const saved = this.store.get(key, DEFAULTS[key]);
1127
+ this.values[key] = saved;
1128
+ }
1129
+ if (!TIERS.includes(this.values.quality)) this.values.quality = DEFAULTS.quality;
1130
+ }
1131
+ get(key) {
1132
+ return this.values[key];
1133
+ }
1134
+ set(key, value) {
1135
+ if (this.values[key] === value) return;
1136
+ this.values[key] = value;
1137
+ this.store.set(key, value);
1138
+ this.changed.emit(key);
1139
+ }
1140
+ /** Every value at once — for an options screen, or a save-file dump. */
1141
+ all() {
1142
+ return { ...this.values };
1143
+ }
1144
+ /** Back to defaults, on disk too. */
1145
+ reset() {
1146
+ for (const key of Object.keys(DEFAULTS)) this.set(key, DEFAULTS[key]);
1147
+ }
1148
+ /**
1149
+ * Push the saved volumes into the engine's buses, and keep writing them back
1150
+ * whenever anything changes them.
1151
+ *
1152
+ * The subscription is the point: a game that sets `engine.audio.music = 0.4`
1153
+ * from a slider has already persisted it, without knowing this class exists.
1154
+ */
1155
+ bindAudio(audio) {
1156
+ audio.master = this.values.master;
1157
+ audio.sfx = this.values.sfx;
1158
+ audio.music = this.values.music;
1159
+ audio.muted = this.values.muted;
1160
+ return audio.changed.connect(() => {
1161
+ this.set("master", audio.master);
1162
+ this.set("sfx", audio.sfx);
1163
+ this.set("music", audio.music);
1164
+ this.set("muted", audio.muted);
1165
+ });
1166
+ }
1167
+ };
1168
+ /**
1169
+ * What a quality tier MEANS, as an environment patch.
1170
+ *
1171
+ * Only the levers that actually cost: shadows, post, clouds, and the resolution
1172
+ * the renderer draws at. Deliberately an `environment` patch, because
1173
+ * `setEnvironment3D(engine, patch)` already applies one live and validated —
1174
+ * this is not a second rendering pipeline, it is three presets for the one that
1175
+ * exists.
1176
+ */
1177
+ function qualityEnvironment(tier) {
1178
+ if (tier === "low") return {
1179
+ shadows: { enabled: false },
1180
+ bloom: { enabled: false },
1181
+ post: { enabled: false },
1182
+ clouds: { enabled: false },
1183
+ rendering: {
1184
+ antialias: false,
1185
+ pixelRatio: 1
1186
+ }
1187
+ };
1188
+ if (tier === "medium") return {
1189
+ shadows: {
1190
+ enabled: true,
1191
+ static: true
1192
+ },
1193
+ bloom: { enabled: true },
1194
+ post: { enabled: false },
1195
+ rendering: {
1196
+ antialias: true,
1197
+ pixelRatio: 1
1198
+ }
1199
+ };
1200
+ return {
1201
+ shadows: { enabled: true },
1202
+ bloom: { enabled: true },
1203
+ post: { enabled: true },
1204
+ rendering: {
1205
+ antialias: true,
1206
+ pixelRatio: "device"
1207
+ }
1208
+ };
1209
+ }
1210
+ //#endregion
1012
1211
  //#region src/core/engine.ts
1013
1212
  const MAX_DT_SECONDS = .25;
1014
1213
  function rafScheduler(cb) {
@@ -1047,9 +1246,27 @@ var Engine = class {
1047
1246
  rng;
1048
1247
  /** The engine log channel (debug overlay + headless harness tail this). */
1049
1248
  log = new LogManager();
1249
+ /**
1250
+ * Screen pixel → the node under it, when a renderer has offered one.
1251
+ *
1252
+ * The raycast has always existed (`Renderer3D.pick` / `Renderer2D.pick`) and
1253
+ * was reachable only from whoever held the renderer — so a Behavior, which
1254
+ * holds a node, could not use it. Every mouse-driven genre needs exactly this
1255
+ * and had no way to ask. Instance-scoped, set by the renderer on construction
1256
+ * and cleared on dispose; null when nothing renders (headless).
1257
+ */
1258
+ picker = null;
1050
1259
  /** Global volume buses — `master` × `sfx`/`music` gain + `muted`. AudioPlayer
1051
1260
  * routes its volume through this; games set it for global volume/mute. */
1052
1261
  audio = new AudioBuses();
1262
+ /**
1263
+ * Persisted player settings — volume, quality, sensitivity.
1264
+ *
1265
+ * `createSaveStore` was good and had zero callers, so every game's volume
1266
+ * reset on reload. Reading it here costs nothing (localStorage, once) and
1267
+ * `bindAudio` makes the volumes write themselves.
1268
+ */
1269
+ settings = new Settings();
1053
1270
  /** Low-latency procedural-SFX player (WebAudio, headless-safe). AudioPlayer
1054
1271
  * presets play through it; games may call `engine.sfx.play(...)` directly. */
1055
1272
  sfx = new SfxEngine();
@@ -1597,17 +1814,42 @@ var HudWidgetBase = class extends Node {
1597
1814
  }
1598
1815
  onReady() {
1599
1816
  if (!hasDom()) return;
1600
- const slot = this.layer()?._slot(this.anchor);
1601
- if (!slot) return;
1817
+ this._mountWidget();
1818
+ }
1819
+ /**
1820
+ * @internal Build and attach, once a host exists.
1821
+ *
1822
+ * A widget inside a UiPanel mounts into the PANEL, not the layer's anchor
1823
+ * slot — that is what turns structure in the tree into structure on screen,
1824
+ * and it is what a menu is. Ready order does not decide it: the panel builds
1825
+ * on demand when a child asks (`_container`), and `update` retries for
1826
+ * anything attached at runtime before its host existed.
1827
+ */
1828
+ _mountWidget() {
1829
+ if (this._element || !hasDom()) return;
1830
+ const host = this.host() ?? this.layer()?._slot(this.anchor);
1831
+ if (!host) return;
1602
1832
  this._element = this._build();
1603
- slot.appendChild(this._element);
1833
+ host.appendChild(this._element);
1834
+ }
1835
+ /** The nearest ancestor that holds widgets itself (a UiPanel). */
1836
+ host() {
1837
+ for (let p = this.parent; p; p = p.parent) {
1838
+ const holder = p;
1839
+ if (typeof holder._container === "function") return holder._container();
1840
+ if (p instanceof HudLayer) return null;
1841
+ }
1842
+ return null;
1604
1843
  }
1605
1844
  onExitTree() {
1606
1845
  this._element?.remove();
1607
1846
  this._element = null;
1608
1847
  }
1609
1848
  update(_dt) {
1610
- if (!this._element) return;
1849
+ if (!this._element) {
1850
+ this._mountWidget();
1851
+ if (!this._element) return;
1852
+ }
1611
1853
  this._element.style.display = this.visible ? "" : "none";
1612
1854
  this._sync();
1613
1855
  }
@@ -2028,6 +2270,297 @@ var Timer = class extends Node {
2028
2270
  }
2029
2271
  };
2030
2272
  //#endregion
2273
+ //#region src/core/nodes/ui-kit.ts
2274
+ function px(n) {
2275
+ return `${n}px`;
2276
+ }
2277
+ /**
2278
+ * A box that holds other widgets — the thing a menu actually is.
2279
+ *
2280
+ * Widgets normally mount into their HudLayer's anchor slot, which is why a
2281
+ * "menu" used to be a flat pile of anchored items with no way to sit them in a
2282
+ * panel, space them, or lay them out in a grid. A `UiPanel` becomes the parent
2283
+ * for every widget under it, so structure in the TREE is structure on SCREEN.
2284
+ *
2285
+ * ```json
2286
+ * { "name": "Pause", "type": "UiPanel", "props": { "anchor": "center", "layout": "column", "gap": 12 },
2287
+ * "children": [
2288
+ * { "name": "Title", "type": "UiText", "props": { "text": "PAUSED", "size": 28 } },
2289
+ * { "name": "Resume", "type": "UiButton", "props": { "label": "Resume" } }
2290
+ * ] }
2291
+ * ```
2292
+ */
2293
+ var UiPanel = class extends HudWidgetBase {
2294
+ static typeName = "UiPanel";
2295
+ static props = {
2296
+ ...HudWidgetBase.props,
2297
+ layout: {
2298
+ default: "column",
2299
+ options: [
2300
+ "column",
2301
+ "row",
2302
+ "grid"
2303
+ ]
2304
+ },
2305
+ /** Columns when `layout: "grid"` — an inventory is a grid of slots. */
2306
+ columns: { default: 4 },
2307
+ gap: { default: 8 },
2308
+ padding: { default: 12 },
2309
+ background: { default: "rgba(0,0,0,0.55)" },
2310
+ radius: { default: 10 },
2311
+ /** 0 = size to the contents. */
2312
+ width: { default: 0 },
2313
+ height: { default: 0 },
2314
+ border: { default: "" }
2315
+ };
2316
+ layout = "column";
2317
+ columns = 4;
2318
+ gap = 8;
2319
+ padding = 12;
2320
+ background = "rgba(0,0,0,0.55)";
2321
+ radius = 10;
2322
+ width = 0;
2323
+ height = 0;
2324
+ border = "";
2325
+ lastKey = "";
2326
+ _build() {
2327
+ const el = document.createElement("div");
2328
+ el.style.cssText = this.css();
2329
+ this.lastKey = this.css();
2330
+ return el;
2331
+ }
2332
+ /**
2333
+ * @internal Children mount HERE, not in the layer's anchor slot.
2334
+ *
2335
+ * Builds on demand: a child asking for its host must not depend on whose
2336
+ * `onReady` the tree happened to run first.
2337
+ */
2338
+ _container() {
2339
+ if (!this._element) this._mountWidget();
2340
+ return this._element;
2341
+ }
2342
+ css() {
2343
+ return `${this.layout === "grid" ? `display:grid;grid-template-columns:repeat(${Math.max(1, this.columns)},auto);` : `display:flex;flex-direction:${this.layout};align-items:stretch;`}gap:${px(this.gap)};padding:${px(this.padding)};background:${this.background};border-radius:${px(this.radius)};` + (this.width > 0 ? `width:${px(this.width)};` : "") + (this.height > 0 ? `height:${px(this.height)};` : "") + (this.border ? `border:${this.border};` : "") + "box-sizing:border-box;pointer-events:auto;";
2344
+ }
2345
+ _sync() {
2346
+ const next = this.css();
2347
+ if (next !== this.lastKey && this._element) {
2348
+ this.lastKey = next;
2349
+ this._element.style.cssText = next;
2350
+ }
2351
+ }
2352
+ };
2353
+ /** A picture: an item icon, a portrait, a logo. `src` takes a url or `$assetKey`. */
2354
+ var UiImage = class extends HudWidgetBase {
2355
+ static typeName = "UiImage";
2356
+ static props = {
2357
+ ...HudWidgetBase.props,
2358
+ src: { default: "" },
2359
+ width: { default: 48 },
2360
+ height: { default: 48 },
2361
+ /** How the picture fills its box — `contain` never crops. */
2362
+ fit: {
2363
+ default: "contain",
2364
+ options: [
2365
+ "contain",
2366
+ "cover",
2367
+ "fill"
2368
+ ]
2369
+ },
2370
+ /** Multiply the image by a colour: greyed-out items, team tints. */
2371
+ tint: { default: "" },
2372
+ opacity: { default: 1 }
2373
+ };
2374
+ src = "";
2375
+ width = 48;
2376
+ height = 48;
2377
+ fit = "contain";
2378
+ tint = "";
2379
+ opacity = 1;
2380
+ last = "\0";
2381
+ _build() {
2382
+ const el = document.createElement("div");
2383
+ el.style.cssText = this.css();
2384
+ this.last = this.key();
2385
+ return el;
2386
+ }
2387
+ resolved() {
2388
+ if (!this.src.startsWith("$")) return this.src;
2389
+ return (this.tree?.engine?.scene?.assets)?.[this.src.slice(1)]?.url ?? "";
2390
+ }
2391
+ key() {
2392
+ return `${this.resolved()}|${this.width}|${this.height}|${this.fit}|${this.tint}|${this.opacity}`;
2393
+ }
2394
+ css() {
2395
+ const url = this.resolved();
2396
+ return `width:${px(this.width)};height:${px(this.height)};flex:none;` + (url ? `background-image:url("${url}");` : "") + `background-size:${this.fit === "fill" ? "100% 100%" : this.fit};background-position:center;background-repeat:no-repeat;opacity:${this.opacity};` + (this.tint ? `background-color:${this.tint};background-blend-mode:multiply;` : "");
2397
+ }
2398
+ _sync() {
2399
+ const next = this.key();
2400
+ if (next !== this.last && this._element) {
2401
+ this.last = next;
2402
+ this._element.style.cssText = this.css();
2403
+ }
2404
+ }
2405
+ };
2406
+ /**
2407
+ * A value the player sets — volume, sensitivity, difficulty.
2408
+ *
2409
+ * Emits `changed(value)`. `incanto-audio.md` promised "a settings slider needs
2410
+ * no extra wiring" while no slider existed; this is that slider.
2411
+ */
2412
+ var UiSlider = class extends HudWidgetBase {
2413
+ static typeName = "UiSlider";
2414
+ static props = {
2415
+ ...HudWidgetBase.props,
2416
+ label: { default: "" },
2417
+ value: { default: .5 },
2418
+ min: { default: 0 },
2419
+ max: { default: 1 },
2420
+ step: { default: .01 },
2421
+ width: { default: 180 },
2422
+ color: { default: "#6ee7dc" }
2423
+ };
2424
+ static signals = ["changed"];
2425
+ label = "";
2426
+ value = .5;
2427
+ min = 0;
2428
+ max = 1;
2429
+ step = .01;
2430
+ width = 180;
2431
+ color = "#6ee7dc";
2432
+ input = null;
2433
+ text = null;
2434
+ last = NaN;
2435
+ _build() {
2436
+ const wrap = document.createElement("div");
2437
+ wrap.style.cssText = "display:flex;align-items:center;gap:8px;pointer-events:auto;font:600 13px system-ui,-apple-system,'Segoe UI',sans-serif;color:#fff;";
2438
+ const text = document.createElement("span");
2439
+ text.textContent = this.label;
2440
+ const input = document.createElement("input");
2441
+ input.type = "range";
2442
+ input.min = String(this.min);
2443
+ input.max = String(this.max);
2444
+ input.step = String(this.step);
2445
+ input.value = String(this.value);
2446
+ input.style.cssText = `width:${px(this.width)};accent-color:${this.color};`;
2447
+ input.addEventListener("input", () => {
2448
+ this.value = Number(input.value);
2449
+ this.last = this.value;
2450
+ this.emit("changed", this.value);
2451
+ });
2452
+ if (this.label) wrap.appendChild(text);
2453
+ wrap.appendChild(input);
2454
+ this.input = input;
2455
+ this.text = text;
2456
+ this.last = this.value;
2457
+ return wrap;
2458
+ }
2459
+ _sync() {
2460
+ if (this.input && this.value !== this.last) {
2461
+ this.last = this.value;
2462
+ this.input.value = String(this.value);
2463
+ }
2464
+ if (this.text && this.text.textContent !== this.label) this.text.textContent = this.label;
2465
+ }
2466
+ };
2467
+ /** An on/off switch — mute, invert-Y, fullscreen. Emits `changed(boolean)`. */
2468
+ var UiToggle = class extends HudWidgetBase {
2469
+ static typeName = "UiToggle";
2470
+ static props = {
2471
+ ...HudWidgetBase.props,
2472
+ label: { default: "" },
2473
+ value: { default: false }
2474
+ };
2475
+ static signals = ["changed"];
2476
+ label = "";
2477
+ value = false;
2478
+ input = null;
2479
+ last = null;
2480
+ _build() {
2481
+ const wrap = document.createElement("label");
2482
+ wrap.style.cssText = "display:flex;align-items:center;gap:8px;pointer-events:auto;cursor:pointer;font:600 13px system-ui,-apple-system,'Segoe UI',sans-serif;color:#fff;";
2483
+ const input = document.createElement("input");
2484
+ input.type = "checkbox";
2485
+ input.checked = this.value;
2486
+ input.addEventListener("change", () => {
2487
+ this.value = input.checked;
2488
+ this.last = this.value;
2489
+ this.emit("changed", this.value);
2490
+ });
2491
+ const text = document.createElement("span");
2492
+ text.textContent = this.label;
2493
+ wrap.append(input, text);
2494
+ this.input = input;
2495
+ this.last = this.value;
2496
+ return wrap;
2497
+ }
2498
+ _sync() {
2499
+ if (this.input && this.value !== this.last) {
2500
+ this.last = this.value;
2501
+ this.input.checked = this.value;
2502
+ }
2503
+ }
2504
+ };
2505
+ /** One of several — quality, resolution, language. Emits `changed(value)`. */
2506
+ var UiSelect = class extends HudWidgetBase {
2507
+ static typeName = "UiSelect";
2508
+ static props = {
2509
+ ...HudWidgetBase.props,
2510
+ label: { default: "" },
2511
+ /** Comma-separated choices: `"low,medium,high"`. */
2512
+ options: { default: "" },
2513
+ value: { default: "" }
2514
+ };
2515
+ static signals = ["changed"];
2516
+ label = "";
2517
+ options = "";
2518
+ value = "";
2519
+ select = null;
2520
+ lastOptions = "\0";
2521
+ last = "\0";
2522
+ _build() {
2523
+ const wrap = document.createElement("label");
2524
+ wrap.style.cssText = "display:flex;align-items:center;gap:8px;pointer-events:auto;font:600 13px system-ui,-apple-system,'Segoe UI',sans-serif;color:#fff;";
2525
+ const text = document.createElement("span");
2526
+ text.textContent = this.label;
2527
+ const select = document.createElement("select");
2528
+ select.addEventListener("change", () => {
2529
+ this.value = select.value;
2530
+ this.last = this.value;
2531
+ this.emit("changed", this.value);
2532
+ });
2533
+ if (this.label) wrap.appendChild(text);
2534
+ wrap.appendChild(select);
2535
+ this.select = select;
2536
+ this.fillOptions();
2537
+ return wrap;
2538
+ }
2539
+ fillOptions() {
2540
+ const select = this.select;
2541
+ if (!select || this.options === this.lastOptions) return;
2542
+ this.lastOptions = this.options;
2543
+ select.textContent = "";
2544
+ for (const raw of this.options.split(",")) {
2545
+ const value = raw.trim();
2546
+ if (!value) continue;
2547
+ const option = document.createElement("option");
2548
+ option.value = value;
2549
+ option.textContent = value;
2550
+ select.appendChild(option);
2551
+ }
2552
+ if (this.value) select.value = this.value;
2553
+ this.last = this.value;
2554
+ }
2555
+ _sync() {
2556
+ this.fillOptions();
2557
+ if (this.select && this.value !== this.last) {
2558
+ this.last = this.value;
2559
+ this.select.value = this.value;
2560
+ }
2561
+ }
2562
+ };
2563
+ //#endregion
2031
2564
  //#region src/core/register.ts
2032
2565
  /**
2033
2566
  * Register the core node types. Call once in your game entry before loading
@@ -2044,6 +2577,11 @@ function registerCoreNodes() {
2044
2577
  registerNode(UiBanner);
2045
2578
  registerNode(UiButton);
2046
2579
  registerNode(UiDialogue);
2580
+ registerNode(UiPanel);
2581
+ registerNode(UiImage);
2582
+ registerNode(UiSlider);
2583
+ registerNode(UiToggle);
2584
+ registerNode(UiSelect);
2047
2585
  }
2048
2586
  //#endregion
2049
- export { fadeGain as C, crossfadeGains as S, SFX_PRESETS as _, HudLayer as a, MusicManager as b, UiText as c, LogManager as d, SfxEngine as f, spatialPan as g, spatialGain as h, UiDialogue as i, AudioPlayer as l, ROLLOFF_MODELS as m, Timer as n, UiBanner as o, isAudioContextAvailable as p, UiButton as r, UiBar as s, registerCoreNodes as t, Engine as u, SFX_PRESET_NAMES as v, AudioBuses as w, WebAudioMusicBackend as x, synthSfx as y };
2587
+ export { MusicManager as A, isAudioContextAvailable as C, SFX_PRESETS as D, spatialPan as E, crossfadeGains as M, fadeGain as N, SFX_PRESET_NAMES as O, AudioBuses as P, SfxEngine as S, spatialGain as T, qualityEnvironment as _, UiSlider as a, createSaveStore as b, UiButton as c, UiBanner as d, UiBar as f, Settings as g, Engine as h, UiSelect as i, WebAudioMusicBackend as j, synthSfx as k, UiDialogue as l, AudioPlayer as m, UiImage as n, UiToggle as o, UiText as p, UiPanel as r, Timer as s, registerCoreNodes as t, HudLayer as u, readDeviceHints as v, ROLLOFF_MODELS as w, LogManager as x, suggestQuality as y };
@@ -1,5 +1,5 @@
1
- import { f as Node, t as buildNodeJson, x as Signal } from "./loader-BlaRQGaA.js";
2
- import { t as registerCoreNodes } from "./register-Dzkd6-os.js";
1
+ import { f as Node, t as buildNodeJson, x as Signal } from "./loader-Buk8Bu1h.js";
2
+ import { t as registerCoreNodes } from "./register-CscIzJEO.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { l as registerNode } from "./registry-CJdGpT2V.js";