incanto 0.30.0 → 0.31.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 (42) hide show
  1. package/dist/2d.d.ts +12 -5
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +47 -6
  4. package/dist/3d.js +4 -4
  5. package/dist/{audit-C6rMyict.js → audit-C4kmDK0o.js} +16 -0
  6. package/dist/{behavior-BAc0erXF.d.ts → behavior-D_jMpFh8.d.ts} +117 -55
  7. package/dist/{create-game-DuBTv2zI.js → create-game-DqOjMBUS.js} +50 -41
  8. package/dist/{create-game-5z_QVtLx.js → create-game-z5XaB1p5.js} +16 -8
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/debug.js +7 -2
  11. package/dist/{duplicate-BgtFrFo4.js → duplicate-KRPtUtzl.js} +1 -1
  12. package/dist/{editor-switch-B0wB_DSr.d.ts → editor-switch-BJb-CWfA.d.ts} +12 -1
  13. package/dist/{register-DJ0SByQg.js → environment-presets--DigHNg4.js} +42 -11
  14. package/dist/{gameplay-Cfr6aFZ1.js → gameplay-BftxM_It.js} +2 -2
  15. package/dist/gameplay.d.ts +1 -1
  16. package/dist/gameplay.js +1 -1
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.js +6 -6
  19. package/dist/{loader-B242FF6N.d.ts → loader-BYBrqTxP.d.ts} +1 -1
  20. package/dist/{loader-BZqOKfI2.js → loader-BlaRQGaA.js} +482 -6
  21. package/dist/net.d.ts +1 -1
  22. package/dist/net.js +3 -3
  23. package/dist/{pathfinding-BwD974Ss.d.ts → pathfinding-BwhqPD3i.d.ts} +1 -1
  24. package/dist/{physics-2d-3kOQCtgd.js → physics-2d-D9wquBvK.js} +4 -3
  25. package/dist/{physics-3d-CeRH-Ff_.js → physics-3d-CnPygVGo.js} +5 -4
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-MelqEdza.js → register-CUY284Is.js} +3 -3
  29. package/dist/{register-BTg0EM7s.js → register-Dzkd6-os.js} +56 -380
  30. package/dist/{register-DWcWq4QG.js → register-tkR_8tWg.js} +4 -4
  31. package/dist/{registry-BVJ2HbCn.js → registry-CJdGpT2V.js} +6 -2
  32. package/dist/test.d.ts +2 -7
  33. package/dist/test.js +43 -14
  34. package/editor/assets/{agent8-CAp0i5qn.js → agent8-o27_Y1xN.js} +1 -1
  35. package/editor/assets/{debug-BoEYfbqK.js → debug-DfcWX3uW.js} +3 -2
  36. package/editor/assets/{index-BO6WU8by.js → index-C9fb5QcT.js} +90 -90
  37. package/editor/index.html +1 -1
  38. package/package.json +1 -1
  39. package/skills/incanto-verifying-your-game.md +62 -0
  40. package/templates-app/beacon-isle-3d/package.json +1 -1
  41. package/templates-app/tps-3d/package.json +1 -1
  42. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,7 +1,7 @@
1
- import { f as Node, y as Signal } from "./loader-BZqOKfI2.js";
1
+ import { f as Node, h as InputMap, x as Signal } from "./loader-BlaRQGaA.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
- import { l as registerNode } from "./registry-BVJ2HbCn.js";
4
+ import { l as registerNode } from "./registry-CJdGpT2V.js";
5
5
  //#region src/core/audio/buses.ts
