incanto 0.30.0 → 0.32.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/bin/incanto-play.mjs +7 -2
  2. package/dist/2d.d.ts +12 -5
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +67 -6
  5. package/dist/3d.js +4 -4
  6. package/dist/{audit-C6rMyict.js → audit-C4kmDK0o.js} +16 -0
  7. package/dist/{behavior-BAc0erXF.d.ts → behavior-D_jMpFh8.d.ts} +117 -55
  8. package/dist/{create-game-5z_QVtLx.js → create-game-bKHgHcsZ.js} +19 -10
  9. package/dist/{create-game-DuBTv2zI.js → create-game-gXI7PYl0.js} +61 -46
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/debug.js +7 -2
  12. package/dist/{duplicate-BgtFrFo4.js → duplicate-KRPtUtzl.js} +1 -1
  13. package/dist/{editor-switch-B0wB_DSr.d.ts → editor-switch-BJb-CWfA.d.ts} +12 -1
  14. package/dist/{register-DJ0SByQg.js → environment-presets--DigHNg4.js} +42 -11
  15. package/dist/{gameplay-Cfr6aFZ1.js → gameplay-BftxM_It.js} +2 -2
  16. package/dist/gameplay.d.ts +1 -1
  17. package/dist/gameplay.js +1 -1
  18. package/dist/index.d.ts +13 -3
  19. package/dist/index.js +7 -7
  20. package/dist/{loader-B242FF6N.d.ts → loader-BYBrqTxP.d.ts} +1 -1
  21. package/dist/{loader-BZqOKfI2.js → loader-BlaRQGaA.js} +482 -6
  22. package/dist/net.d.ts +1 -1
  23. package/dist/net.js +3 -3
  24. package/dist/{pathfinding-BwD974Ss.d.ts → pathfinding-BwhqPD3i.d.ts} +1 -1
  25. package/dist/{physics-2d-3kOQCtgd.js → physics-2d-D9wquBvK.js} +4 -3
  26. package/dist/{physics-3d-CeRH-Ff_.js → physics-3d-CnPygVGo.js} +5 -4
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-MelqEdza.js → register-CUY284Is.js} +3 -3
  30. package/dist/{register-BTg0EM7s.js → register-Dzkd6-os.js} +56 -380
  31. package/dist/{register-DWcWq4QG.js → register-tkR_8tWg.js} +4 -4
  32. package/dist/{registry-BVJ2HbCn.js → registry-CJdGpT2V.js} +6 -2
  33. package/dist/test-WwRIlXsK.js +642 -0
  34. package/dist/test.d.ts +55 -8
  35. package/dist/test.js +3 -364
  36. package/dist/{touch-031PxtCR.js → touch-BoNg_MnF.js} +2 -1
  37. package/dist/vite.js +1 -1
  38. package/editor/assets/{agent8-CAp0i5qn.js → agent8-C5k1nTCH.js} +1 -1
  39. package/editor/assets/{debug-BoEYfbqK.js → debug-BT_0mjk-.js} +3 -2
  40. package/editor/assets/{index-BO6WU8by.js → index-gfyrByWw.js} +90 -90
  41. package/editor/index.html +1 -1
  42. package/package.json +1 -1
  43. package/skills/README.md +14 -2
  44. package/skills/incanto-building-2d-games.md +16 -0
  45. package/skills/incanto-building-3d-games.md +16 -0
  46. package/skills/incanto-verifying-your-game.md +118 -0
  47. package/templates-app/beacon-isle-3d/package.json +1 -1
  48. package/templates-app/tps-3d/package.json +1 -1
  49. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
2
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
3
- import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-BVJ2HbCn.js";
3
+ import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-CJdGpT2V.js";
4
4
  //#region src/core/signal.ts
5
5
  var Signal = class {
6
6
  connections = [];
@@ -118,6 +118,402 @@ function createBehavior(name, props) {
118
118
  return behavior;
119
119
  }
120
120
  //#endregion
121
+ //#region src/core/input.ts
122
+ /** Keys typed into editable elements belong to the page UI, not the game. */
123
+ function isEditableTarget(target) {
124
+ const el = target;
125
+ return !!el && (el.isContentEditable === true || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT");
126
+ }
127
+ /**
128
+ * Declarative input from scene JSON `input{}` — DOM-free logic (feed key state
129
+ * via `handleKey`, or wire a browser with `attachKeyboard(window)`).
130
+ *
131
+ * Convention: vector2 follows the 2D y-down space (up = -y). `justPressed` /
132
+ * `justReleased` are one-frame edges, settled by the Engine each tick.
133
+ */
134
+ var InputMap = class {
135
+ actions = /* @__PURE__ */ new Map();
136
+ down = /* @__PURE__ */ new Set();
137
+ pressedEdge = /* @__PURE__ */ new Set();
138
+ releasedEdge = /* @__PURE__ */ new Set();
139
+ detach = null;
140
+ /** Load (or extend with) scene-JSON action declarations. Hard-validates shape. */
141
+ declare(decls) {
142
+ for (const [name, action] of parseInputDecls(decls)) this.actions.set(name, action);
143
+ }
144
+ /** Feed a key state change (code = KeyboardEvent.code, e.g. 'Space', 'KeyW'). */
145
+ handleKey(code, isDown) {
146
+ if (isDown) {
147
+ if (!this.down.has(code)) {
148
+ this.down.add(code);
149
+ this.pressedEdge.add(code);
150
+ }
151
+ } else if (this.down.has(code)) {
152
+ this.down.delete(code);
153
+ this.releasedEdge.add(code);
154
+ }
155
+ }
156
+ injectedDown = /* @__PURE__ */ new Set();
157
+ injectedPressed = /* @__PURE__ */ new Set();
158
+ injectedReleased = /* @__PURE__ */ new Set();
159
+ injectedVectors = /* @__PURE__ */ new Map();
160
+ /**
161
+ * Press a button ACTION directly — no key codes involved. This is how
162
+ * scripted gameplay tests and touch buttons drive the game by intent
163
+ * (`press('jump')`) instead of reverse-engineering keybinds.
164
+ */
165
+ pressAction(action) {
166
+ this.button(action);
167
+ if (!this.injectedDown.has(action)) {
168
+ this.injectedDown.add(action);
169
+ this.injectedPressed.add(action);
170
+ }
171
+ }
172
+ /** Release an injected button action (yields one justReleased frame). */
173
+ releaseAction(action) {
174
+ this.button(action);
175
+ if (this.injectedDown.delete(action)) this.injectedReleased.add(action);
176
+ }
177
+ /**
178
+ * Feed an analog direction for a vector2 ACTION — virtual joysticks and
179
+ * scripted runs. Persists until replaced; (0, 0) clears the injection.
180
+ * Combines with key state in getVector (clamped to unit length).
181
+ */
182
+ setActionVector(action, x, y) {
183
+ if (this.get(action).type !== "vector2") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a button — use pressAction/releaseAction, not setActionVector.`);
184
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new IncantoError("BAD_FORMAT", `setActionVector('${action}') needs finite numbers, got (${x}, ${y}).`);
185
+ if (x === 0 && y === 0) this.injectedVectors.delete(action);
186
+ else this.injectedVectors.set(action, {
187
+ x,
188
+ y
189
+ });
190
+ }
191
+ /**
192
+ * Actions the scene wants on-screen touch controls for, in declaration
193
+ * order (`"touch": "joystick"` on vector2, `"touch": "button"` on buttons).
194
+ * The touch overlay (core/touch.ts) renders these.
195
+ */
196
+ touchControls() {
197
+ const out = [];
198
+ for (const [name, def] of this.actions) if (def.touch) out.push({
199
+ action: name,
200
+ kind: def.touch
201
+ });
202
+ return out;
203
+ }
204
+ /** Drop all injected action state (held buttons, vectors, pending edges). */
205
+ clearInjected() {
206
+ this.injectedDown.clear();
207
+ this.injectedPressed.clear();
208
+ this.injectedReleased.clear();
209
+ this.injectedVectors.clear();
210
+ }
211
+ dx = 0;
212
+ dy = 0;
213
+ wheel = 0;
214
+ /** Mouse buttons feed the same code space as keys: Mouse0/Mouse1/Mouse2. */
215
+ handleMouseButton(button, isDown) {
216
+ this.handleKey(`Mouse${button}`, isDown);
217
+ }
218
+ /** Accumulate look deltas (movementX/Y under pointer lock, else move deltas). */
219
+ handlePointerMove(dx, dy) {
220
+ this.dx += dx;
221
+ this.dy += dy;
222
+ }
223
+ handleWheel(deltaY) {
224
+ this.wheel += deltaY;
225
+ }
226
+ /** Drain the accumulated pointer delta (read once per frame). */
227
+ pointerDelta() {
228
+ const d = {
229
+ x: this.dx,
230
+ y: this.dy
231
+ };
232
+ this.dx = 0;
233
+ this.dy = 0;
234
+ return d;
235
+ }
236
+ /** Drain the accumulated wheel delta. */
237
+ wheelDelta() {
238
+ const w = this.wheel;
239
+ this.wheel = 0;
240
+ return w;
241
+ }
242
+ /**
243
+ * Wire browser pointer events on a canvas: buttons → Mouse0/1/2 codes,
244
+ * movement → pointerDelta (movementX/Y so pointer lock just works),
245
+ * wheel → wheelDelta. `lockOnClick` requests pointer lock on mousedown
246
+ * (the FPS pattern).
247
+ */
248
+ padAxesState = {
249
+ lx: 0,
250
+ ly: 0,
251
+ rx: 0,
252
+ ry: 0
253
+ };
254
+ padButtonsDown = 0;
255
+ /**
256
+ * Feed one polled gamepad snapshot (standard mapping). Buttons become
257
+ * codes `Pad0`..`Pad16` in the same space as keys — declare them in input
258
+ * actions like any key ("jump": ["Space", "Pad0"]). Axes land in
259
+ * `padAxes()` with a deadzone. Pass null when no pad is connected.
260
+ */
261
+ pollGamepad(pad) {
262
+ const dead = .15;
263
+ const shape = (v) => Math.abs(v) < dead ? 0 : v;
264
+ if (!pad) {
265
+ for (let i = 0; i < 17; i++) if (this.padButtonsDown & 1 << i) this.handleKey(`Pad${i}`, false);
266
+ this.padButtonsDown = 0;
267
+ this.padAxesState.lx = 0;
268
+ this.padAxesState.ly = 0;
269
+ this.padAxesState.rx = 0;
270
+ this.padAxesState.ry = 0;
271
+ return;
272
+ }
273
+ for (let i = 0; i < Math.min(17, pad.buttons.length); i++) {
274
+ const down = pad.buttons[i]?.pressed === true;
275
+ if (down !== ((this.padButtonsDown & 1 << i) !== 0)) {
276
+ this.handleKey(`Pad${i}`, down);
277
+ this.padButtonsDown = down ? this.padButtonsDown | 1 << i : this.padButtonsDown & ~(1 << i);
278
+ }
279
+ }
280
+ this.padAxesState.lx = shape(pad.axes[0] ?? 0);
281
+ this.padAxesState.ly = shape(pad.axes[1] ?? 0);
282
+ this.padAxesState.rx = shape(pad.axes[2] ?? 0);
283
+ this.padAxesState.ry = shape(pad.axes[3] ?? 0);
284
+ }
285
+ /** Deadzoned analog sticks: 0 = left {x,y}, 1 = right. */
286
+ padAxes(stick = 0) {
287
+ return stick === 0 ? {
288
+ x: this.padAxesState.lx,
289
+ y: this.padAxesState.ly
290
+ } : {
291
+ x: this.padAxesState.rx,
292
+ y: this.padAxesState.ry
293
+ };
294
+ }
295
+ /**
296
+ * Poll the browser Gamepad API every frame (the first connected pad).
297
+ * Wire once at boot: `engine.input.attachGamepad(engine)`. Headless no-op.
298
+ */
299
+ attachGamepad(engine) {
300
+ if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") return () => {};
301
+ const disconnect = engine.updated.connect(() => {
302
+ const pads = navigator.getGamepads();
303
+ let pad = null;
304
+ for (const p of pads) if (p?.connected) {
305
+ pad = p;
306
+ break;
307
+ }
308
+ this.pollGamepad(pad);
309
+ });
310
+ const prev = this.detach;
311
+ this.detach = () => {
312
+ prev?.();
313
+ disconnect();
314
+ };
315
+ return disconnect;
316
+ }
317
+ attachPointer(target, opts) {
318
+ let buttonsHeld = 0;
319
+ const onDown = (e) => {
320
+ buttonsHeld++;
321
+ this.handleMouseButton(e.button, true);
322
+ if (opts?.lockOnClick && document.pointerLockElement !== target) target.requestPointerLock?.();
323
+ };
324
+ const onUp = (e) => {
325
+ buttonsHeld = Math.max(0, buttonsHeld - 1);
326
+ this.handleMouseButton(e.button, false);
327
+ };
328
+ const onMove = (e) => {
329
+ if (document.pointerLockElement === target || buttonsHeld > 0) this.handlePointerMove(e.movementX, e.movementY);
330
+ };
331
+ const onWheel = (e) => {
332
+ e.preventDefault();
333
+ this.handleWheel(e.deltaY);
334
+ };
335
+ const onContext = (e) => e.preventDefault();
336
+ target.addEventListener("mousedown", onDown);
337
+ window.addEventListener("mouseup", onUp);
338
+ window.addEventListener("mousemove", onMove);
339
+ target.addEventListener("wheel", onWheel, { passive: false });
340
+ target.addEventListener("contextmenu", onContext);
341
+ const detach = () => {
342
+ target.removeEventListener("mousedown", onDown);
343
+ window.removeEventListener("mouseup", onUp);
344
+ window.removeEventListener("mousemove", onMove);
345
+ target.removeEventListener("wheel", onWheel);
346
+ target.removeEventListener("contextmenu", onContext);
347
+ };
348
+ const prev = this.detach;
349
+ this.detach = () => {
350
+ prev?.();
351
+ detach();
352
+ };
353
+ return detach;
354
+ }
355
+ /**
356
+ * Wire browser keyboard events. Returns (and chains into dispose()) a detach
357
+ * function.
358
+ *
359
+ * By default the browser default is prevented for keys BOUND to a declared
360
+ * action — an embedded game must not scroll its host page on Space/arrows.
361
+ * Keys typed into editable elements (inputs, textareas, contenteditable)
362
+ * are ignored entirely so DOM UI overlays keep working.
363
+ */
364
+ attachKeyboard(target, opts) {
365
+ const mode = opts?.preventDefault ?? "bound";
366
+ const onDown = (e) => {
367
+ if (isEditableTarget(e.target)) return;
368
+ if ((e.ctrlKey || e.metaKey) && !e.code.startsWith("Control") && !e.code.startsWith("Meta")) return;
369
+ if (mode === "bound" && this.codeIsBound(e.code)) e.preventDefault?.();
370
+ if (!e.repeat) this.handleKey(e.code, true);
371
+ };
372
+ const onUp = (e) => {
373
+ if (isEditableTarget(e.target)) return;
374
+ this.handleKey(e.code, false);
375
+ };
376
+ target.addEventListener("keydown", onDown);
377
+ target.addEventListener("keyup", onUp);
378
+ const detach = () => {
379
+ target.removeEventListener("keydown", onDown);
380
+ target.removeEventListener("keyup", onUp);
381
+ };
382
+ const prev = this.detach;
383
+ this.detach = () => {
384
+ prev?.();
385
+ detach();
386
+ };
387
+ return detach;
388
+ }
389
+ /** Whether any declared action binds this key code. */
390
+ codeIsBound(code) {
391
+ for (const action of this.actions.values()) if (action.type === "button") {
392
+ if (action.keys.includes(code)) return true;
393
+ } else {
394
+ const k = action.keys;
395
+ if (k.up.includes(code) || k.down.includes(code) || k.left.includes(code) || k.right.includes(code)) return true;
396
+ }
397
+ return false;
398
+ }
399
+ dispose() {
400
+ this.detach?.();
401
+ this.detach = null;
402
+ }
403
+ isPressed(action) {
404
+ return this.button(action).keys.some((k) => this.down.has(k)) || this.injectedDown.has(action);
405
+ }
406
+ justPressed(action) {
407
+ return this.button(action).keys.some((k) => this.pressedEdge.has(k)) || this.injectedPressed.has(action);
408
+ }
409
+ justReleased(action) {
410
+ return this.button(action).keys.some((k) => this.releasedEdge.has(k)) || this.injectedReleased.has(action);
411
+ }
412
+ /** Normalized direction for a vector2 action (y-down: up = -y). */
413
+ getVector(action) {
414
+ const def = this.get(action);
415
+ if (def.type !== "vector2") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a button — use isPressed/justPressed, not getVector.`);
416
+ const active = (codes) => codes.some((k) => this.down.has(k)) ? 1 : 0;
417
+ let x = active(def.keys.right) - active(def.keys.left);
418
+ let y = active(def.keys.down) - active(def.keys.up);
419
+ const injected = this.injectedVectors.get(action);
420
+ if (injected) {
421
+ x += injected.x;
422
+ y += injected.y;
423
+ }
424
+ const len = Math.hypot(x, y);
425
+ if (len > 1) {
426
+ x /= len;
427
+ y /= len;
428
+ }
429
+ return {
430
+ x,
431
+ y
432
+ };
433
+ }
434
+ /** Consume one-frame edges. The Engine calls this at the end of every tick. */
435
+ endFrame() {
436
+ this.pressedEdge.clear();
437
+ this.releasedEdge.clear();
438
+ this.injectedPressed.clear();
439
+ this.injectedReleased.clear();
440
+ }
441
+ /**
442
+ * Drop all action declarations and injected action state (physical key
443
+ * state is kept). The Engine calls this on setScene so keybinds never
444
+ * bleed between scenes.
445
+ */
446
+ clear() {
447
+ this.actions.clear();
448
+ this.pressedEdge.clear();
449
+ this.releasedEdge.clear();
450
+ this.clearInjected();
451
+ }
452
+ get(action) {
453
+ const def = this.actions.get(action);
454
+ if (!def) throw new IncantoError("BAD_FORMAT", `Unknown input action '${action}'. Declared actions: [${[...this.actions.keys()].join(", ")}].`);
455
+ return def;
456
+ }
457
+ button(action) {
458
+ const def = this.get(action);
459
+ if (def.type !== "button") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a vector2 — use getVector, not button queries.`);
460
+ return def;
461
+ }
462
+ };
463
+ /**
464
+ * Parse and HARD-VALIDATE a scene's `input{}` block.
465
+ *
466
+ * Standalone because validating it used to require an Engine: the only caller
467
+ * was `setScene`, so `incanto-check` — the gate an agent runs after every edit —
468
+ * never looked at `input` at all. A typo there ("keys": "KeyW" instead of
469
+ * ["KeyW"]) passed the check and threw in the browser at boot.
470
+ */
471
+ function parseInputDecls(decls) {
472
+ const out = /* @__PURE__ */ new Map();
473
+ for (const [name, raw] of Object.entries(decls)) {
474
+ const decl = raw;
475
+ if (decl.type === "button") {
476
+ const keys = decl.keys;
477
+ 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.`);
478
+ 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)}.`);
479
+ out.set(name, {
480
+ type: "button",
481
+ keys: [...keys],
482
+ ...decl.touch === "button" ? { touch: "button" } : {}
483
+ });
484
+ } else if (decl.type === "vector2") {
485
+ const dirs = decl.keys;
486
+ for (const dir of [
487
+ "up",
488
+ "down",
489
+ "left",
490
+ "right"
491
+ ]) if (!Array.isArray(dirs?.[dir])) throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "keys" needs arrays for up/down/left/right.`);
492
+ 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)}.`);
493
+ const d = dirs;
494
+ out.set(name, {
495
+ type: "vector2",
496
+ keys: {
497
+ up: [...d.up],
498
+ down: [...d.down],
499
+ left: [...d.left],
500
+ right: [...d.right]
501
+ },
502
+ ...decl.touch === "joystick" ? { touch: "joystick" } : {}
503
+ });
504
+ } else throw new IncantoError("BAD_FORMAT", `Input action '${name}': "type" must be 'button' or 'vector2', got ${JSON.stringify(decl.type)}.`);
505
+ }
506
+ return out;
507
+ }
508
+ //#endregion
509
+ //#region src/core/diagnostics.ts
510
+ function diagnose(engine, level, ...parts) {
511
+ engine?.log[level](...parts);
512
+ if (level === "error") console.error(...parts);
513
+ else if (level === "warn") console.warn(...parts);
514
+ else console.info(...parts);
515
+ }
516
+ //#endregion
121
517
  //#region src/core/node-path.ts
122
518
  function parseNodePath(path) {
123
519
  if (path === "") throw new IncantoError("BAD_NODE_PATH", "Node path must not be empty.");
@@ -432,18 +828,86 @@ var Node = class {
432
828
  }
433
829
  /** @internal */
434
830
  _propagateUpdate(dt) {
435
- this.update(dt);
436
- this.behavior?.update?.(dt);
831
+ if (!this.errored) try {
832
+ this.update(dt);
833
+ } catch (error) {
834
+ this._quarantine("update", error, "node");
835
+ }
836
+ if (this.behavior && !this.behaviorErrored) try {
837
+ this.behavior.update?.(dt);
838
+ } catch (error) {
839
+ this._quarantine("update", error, "behavior");
840
+ }
437
841
  const kids = this._children;
438
842
  for (let i = 0, n = kids.length; i < n; i++) kids[i]?._propagateUpdate(dt);
439
843
  }
440
844
  /** @internal */
441
845
  _propagateFixedUpdate(dt) {
442
- this.fixedUpdate(dt);
443
- this.behavior?.fixedUpdate?.(dt);
846
+ if (!this.errored) try {
847
+ this.fixedUpdate(dt);
848
+ } catch (error) {
849
+ this._quarantine("fixedUpdate", error, "node");
850
+ }
851
+ if (this.behavior && !this.behaviorErrored) try {
852
+ this.behavior.fixedUpdate?.(dt);
853
+ } catch (error) {
854
+ this._quarantine("fixedUpdate", error, "behavior");
855
+ }
444
856
  const kids = this._children;
445
857
  for (let i = 0, n = kids.length; i < n; i++) kids[i]?._propagateFixedUpdate(dt);
446
858
  }
859
+ /**
860
+ * Report an engine-level problem about THIS node.
861
+ *
862
+ * Goes to `engine.log` (what the debug overlay, `runScript()` and any agent
863
+ * tool read) as well as the console. A `console.warn` alone is invisible to
864
+ * every channel except a human with devtools open.
865
+ */
866
+ diagnose(level, ...parts) {
867
+ diagnose(this._tree?.engine ?? null, level, ...parts);
868
+ }
869
+ /**
870
+ * This node threw and is being skipped. Its CHILDREN still run.
871
+ *
872
+ * The alternative is what used to happen: the exception escaped into the rAF
873
+ * callback, the loop was never rescheduled, and the game froze — permanently,
874
+ * silently, with `stats().running` still reporting true. In a vibe-coded
875
+ * project the least-tested code in the repo is a behavior's `update`, so this
876
+ * is not an edge case; it is Tuesday.
877
+ *
878
+ * Set it back to false to try the node again (the debug overlay's "resume").
879
+ */
880
+ errored = false;
881
+ /**
882
+ * This node's SCRIPT threw and is being skipped — the node itself still runs.
883
+ *
884
+ * A behavior is your code; the node is the engine's. A typo in the first must
885
+ * not switch off the second, or breaking a sword script stops the character
886
+ * from walking.
887
+ */
888
+ behaviorErrored = false;
889
+ /**
890
+ * Report ONCE and stop running this node's own update.
891
+ *
892
+ * Once, because a behavior that throws throws every frame: sixty identical
893
+ * stack traces a second buries the one line that matters and costs more than
894
+ * the game did.
895
+ */
896
+ _quarantine(phase, error, what) {
897
+ const where = `${this.getPath()} (${this.constructor.typeName ?? "Node"})`;
898
+ let message;
899
+ if (what === "behavior") {
900
+ this.behaviorErrored = true;
901
+ message = `[incanto] behavior '${this.behavior?.constructor.name ?? "?"}' on ${where} threw in ${phase} — THE SCRIPT is now skipped; the node itself and the rest of the scene keep running.`;
902
+ } else {
903
+ this.errored = true;
904
+ message = `[incanto] ${where} threw in ${phase} — this node is now SKIPPED. Its children and the rest of the scene keep running.`;
905
+ }
906
+ const engine = this._tree?.engine;
907
+ engine?.log.error(message, error);
908
+ engine?._nodeErrored(this);
909
+ console.error(message, error);
910
+ }
447
911
  onEnterTree() {}
448
912
  onReady() {}
449
913
  onExitTree() {}
@@ -841,6 +1305,18 @@ function validateHeader(json) {
841
1305
  if (json.dimension !== void 0 && json.dimension !== "2d" && json.dimension !== "3d") throw new IncantoError("BAD_FORMAT", `Scene "dimension" must be "2d" or "3d", got ${JSON.stringify(json.dimension)}.`);
842
1306
  if (typeof json.root !== "object" || json.root === null) throw new IncantoError("BAD_FORMAT", "Scene \"root\" must be a node object.");
843
1307
  resolveViewport(json.viewport);
1308
+ if (json.input !== void 0) {
1309
+ if (typeof json.input !== "object" || json.input === null || Array.isArray(json.input)) throw new IncantoError("BAD_FORMAT", "Scene \"input\" must be an object of action declarations.");
1310
+ parseInputDecls(json.input);
1311
+ }
1312
+ if (json.assets !== void 0) {
1313
+ if (typeof json.assets !== "object" || json.assets === null || Array.isArray(json.assets)) throw new IncantoError("BAD_FORMAT", "Scene \"assets\" must be an object of asset declarations.");
1314
+ for (const [key, decl] of Object.entries(json.assets)) {
1315
+ if (typeof decl !== "object" || decl === null || Array.isArray(decl)) throw new IncantoError("BAD_FORMAT", `Asset '${key}' must be an object, got ${JSON.stringify(decl)}.`);
1316
+ const url = decl.url;
1317
+ if (typeof url !== "string" || url === "") throw new IncantoError("BAD_FORMAT", `Asset '${key}' needs a non-empty "url" string.`);
1318
+ }
1319
+ }
844
1320
  }
845
1321
  /**
846
1322
  * Re-raise a node-build error with the node's absolute scene path attached —
@@ -978,4 +1454,4 @@ function validateUniqueUids(root) {
978
1454
  walk(root);
979
1455
  }
980
1456
  //#endregion
981
- export { registerBehavior as _, SCENE_FORMAT as a, SceneTree as c, resolveConstants as d, Node as f, getBehavior as g, clearBehaviors as h, serializeNode as i, CONST_REF_KEY as l, Behavior as m, loadScene as n, computeViewport as o, parseNodePath as p, Scene as r, resolveViewport as s, buildNodeJson as t, isConstRef as u, registeredBehaviors as v, Signal as y };
1457
+ export { clearBehaviors as _, SCENE_FORMAT as a, registeredBehaviors as b, SceneTree as c, resolveConstants as d, Node as f, Behavior as g, InputMap as h, serializeNode as i, CONST_REF_KEY as l, diagnose as m, loadScene as n, computeViewport as o, parseNodePath as p, Scene as r, resolveViewport as s, buildNodeJson as t, isConstRef as u, getBehavior as v, Signal as x, registerBehavior as y };
package/dist/net.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { X as Node, l as PropSchema, nt as Signal, v as Engine } from "./behavior-D_jMpFh8.js";
1
2
  import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
2
- import { $ as Node, l as PropSchema, tt as Signal, v as Engine } from "./behavior-BAc0erXF.js";
3
3
 
4
4
  //#region src/net/types.d.ts
5
5
  type Unsubscribe = () => void;
package/dist/net.js CHANGED
@@ -1,9 +1,9 @@
1
- import { n as loadScene } from "./loader-BZqOKfI2.js";
2
- import { u as Engine } from "./register-BTg0EM7s.js";
1
+ import { n as loadScene } 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
4
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
6
- import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-MelqEdza.js";
6
+ import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-CUY284Is.js";
7
7
  //#region src/net/loopback.ts
8
8
  /**
9
9
  * In-memory multiplayer hub implementing the SAME kernel contract as
@@ -1,4 +1,4 @@
1
- import { $ as Node, P as Listener, l as PropSchema } from "./behavior-BAc0erXF.js";
1
+ import { X as Node, j as Listener, l as PropSchema } from "./behavior-D_jMpFh8.js";
2
2
 
3
3
  //#region src/core/nodes/audio-player.d.ts
4
4
  /**
@@ -1,6 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { m as diagnose } from "./loader-BlaRQGaA.js";
2
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-DWcWq4QG.js";
4
+ import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-tkR_8tWg.js";
4
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
5
6
  //#region src/2d/physics/physics-2d.ts
6
7
  var physics_2d_exports = /* @__PURE__ */ __exportAll({
@@ -113,7 +114,7 @@ var Physics2D = class {
113
114
  a.emit(sig, b);
114
115
  b.emit(sig, a);
115
116
  } catch (error) {
116
- console.error(`incanto: error in a '${sig}' handler (${a.name} ↔ ${b.name}):`, error);
117
+ diagnose(this.engine, "error", `incanto: error in a '${sig}' handler (${a.name} ↔ ${b.name}):`, error);
117
118
  }
118
119
  });
119
120
  }
@@ -346,7 +347,7 @@ var Physics2D = class {
346
347
  }
347
348
  if (!this.warnedNoCollider.has(node)) {
348
349
  this.warnedNoCollider.add(node);
349
- console.warn(`[incanto] '${node.name}' has no collider — physics skips it.`);
350
+ diagnose(this.engine, "warn", `[incanto] '${node.name}' has no collider — physics skips it.`);
350
351
  }
351
352
  continue;
352
353
  }
@@ -1,8 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { m as diagnose } from "./loader-BlaRQGaA.js";
2
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-Cfr6aFZ1.js";
4
+ import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-BftxM_It.js";
4
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
5
- import { I as buildMeshGeometry, N as Joint3D, P as InstancedMesh3D, k as Terrain3D } from "./register-DJ0SByQg.js";
6
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets--DigHNg4.js";
6
7
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
7
8
  //#region src/3d/physics/collider-lines.ts
8
9
  /**
@@ -336,7 +337,7 @@ var Physics3D = class {
336
337
  a.emit(sig, b);
337
338
  b.emit(sig, a);
338
339
  } catch (error) {
339
- console.error(`incanto: error in a '${sig}' handler (${a.name} ↔ ${b.name}):`, error);
340
+ diagnose(this.engine, "error", `incanto: error in a '${sig}' handler (${a.name} ↔ ${b.name}):`, error);
340
341
  }
341
342
  });
342
343
  }
@@ -519,7 +520,7 @@ var Physics3D = class {
519
520
  }
520
521
  if (!this.warnedNoCollider.has(node)) {
521
522
  this.warnedNoCollider.add(node);
522
- console.warn(`[incanto] '${node.name}' has no collider — physics skips it.`);
523
+ diagnose(this.engine, "warn", `[incanto] '${node.name}' has no collider — physics skips it.`);
523
524
  }
524
525
  continue;
525
526
  }
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-D_jMpFh8.js";
1
2
  import { c as JsonValue } from "./schema-CcoWb32N.js";
2
- import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAc0erXF.js";
3
3
  import { CSSProperties, ReactNode } from "react";
4
4
 
5
5
  //#region src/react/index.d.ts
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DuBTv2zI.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-5z_QVtLx.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-gXI7PYl0.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-bKHgHcsZ.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,8 +1,8 @@
1
- import { f as Node, t as buildNodeJson, y as Signal } from "./loader-BZqOKfI2.js";
2
- import { t as registerCoreNodes } from "./register-BTg0EM7s.js";
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";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
- import { l as registerNode } from "./registry-BVJ2HbCn.js";
5
+ import { l as registerNode } from "./registry-CJdGpT2V.js";
6
6
  //#region src/net/network-manager.ts
7
7
  const managers = /* @__PURE__ */ new WeakMap();
8
8
  /**