6
6
  /**
7
7
  * Global volume state — `master` × `sfx`/`music` gain (0..1) plus a `muted`
@@ -960,381 +960,6 @@ var FrameStatsRing = class {
960
960
  }
961
961
  };
962
962
  //#endregion
963
- //#region src/core/input.ts
964
- /** Keys typed into editable elements belong to the page UI, not the game. */
965
- function isEditableTarget(target) {
966
- const el = target;
967
- return !!el && (el.isContentEditable === true || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT");
968
- }
969
- /**
970
- * Declarative input from scene JSON `input{}` — DOM-free logic (feed key state
971
- * via `handleKey`, or wire a browser with `attachKeyboard(window)`).
972
- *
973
- * Convention: vector2 follows the 2D y-down space (up = -y). `justPressed` /
974
- * `justReleased` are one-frame edges, settled by the Engine each tick.
975
- */
976
- var InputMap = class {
977
- actions = /* @__PURE__ */ new Map();
978
- down = /* @__PURE__ */ new Set();
979
- pressedEdge = /* @__PURE__ */ new Set();
980
- releasedEdge = /* @__PURE__ */ new Set();
981
- detach = null;
982
- /** Load (or extend with) scene-JSON action declarations. Hard-validates shape. */
983
- declare(decls) {
984
- for (const [name, raw] of Object.entries(decls)) {
985
- const decl = raw;
986
- if (decl.type === "button") {
987
- const keys = decl.keys;
988
- if (!Array.isArray(keys) || keys.some((k) => typeof k !== "string")) throw new IncantoError("BAD_FORMAT", `Input action '${name}': button "keys" must be an array of KeyboardEvent.code strings.`);
989
- if (decl.touch !== void 0 && decl.touch !== "button") throw new IncantoError("BAD_FORMAT", `Input action '${name}': button "touch" must be 'button', got ${JSON.stringify(decl.touch)}.`);
990
- this.actions.set(name, {
991
- type: "button",
992
- keys: [...keys],
993
- ...decl.touch === "button" ? { touch: "button" } : {}
994
- });
995
- } else if (decl.type === "vector2") {
996
- const dirs = decl.keys;
997
- for (const dir of [
998
- "up",
999
- "down",
1000
- "left",
1001
- "right"
1002
- ]) if (!Array.isArray(dirs?.[dir])) throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "keys" needs arrays for up/down/left/right.`);
1003
- if (decl.touch !== void 0 && decl.touch !== "joystick") throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "touch" must be 'joystick', got ${JSON.stringify(decl.touch)}.`);
1004
- const d = dirs;
1005
- this.actions.set(name, {
1006
- type: "vector2",
1007
- keys: {
1008
- up: [...d.up],
1009
- down: [...d.down],
1010
- left: [...d.left],
1011
- right: [...d.right]
1012
- },
1013
- ...decl.touch === "joystick" ? { touch: "joystick" } : {}
1014
- });
1015
- } else throw new IncantoError("BAD_FORMAT", `Input action '${name}': "type" must be 'button' or 'vector2', got ${JSON.stringify(decl.type)}.`);
1016
- }
1017
- }
1018
- /** Feed a key state change (code = KeyboardEvent.code, e.g. 'Space', 'KeyW'). */
1019
- handleKey(code, isDown) {
1020
- if (isDown) {
1021
- if (!this.down.has(code)) {
1022
- this.down.add(code);
1023
- this.pressedEdge.add(code);
1024
- }
1025
- } else if (this.down.has(code)) {
1026
- this.down.delete(code);
1027
- this.releasedEdge.add(code);
1028
- }
1029
- }
1030
- injectedDown = /* @__PURE__ */ new Set();
1031
- injectedPressed = /* @__PURE__ */ new Set();
1032
- injectedReleased = /* @__PURE__ */ new Set();
1033
- injectedVectors = /* @__PURE__ */ new Map();
1034
- /**
1035
- * Press a button ACTION directly — no key codes involved. This is how
1036
- * scripted gameplay tests and touch buttons drive the game by intent
1037
- * (`press('jump')`) instead of reverse-engineering keybinds.
1038
- */
1039
- pressAction(action) {
1040
- this.button(action);
1041
- if (!this.injectedDown.has(action)) {
1042
- this.injectedDown.add(action);
1043
- this.injectedPressed.add(action);
1044
- }
1045
- }
1046
- /** Release an injected button action (yields one justReleased frame). */
1047
- releaseAction(action) {
1048
- this.button(action);
1049
- if (this.injectedDown.delete(action)) this.injectedReleased.add(action);
1050
- }
1051
- /**
1052
- * Feed an analog direction for a vector2 ACTION — virtual joysticks and
1053
- * scripted runs. Persists until replaced; (0, 0) clears the injection.
1054
- * Combines with key state in getVector (clamped to unit length).
1055
- */
1056
- setActionVector(action, x, y) {
1057
- if (this.get(action).type !== "vector2") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a button — use pressAction/releaseAction, not setActionVector.`);
1058
- if (!Number.isFinite(x) || !Number.isFinite(y)) throw new IncantoError("BAD_FORMAT", `setActionVector('${action}') needs finite numbers, got (${x}, ${y}).`);
1059
- if (x === 0 && y === 0) this.injectedVectors.delete(action);
1060
- else this.injectedVectors.set(action, {
1061
- x,
1062
- y
1063
- });
1064
- }
1065
- /**
1066
- * Actions the scene wants on-screen touch controls for, in declaration
1067
- * order (`"touch": "joystick"` on vector2, `"touch": "button"` on buttons).
1068
- * The touch overlay (core/touch.ts) renders these.
1069
- */
1070
- touchControls() {
1071
- const out = [];
1072
- for (const [name, def] of this.actions) if (def.touch) out.push({
1073
- action: name,
1074
- kind: def.touch
1075
- });
1076
- return out;
1077
- }
1078
- /** Drop all injected action state (held buttons, vectors, pending edges). */
1079
- clearInjected() {
1080
- this.injectedDown.clear();
1081
- this.injectedPressed.clear();
1082
- this.injectedReleased.clear();
1083
- this.injectedVectors.clear();
1084
- }
1085
- dx = 0;
1086
- dy = 0;
1087
- wheel = 0;
1088
- /** Mouse buttons feed the same code space as keys: Mouse0/Mouse1/Mouse2. */
1089
- handleMouseButton(button, isDown) {
1090
- this.handleKey(`Mouse${button}`, isDown);
1091
- }
1092
- /** Accumulate look deltas (movementX/Y under pointer lock, else move deltas). */
1093
- handlePointerMove(dx, dy) {
1094
- this.dx += dx;
1095
- this.dy += dy;
1096
- }
1097
- handleWheel(deltaY) {
1098
- this.wheel += deltaY;
1099
- }
1100
- /** Drain the accumulated pointer delta (read once per frame). */
1101
- pointerDelta() {
1102
- const d = {
1103
- x: this.dx,
1104
- y: this.dy
1105
- };
1106
- this.dx = 0;
1107
- this.dy = 0;
1108
- return d;
1109
- }
1110
- /** Drain the accumulated wheel delta. */
1111
- wheelDelta() {
1112
- const w = this.wheel;
1113
- this.wheel = 0;
1114
- return w;
1115
- }
1116
- /**
1117
- * Wire browser pointer events on a canvas: buttons → Mouse0/1/2 codes,
1118
- * movement → pointerDelta (movementX/Y so pointer lock just works),
1119
- * wheel → wheelDelta. `lockOnClick` requests pointer lock on mousedown
1120
- * (the FPS pattern).
1121
- */
1122
- padAxesState = {
1123
- lx: 0,
1124
- ly: 0,
1125
- rx: 0,
1126
- ry: 0
1127
- };
1128
- padButtonsDown = 0;
1129
- /**
1130
- * Feed one polled gamepad snapshot (standard mapping). Buttons become
1131
- * codes `Pad0`..`Pad16` in the same space as keys — declare them in input
1132
- * actions like any key ("jump": ["Space", "Pad0"]). Axes land in
1133
- * `padAxes()` with a deadzone. Pass null when no pad is connected.
1134
- */
1135
- pollGamepad(pad) {
1136
- const dead = .15;
1137
- const shape = (v) => Math.abs(v) < dead ? 0 : v;
1138
- if (!pad) {
1139
- for (let i = 0; i < 17; i++) if (this.padButtonsDown & 1 << i) this.handleKey(`Pad${i}`, false);
1140
- this.padButtonsDown = 0;
1141
- this.padAxesState.lx = 0;
1142
- this.padAxesState.ly = 0;
1143
- this.padAxesState.rx = 0;
1144
- this.padAxesState.ry = 0;
1145
- return;
1146
- }
1147
- for (let i = 0; i < Math.min(17, pad.buttons.length); i++) {
1148
- const down = pad.buttons[i]?.pressed === true;
1149
- if (down !== ((this.padButtonsDown & 1 << i) !== 0)) {
1150
- this.handleKey(`Pad${i}`, down);
1151
- this.padButtonsDown = down ? this.padButtonsDown | 1 << i : this.padButtonsDown & ~(1 << i);
1152
- }
1153
- }
1154
- this.padAxesState.lx = shape(pad.axes[0] ?? 0);
1155
- this.padAxesState.ly = shape(pad.axes[1] ?? 0);
1156
- this.padAxesState.rx = shape(pad.axes[2] ?? 0);
1157
- this.padAxesState.ry = shape(pad.axes[3] ?? 0);
1158
- }
1159
- /** Deadzoned analog sticks: 0 = left {x,y}, 1 = right. */
1160
- padAxes(stick = 0) {
1161
- return stick === 0 ? {
1162
- x: this.padAxesState.lx,
1163
- y: this.padAxesState.ly
1164
- } : {
1165
- x: this.padAxesState.rx,
1166
- y: this.padAxesState.ry
1167
- };
1168
- }
1169
- /**
1170
- * Poll the browser Gamepad API every frame (the first connected pad).
1171
- * Wire once at boot: `engine.input.attachGamepad(engine)`. Headless no-op.
1172
- */
1173
- attachGamepad(engine) {
1174
- if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") return () => {};
1175
- const disconnect = engine.updated.connect(() => {
1176
- const pads = navigator.getGamepads();
1177
- let pad = null;
1178
- for (const p of pads) if (p?.connected) {
1179
- pad = p;
1180
- break;
1181
- }
1182
- this.pollGamepad(pad);
1183
- });
1184
- const prev = this.detach;
1185
- this.detach = () => {
1186
- prev?.();
1187
- disconnect();
1188
- };
1189
- return disconnect;
1190
- }
1191
- attachPointer(target, opts) {
1192
- let buttonsHeld = 0;
1193
- const onDown = (e) => {
1194
- buttonsHeld++;
1195
- this.handleMouseButton(e.button, true);
1196
- if (opts?.lockOnClick && document.pointerLockElement !== target) target.requestPointerLock?.();
1197
- };
1198
- const onUp = (e) => {
1199
- buttonsHeld = Math.max(0, buttonsHeld - 1);
1200
- this.handleMouseButton(e.button, false);
1201
- };
1202
- const onMove = (e) => {
1203
- if (document.pointerLockElement === target || buttonsHeld > 0) this.handlePointerMove(e.movementX, e.movementY);
1204
- };
1205
- const onWheel = (e) => {
1206
- e.preventDefault();
1207
- this.handleWheel(e.deltaY);
1208
- };
1209
- const onContext = (e) => e.preventDefault();
1210
- target.addEventListener("mousedown", onDown);
1211
- window.addEventListener("mouseup", onUp);
1212
- window.addEventListener("mousemove", onMove);
1213
- target.addEventListener("wheel", onWheel, { passive: false });
1214
- target.addEventListener("contextmenu", onContext);
1215
- const detach = () => {
1216
- target.removeEventListener("mousedown", onDown);
1217
- window.removeEventListener("mouseup", onUp);
1218
- window.removeEventListener("mousemove", onMove);
1219
- target.removeEventListener("wheel", onWheel);
1220
- target.removeEventListener("contextmenu", onContext);
1221
- };
1222
- const prev = this.detach;
1223
- this.detach = () => {
1224
- prev?.();
1225
- detach();
1226
- };
1227
- return detach;
1228
- }
1229
- /**
1230
- * Wire browser keyboard events. Returns (and chains into dispose()) a detach
1231
- * function.
1232
- *
1233
- * By default the browser default is prevented for keys BOUND to a declared
1234
- * action — an embedded game must not scroll its host page on Space/arrows.
1235
- * Keys typed into editable elements (inputs, textareas, contenteditable)
1236
- * are ignored entirely so DOM UI overlays keep working.
1237
- */
1238
- attachKeyboard(target, opts) {
1239
- const mode = opts?.preventDefault ?? "bound";
1240
- const onDown = (e) => {
1241
- if (isEditableTarget(e.target)) return;
1242
- if ((e.ctrlKey || e.metaKey) && !e.code.startsWith("Control") && !e.code.startsWith("Meta")) return;
1243
- if (mode === "bound" && this.codeIsBound(e.code)) e.preventDefault?.();
1244
- if (!e.repeat) this.handleKey(e.code, true);
1245
- };
1246
- const onUp = (e) => {
1247
- if (isEditableTarget(e.target)) return;
1248
- this.handleKey(e.code, false);
1249
- };
1250
- target.addEventListener("keydown", onDown);
1251
- target.addEventListener("keyup", onUp);
1252
- const detach = () => {
1253
- target.removeEventListener("keydown", onDown);
1254
- target.removeEventListener("keyup", onUp);
1255
- };
1256
- const prev = this.detach;
1257
- this.detach = () => {
1258
- prev?.();
1259
- detach();
1260
- };
1261
- return detach;
1262
- }
1263
- /** Whether any declared action binds this key code. */
1264
- codeIsBound(code) {
1265
- for (const action of this.actions.values()) if (action.type === "button") {
1266
- if (action.keys.includes(code)) return true;
1267
- } else {
1268
- const k = action.keys;
1269
- if (k.up.includes(code) || k.down.includes(code) || k.left.includes(code) || k.right.includes(code)) return true;
1270
- }
1271
- return false;
1272
- }
1273
- dispose() {
1274
- this.detach?.();
1275
- this.detach = null;
1276
- }
1277
- isPressed(action) {
1278
- return this.button(action).keys.some((k) => this.down.has(k)) || this.injectedDown.has(action);
1279
- }
1280
- justPressed(action) {
1281
- return this.button(action).keys.some((k) => this.pressedEdge.has(k)) || this.injectedPressed.has(action);
1282
- }
1283
- justReleased(action) {
1284
- return this.button(action).keys.some((k) => this.releasedEdge.has(k)) || this.injectedReleased.has(action);
1285
- }
1286
- /** Normalized direction for a vector2 action (y-down: up = -y). */
1287
- getVector(action) {
1288
- const def = this.get(action);
1289
- if (def.type !== "vector2") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a button — use isPressed/justPressed, not getVector.`);
1290
- const active = (codes) => codes.some((k) => this.down.has(k)) ? 1 : 0;
1291
- let x = active(def.keys.right) - active(def.keys.left);
1292
- let y = active(def.keys.down) - active(def.keys.up);
1293
- const injected = this.injectedVectors.get(action);
1294
- if (injected) {
1295
- x += injected.x;
1296
- y += injected.y;
1297
- }
1298
- const len = Math.hypot(x, y);
1299
- if (len > 1) {
1300
- x /= len;
1301
- y /= len;
1302
- }
1303
- return {
1304
- x,
1305
- y
1306
- };
1307
- }
1308
- /** Consume one-frame edges. The Engine calls this at the end of every tick. */
1309
- endFrame() {
1310
- this.pressedEdge.clear();
1311
- this.releasedEdge.clear();
1312
- this.injectedPressed.clear();
1313
- this.injectedReleased.clear();
1314
- }
1315
- /**
1316
- * Drop all action declarations and injected action state (physical key
1317
- * state is kept). The Engine calls this on setScene so keybinds never
1318
- * bleed between scenes.
1319
- */
1320
- clear() {
1321
- this.actions.clear();
1322
- this.pressedEdge.clear();
1323
- this.releasedEdge.clear();
1324
- this.clearInjected();
1325
- }
1326
- get(action) {
1327
- const def = this.actions.get(action);
1328
- if (!def) throw new IncantoError("BAD_FORMAT", `Unknown input action '${action}'. Declared actions: [${[...this.actions.keys()].join(", ")}].`);
1329
- return def;
1330
- }
1331
- button(action) {
1332
- const def = this.get(action);
1333
- if (def.type !== "button") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a vector2 — use getVector, not button queries.`);
1334
- return def;
1335
- }
1336
- };
1337
- //#endregion
1338
963
  //#region src/core/log.ts
1339
964
  /**
1340
965
  * The engine's log channel (`engine.log`, `this.log` in a Behavior): a ring
@@ -1502,7 +1127,57 @@ var Engine = class {
1502
1127
  }
1503
1128
  start() {
1504
1129
  if (this.disposeScheduler) return;
1505
- this.disposeScheduler = this.scheduler((now) => this.tick(now));
1130
+ this.disposeScheduler = this.scheduler((now) => {
1131
+ try {
1132
+ this.tick(now);
1133
+ } catch (error) {
1134
+ this.reportFrameError(error);
1135
+ }
1136
+ });
1137
+ }
1138
+ /**
1139
+ * A frame threw somewhere outside a single node (a renderer, physics, a
1140
+ * signal handler). Report the first one and count the rest: a broken frame
1141
+ * repeats sixty times a second, and sixty identical traces a second is how
1142
+ * you lose the first one.
1143
+ */
1144
+ reportFrameError(error) {
1145
+ this.errorCount += 1;
1146
+ const text = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
1147
+ if (text === this.lastFrameError) return;
1148
+ this.lastFrameError = text;
1149
+ this.log.error("[incanto] a frame threw — the loop keeps running:", error);
1150
+ console.error("[incanto] a frame threw — the loop keeps running:", error);
1151
+ }
1152
+ /** @internal A node quarantined itself; count it for `stats().errors`. */
1153
+ _nodeErrored(_node) {
1154
+ this.errorCount += 1;
1155
+ }
1156
+ errorCount = 0;
1157
+ lastFrameError = null;
1158
+ /**
1159
+ * Every node with something quarantined — its own update, its script, or
1160
+ * both — so a tool can list them and put them back. Walks on demand; there is
1161
+ * no per-frame bookkeeping for this.
1162
+ */
1163
+ erroredNodes() {
1164
+ const out = [];
1165
+ const walk = (node) => {
1166
+ if (node.errored || node.behaviorErrored) out.push(node);
1167
+ for (const child of node.children) walk(child);
1168
+ };
1169
+ if (this._scene?.root) walk(this._scene.root);
1170
+ return out;
1171
+ }
1172
+ /** Un-skip every quarantined node — the "try again" after you fix the code. */
1173
+ resumeErroredNodes() {
1174
+ const nodes = this.erroredNodes();
1175
+ for (const node of nodes) {
1176
+ node.errored = false;
1177
+ node.behaviorErrored = false;
1178
+ }
1179
+ this.lastFrameError = null;
1180
+ return nodes.length;
1506
1181
  }
1507
1182
  stop() {
1508
1183
  this.disposeScheduler?.();
@@ -1522,7 +1197,8 @@ var Engine = class {
1522
1197
  fps: this.frameStats.fps(),
1523
1198
  frameMs: this.frameStats.frameMs(),
1524
1199
  nodes: this._scene?.tree.root ? countNodes(this._scene.tree.root) : 0,
1525
- running: this.disposeScheduler !== null
1200
+ running: this.disposeScheduler !== null,
1201
+ errors: this.errorCount
1526
1202
  };
1527
1203
  }
1528
1204
  /**
@@ -2370,4 +2046,4 @@ function registerCoreNodes() {
2370
2046
  registerNode(UiDialogue);
2371
2047
  }
2372
2048
  //#endregion
2373
- export { crossfadeGains as C, WebAudioMusicBackend as S, AudioBuses as T, spatialPan as _, HudLayer as a, synthSfx as b, UiText as c, LogManager as d, InputMap as f, spatialGain as g, ROLLOFF_MODELS as h, UiDialogue as i, AudioPlayer as l, isAudioContextAvailable as m, Timer as n, UiBanner as o, SfxEngine as p, UiButton as r, UiBar as s, registerCoreNodes as t, Engine as u, SFX_PRESETS as v, fadeGain as w, MusicManager as x, SFX_PRESET_NAMES as y };
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 };
@@ -1,9 +1,9 @@
1
- import { f as Node } from "./loader-BZqOKfI2.js";
2
- import { t as registerCoreNodes } from "./register-BTg0EM7s.js";
1
+ import { f as Node } from "./loader-BlaRQGaA.js";
2
+ import { t as registerCoreNodes } from "./register-Dzkd6-os.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
6
- import { r as effectiveOrder } from "./duplicate-BgtFrFo4.js";
5
+ import { i as getNodeSchema, l as registerNode } from "./registry-CJdGpT2V.js";
6
+ import { r as effectiveOrder } from "./duplicate-KRPtUtzl.js";
7
7
  import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
8
8
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry } from "three";
9
9
  //#region src/2d/physics/collider-validate.ts
@@ -8,7 +8,11 @@ import { r as jsonKind, t as jsonClone } from "./json-BLk7H2Qa.js";
8
8
  * - non-empty default arrays are FIXED templates: same length, element-wise match
9
9
  * (empty default arrays are unconstrained — variable-length lists)
10
10
  */
11
- function validatePropValue(value, def, at) {
11
+ function validatePropValue(value, def, at, options) {
12
+ if (options && typeof value === "string" && !options.includes(value)) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${at}' is ${JSON.stringify(value)}, which is not one of: ${options.map((o) => JSON.stringify(o)).join(", ")}.`, {
13
+ prop: at,
14
+ validOptions: [...options]
15
+ });
12
16
  const expected = jsonKind(def);
13
17
  if (expected === "null") return;
14
18
  const actual = jsonKind(value);
@@ -114,7 +118,7 @@ function applySchemaProps(target, schema, props, label, aliases = {}) {
114
118
  validOptions: valid
115
119
  });
116
120
  }
117
- validatePropValue(value, def.default, `${label}.${key}`);
121
+ validatePropValue(value, def.default, `${label}.${key}`, def.options);
118
122
  target[key] = jsonClone(value);
119
123
  }
120
124
  }
package/dist/test.d.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { $ as LogEntry, X as Node, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-D_jMpFh8.js";
1
2
  import { c as JsonValue, i as SceneJson } from "./schema-CcoWb32N.js";
2
- import { $ as Node, T as LogEntry, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAc0erXF.js";
3
- import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
3
+ import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
4
4
  import { i as auditScene, t as IncantoError } from "./errors-1dXlIwoR.js";
5
5
 
6
6
  //#region src/test/index.d.ts
@@ -74,11 +74,6 @@ interface ValidationResult {
74
74
  /** The hard load error, when ok is false — `details` is machine-readable. */
75
75
  error?: IncantoError;
76
76
  }
77
- /**
78
- * Run every hard load-time check headlessly. Unregistered behaviors are
79
- * stubbed by default (structure-only validation, no TypeScript needed) —
80
- * pass the real classes via `behaviors` to validate script props too.
81
- */
82
77
  declare function validateScene(json: unknown, opts?: ValidateSceneOptions): ValidationResult;
83
78
  interface RunContext {
84
79
  engine: Engine;
package/dist/test.js CHANGED
@@ -1,13 +1,13 @@
1
- import { _ as registerBehavior, n as loadScene } from "./loader-BZqOKfI2.js";
2
- import { u as Engine } from "./register-BTg0EM7s.js";
1
+ import { n as loadScene, y as registerBehavior } from "./loader-BlaRQGaA.js";
2
+ import { u as Engine } from "./register-Dzkd6-os.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { t as auditScene } from "./audit-C6rMyict.js";
4
+ import { t as auditScene } from "./audit-C4kmDK0o.js";
5
5
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
6
- import { i as getNodeSchema, s as mergeStaticProps } from "./registry-BVJ2HbCn.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
8
- import { t as registerNodes2D } from "./register-DWcWq4QG.js";
9
- import { t as registerNodes3D } from "./register-DJ0SByQg.js";
10
- import { t as registerNodesNet } from "./register-MelqEdza.js";
6
+ import { i as getNodeSchema, s as mergeStaticProps } from "./registry-CJdGpT2V.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-BftxM_It.js";
8
+ import { t as registerNodes2D } from "./register-tkR_8tWg.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets--DigHNg4.js";
10
+ import { t as registerNodesNet } from "./register-CUY284Is.js";
11
11
  //#region src/test/index.ts
12
12
  /**
13
13
  * incanto/test — the browserless verification harness.
@@ -184,10 +184,28 @@ function defaultsFor(typeName) {
184
184
  * stubbed by default (structure-only validation, no TypeScript needed) —
185
185
  * pass the real classes via `behaviors` to validate script props too.
186
186
  */
187
+ /**
188
+ * `environment.preset` is resolved deep inside Renderer3D, which no headless
189
+ * tool reaches — so a typo there passed `incanto-check` and hard-failed at boot.
190
+ * It cannot live in the core loader (core must never import 3d), but it belongs
191
+ * exactly here: the test entry already registers the 3D nodes.
192
+ */
193
+ function checkEnvironment(json) {
194
+ const env = json?.environment;
195
+ if (!env || typeof env !== "object") return;
196
+ if (env.preset === void 0) return;
197
+ if (typeof env.preset !== "string") throw new IncantoError("BAD_FORMAT", `Scene "environment.preset" must be a string, got ${JSON.stringify(env.preset)}.`);
198
+ try {
199
+ resolveEnvironmentHdri(env);
200
+ } catch (error) {
201
+ throw new IncantoError("BAD_FORMAT", error instanceof Error ? error.message : String(error));
202
+ }
203
+ }
187
204
  function validateScene(json, opts = {}) {
188
205
  registerAllNodes();
189
206
  for (const [name, ctor] of Object.entries(opts.behaviors ?? {})) registerBehavior(name, ctor, { replace: true });
190
207
  try {
208
+ checkEnvironment(json);
191
209
  loadScene(json, {
192
210
  resolveScene: opts.resolveScene,
193
211
  stubMissingBehaviors: !opts.strictBehaviors
@@ -218,10 +236,10 @@ async function runScript(json, opts) {
218
236
  engine.setScene(scene);
219
237
  const physics = opts.physics ?? "auto";
220
238
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
221
- const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
239
+ const { enablePhysics2D } = await import("./physics-2d-D9wquBvK.js").then((n) => n.r);
222
240
  await enablePhysics2D(engine);
223
241
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
224
- const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
242
+ const { enablePhysics3D } = await import("./physics-3d-CnPygVGo.js").then((n) => n.r);
225
243
  await enablePhysics3D(engine);
226
244
  }
227
245
  const failures = [];
@@ -284,6 +302,12 @@ async function runScript(json, opts) {
284
302
  }
285
303
  const finalCapture = captureScene(scene);
286
304
  const logs = [...engine.log.entries()];
305
+ const swallowed = engine.stats().errors;
306
+ if (swallowed > 0) failures.push({
307
+ atMs: opts.durationMs,
308
+ label: "engine",
309
+ message: `${swallowed} error(s) were swallowed to keep the loop alive — the logs above name the node, the behavior and the phase`
310
+ });
287
311
  engine.dispose();
288
312
  const ok = failures.length === 0;
289
313
  return {
@@ -293,9 +317,14 @@ async function runScript(json, opts) {
293
317
  logs,
294
318
  finalCapture,
295
319
  describe: () => {
320
+ const engineWarnings = logs.filter((l) => l.level === "warn" || l.level === "error");
321
+ const head = `run ${ok ? "OK" : "FAILED"} — ${opts.durationMs}ms simulated, ${failures.length} failure(s), ${logs.length} log(s)`;
322
+ const fails = failures.map((f) => ` ✗ at ${f.atMs}ms${f.label ? ` [${f.label}]` : ""}: ${f.message}`);
323
+ const noisy = engineWarnings.map((l) => ` ! ${l.level}: ${l.parts.map((p) => typeof p === "string" ? p : String(p)).join(" ")}`);
296
324
  return [
297
- `run ${ok ? "OK" : "FAILED"} — ${opts.durationMs}ms simulated, ${failures.length} failure(s), ${logs.length} log(s)`,
298
- ...failures.map((f) => ` ✗ at ${f.atMs}ms${f.label ? ` [${f.label}]` : ""}: ${f.message}`),
325
+ head,
326
+ ...fails,
327
+ ...noisy,
299
328
  describeCapture(finalCapture)
300
329
  ].join("\n");
301
330
  }
@@ -323,10 +352,10 @@ async function createPlaySession(json, opts = {}) {
323
352
  engine.setScene(scene);
324
353
  const physics = opts.physics ?? "auto";
325
354
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
326
- const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
355
+ const { enablePhysics2D } = await import("./physics-2d-D9wquBvK.js").then((n) => n.r);
327
356
  await enablePhysics2D(engine);
328
357
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
329
- const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
358
+ const { enablePhysics3D } = await import("./physics-3d-CnPygVGo.js").then((n) => n.r);
330
359
  await enablePhysics3D(engine);
331
360
  }
332
361
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -1 +1 @@
1
- import{n as e}from"./index-BO6WU8by.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};
1
+ import{n as e}from"./index-C9fb5QcT.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};