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
package/dist/2d.d.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
2
+ import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
1
3
  import { i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
2
- import { $ as Node, C as RendererStats, S as GameStats, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-BAc0erXF.js";
3
- import { t as EditorSwitchOptions } from "./editor-switch-B0wB_DSr.js";
4
- import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
4
+ import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
5
5
  import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
6
6
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
7
7
  import * as RapierNs from "@dimforge/rapier2d-compat";
@@ -24,9 +24,15 @@ interface SheetInfo {
24
24
  * `"$key"`. The texture loader is injectable so tests run headless.
25
25
  */
26
26
  declare class AssetStore2D {
27
+ private readonly engine;
27
28
  private readonly entries;
28
29
  private readonly loader;
29
- constructor(loader?: (url: string, callbacks?: TextureLoadCallbacks) => Texture);
30
+ /**
31
+ * `engine` is optional so a bare store still constructs in tests. Given one,
32
+ * a texture that fails to load lands in `engine.log` — where the overlay and
33
+ * `runScript()` can see it — instead of only in the browser console.
34
+ */
35
+ constructor(loader?: (url: string, callbacks?: TextureLoadCallbacks) => Texture, engine?: DiagnosticSink | null);
30
36
  /** Load (or extend with) a scene's `assets` declarations. Hard-validates each entry. */
31
37
  load(assets: Record<string, JsonObject>): void;
32
38
  /** Load status of a declared asset KEY (no '$'), or undefined if unknown. */
@@ -757,7 +763,8 @@ declare class Renderer2D {
757
763
  private readonly worldCam;
758
764
  private readonly uiCam;
759
765
  private readonly engine;
760
- private readonly assets;
766
+ /** The texture store. Public so `renderer.assets.status(key)` is reachable. */
767
+ readonly assets: AssetStore2D;
761
768
  private readonly loadedAssetScenes;
762
769
  private readonly disconnect;
763
770
  private readonly canvas;
package/dist/2d.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-5z_QVtLx.js";
3
- import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-DWcWq4QG.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-3kOQCtgd.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-z5XaB1p5.js";
3
+ import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-tkR_8tWg.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-D9wquBvK.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset
package/dist/3d.d.ts CHANGED
@@ -1,9 +1,9 @@
1
+ import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
2
+ import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
1
3
  import { i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
2
- import { $ as Node, C as RendererStats, S as GameStats, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-BAc0erXF.js";
3
- import { t as EditorSwitchOptions } from "./editor-switch-B0wB_DSr.js";
4
- import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
4
+ import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
5
5
  import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
6
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-BwD974Ss.js";
6
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-BwhqPD3i.js";
7
7
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Vector3, WebGLRenderer } from "three";
8
8
  import { VRM } from "@pixiv/three-vrm";
9
9
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -44,12 +44,28 @@ interface AnimationEntry {
44
44
  * @pixiv/three-vrm (VRM 0.x facing fixed via rotateVRM0).
45
45
  */
46
46
  declare class AssetStore3D {
47
+ private readonly engine;
47
48
  private readonly models;
48
49
  private readonly animations;
49
50
  private readonly modelKeys;
50
51
  private readonly animationKeys;
51
52
  private readonly loader;
52
- constructor();
53
+ /**
54
+ * `engine` is optional only so a bare store still constructs in tests. Give it
55
+ * one and every load failure lands in `engine.log` — where the debug overlay,
56
+ * `runScript()` and any agent tool can see it. Without it a 404'd GLB was a
57
+ * console line and an invisible model.
58
+ */
59
+ constructor(engine?: DiagnosticSink | null);
60
+ /**
61
+ * Every asset that failed, with the reason — the answer to "why is my model
62
+ * not there". Empty is the healthy case.
63
+ */
64
+ errors(): Array<{
65
+ ref: string;
66
+ url: string;
67
+ error: string;
68
+ }>;
53
69
  /** Map a scene's `assets` header. */
54
70
  load(assets: JsonObject): void;
55
71
  /** Animation `$key`s declared by the scene (editor dropdowns). */
@@ -495,6 +511,14 @@ interface CreateGame3DOptions {
495
511
  interface GameRenderer {
496
512
  dispose(): void;
497
513
  stats?(): RendererStats;
514
+ /** Loaded models/animations — `assets.errors()` answers "why is my model missing". */
515
+ assets?: {
516
+ errors(): Array<{
517
+ ref: string;
518
+ url: string;
519
+ error: string;
520
+ }>;
521
+ };
498
522
  }
499
523
  interface Game3D {
500
524
  engine: Engine;
@@ -513,6 +537,18 @@ interface Game3D {
513
537
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
514
538
  stats(): GameStats;
515
539
  /**
540
+ * Assets that failed to load, with the reason.
541
+ *
542
+ * "The model just never appeared" used to be unanswerable from outside: the
543
+ * store was private on the renderer and the 404 went to the console only.
544
+ * Check this — and `stats().errors` — before assuming a scene is fine.
545
+ */
546
+ assetErrors(): Array<{
547
+ ref: string;
548
+ url: string;
549
+ error: string;
550
+ }>;
551
+ /**
516
552
  * Turn this page into the scene editor, right where the player is standing —
517
553
  * what the ☰ debug menu's "edit this scene" calls. Bind it to your own key or
518
554
  * button if you want the switch without the overlay.
@@ -2991,7 +3027,12 @@ declare class Renderer3D {
2991
3027
  private readonly engine;
2992
3028
  private readonly disconnect;
2993
3029
  private readonly canvas;
2994
- private readonly assets;
3030
+ /**
3031
+ * The model/animation store. Public so a game (and an agent's tool) can ask
3032
+ * `renderer.assets.errors()` why a model is not there — it used to be private,
3033
+ * so a 404'd GLB was unanswerable from outside.
3034
+ */
3035
+ readonly assets: AssetStore3D;
2995
3036
  private readonly ownsAssets;
2996
3037
  private readonly loadedAssetScenes;
2997
3038
  /** The scene whose GPU programs have been pre-compiled (warm-up, see render). */
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-Cfr6aFZ1.js";
3
- import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-DuBTv2zI.js";
4
- import { A as DEFAULT_TERRAIN_TEXTURE_BASE, B as movementState, C as FLOWER_VARIETIES, D as BoneAttachment3D, E as BoneLookAt3D, F as MeshInstance3D, L as QUARTER_PITCH, M as terrainThemeLayers, N as Joint3D, O as Billboard3D, P as InstancedMesh3D, R as cameraRelative, S as resolveFlowerDensity, T as Camera3D, V as rigPose, _ as DirectionalLight3D, a as Trail3D, b as DENSITY_PRESETS, c as findRiverCoverageGaps, d as riverStepFor, f as smoothCourse, g as LoftMesh3D, h as ModelInstance3D, i as Tree3D, j as TERRAIN_THEMES, k as Terrain3D, l as projectToRiver, m as Particles3D, n as VOXEL_PALETTE, o as River3D, p as traceDownhillPath, r as VoxelGrid3D, s as buildRiverRings, t as registerNodes3D, u as riverCarveChannels, v as OmniLight3D, w as CharacterController3D, x as Flowers3D, y as Foliage3D, z as keyboardIntensity } from "./register-DJ0SByQg.js";
2
+ import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BftxM_It.js";
3
+ import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-DqOjMBUS.js";
4
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, V as movementState, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets--DigHNg4.js";
5
5
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CeRH-Ff_.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CnPygVGo.js";
7
7
  //#region src/3d/environment-runtime.ts
8
8
  /**
9
9
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -25,6 +25,19 @@ const HUD_WIDGETS = new Set([
25
25
  "UiButton",
26
26
  "UiDialogue"
27
27
  ]);
28
+ const LIGHT_TYPES = new Set([
29
+ "DirectionalLight3D",
30
+ "OmniLight3D",
31
+ "SpotLight3D"
32
+ ]);
33
+ /** Does the scene header light anything by itself (sky, hdri, ambient, preset)? */
34
+ function isLit(environment) {
35
+ if (!environment) return false;
36
+ if (environment.preset !== void 0 || environment.sky !== void 0) return true;
37
+ if (environment.hdri !== void 0) return true;
38
+ const ambient = environment.ambient;
39
+ return typeof ambient?.intensity === "number" && ambient.intensity > 0;
40
+ }
28
41
  /** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
29
42
  function auditScene(scene) {
30
43
  const warnings = [];
@@ -32,6 +45,7 @@ function auditScene(scene) {
32
45
  if (!root) return warnings;
33
46
  let currentCameras = 0;
34
47
  let cameras = 0;
48
+ let lights = 0;
35
49
  const walk = (node, path, insideHud, staticRoot) => {
36
50
  const type = node.type ?? "";
37
51
  const props = node.props ?? {};
@@ -39,6 +53,7 @@ function auditScene(scene) {
39
53
  cameras += 1;
40
54
  if (props.current === true) currentCameras += 1;
41
55
  }
56
+ if (LIGHT_TYPES.has(type)) lights += 1;
42
57
  if (BODY_TYPES.has(type)) {
43
58
  const collider = props.collider;
44
59
  if (!collider || collider.shape === void 0) warnings.push(`${path}: ${type} has no collider — physics will skip it (add "collider": { "shape": ... }).`);
@@ -52,6 +67,7 @@ function auditScene(scene) {
52
67
  walk(root, root.name ?? "root", false, root.props?.static === true ? root.name ?? "root" : null);
53
68
  if (cameras > 0 && currentCameras === 0) warnings.push("no camera has \"current\": true — the screen renders from a default view, probably not what you framed.");
54
69
  if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
70
+ if (scene.dimension === "3d" && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" }.");
55
71
  return warnings;
56
72
  }
57
73
  //#endregion
@@ -1,5 +1,59 @@
1
1
  import { a as Rng, c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
2
2
 
3
+ //#region src/core/signal.d.ts
4
+ /**
5
+ * Minimal typed observer used for every engine event (Godot signal semantics).
6
+ *
7
+ * Emission is snapshot-based: listeners added during an emit do not fire in that
8
+ * emit, and listeners removed during an emit are skipped.
9
+ */
10
+ type SignalListener<Args extends unknown[]> = (...args: Args) => void;
11
+ declare class Signal<Args extends unknown[] = unknown[]> {
12
+ private connections;
13
+ /** Connect a listener. Returns a disposer. Connecting the same fn twice is a no-op. */
14
+ connect(fn: SignalListener<Args>, opts?: {
15
+ once?: boolean;
16
+ }): () => void;
17
+ disconnect(fn: SignalListener<Args>): void;
18
+ disconnectAll(): void;
19
+ emit(...args: Args): void;
20
+ get connectionCount(): number;
21
+ }
22
+ //#endregion
23
+ //#region src/core/log.d.ts
24
+ type LogLevel = "debug" | "info" | "warn" | "error";
25
+ interface LogEntry {
26
+ /** Monotonic per-manager sequence number (1-based). */
27
+ readonly seq: number;
28
+ /** Wall-clock-ish timestamp (performance.now when available) — diagnostics only. */
29
+ readonly timeMs: number;
30
+ readonly level: LogLevel;
31
+ /** The raw logged values, untouched — consumers format. */
32
+ readonly parts: readonly unknown[];
33
+ }
34
+ /**
35
+ * The engine's log channel (`engine.log`, `this.log` in a Behavior): a ring
36
+ * buffer plus a live `added` signal, so debug overlays and headless test
37
+ * harnesses can tail game logs without scraping the browser console.
38
+ */
39
+ declare class LogManager {
40
+ /** Fires once per entry, after it is buffered. */
41
+ readonly added: Signal<[LogEntry]>;
42
+ private readonly buffer;
43
+ private readonly capacity;
44
+ private seq;
45
+ constructor(capacity?: number);
46
+ debug(...parts: unknown[]): void;
47
+ info(...parts: unknown[]): void;
48
+ warn(...parts: unknown[]): void;
49
+ error(...parts: unknown[]): void;
50
+ /** Buffered entries, oldest first (capped at capacity). */
51
+ entries(): readonly LogEntry[];
52
+ /** Empty the buffer. The sequence keeps counting (entries stay unique). */
53
+ clear(): void;
54
+ private push;
55
+ }
56
+ //#endregion
3
57
  //#region src/core/scene-tree.d.ts
4
58
  /**
5
59
  * Owns a node tree and drives its lifecycle:
@@ -63,26 +117,6 @@ declare class SceneTree {
63
117
  private flushFreeQueue;
64
118
  }
65
119
  //#endregion
66
- //#region src/core/signal.d.ts
67
- /**
68
- * Minimal typed observer used for every engine event (Godot signal semantics).
69
- *
70
- * Emission is snapshot-based: listeners added during an emit do not fire in that
71
- * emit, and listeners removed during an emit are skipped.
72
- */
73
- type SignalListener<Args extends unknown[]> = (...args: Args) => void;
74
- declare class Signal<Args extends unknown[] = unknown[]> {
75
- private connections;
76
- /** Connect a listener. Returns a disposer. Connecting the same fn twice is a no-op. */
77
- connect(fn: SignalListener<Args>, opts?: {
78
- once?: boolean;
79
- }): () => void;
80
- disconnect(fn: SignalListener<Args>): void;
81
- disconnectAll(): void;
82
- emit(...args: Args): void;
83
- get connectionCount(): number;
84
- }
85
- //#endregion
86
120
  //#region src/core/node.d.ts
87
121
  /** Lifecycle hooks a SceneTree drives. Wired in scene-tree.ts. */
88
122
  interface NodeLifecycle {
@@ -186,6 +220,42 @@ declare class Node implements NodeLifecycle {
186
220
  _propagateUpdate(dt: number): void;
187
221
  /** @internal */
188
222
  _propagateFixedUpdate(dt: number): void;
223
+ /**
224
+ * Report an engine-level problem about THIS node.
225
+ *
226
+ * Goes to `engine.log` (what the debug overlay, `runScript()` and any agent
227
+ * tool read) as well as the console. A `console.warn` alone is invisible to
228
+ * every channel except a human with devtools open.
229
+ */
230
+ protected diagnose(level: "warn" | "error", ...parts: unknown[]): void;
231
+ /**
232
+ * This node threw and is being skipped. Its CHILDREN still run.
233
+ *
234
+ * The alternative is what used to happen: the exception escaped into the rAF
235
+ * callback, the loop was never rescheduled, and the game froze — permanently,
236
+ * silently, with `stats().running` still reporting true. In a vibe-coded
237
+ * project the least-tested code in the repo is a behavior's `update`, so this
238
+ * is not an edge case; it is Tuesday.
239
+ *
240
+ * Set it back to false to try the node again (the debug overlay's "resume").
241
+ */
242
+ errored: boolean;
243
+ /**
244
+ * This node's SCRIPT threw and is being skipped — the node itself still runs.
245
+ *
246
+ * A behavior is your code; the node is the engine's. A typo in the first must
247
+ * not switch off the second, or breaking a sword script stops the character
248
+ * from walking.
249
+ */
250
+ behaviorErrored: boolean;
251
+ /**
252
+ * Report ONCE and stop running this node's own update.
253
+ *
254
+ * Once, because a behavior that throws throws every frame: sixty identical
255
+ * stack traces a second buries the one line that matters and costs more than
256
+ * the game did.
257
+ */
258
+ private _quarantine;
189
259
  onEnterTree(): void;
190
260
  onReady(): void;
191
261
  onExitTree(): void;
@@ -646,40 +716,6 @@ declare class InputMap {
646
716
  private button;
647
717
  }
648
718
  //#endregion
649
- //#region src/core/log.d.ts
650
- type LogLevel = "debug" | "info" | "warn" | "error";
651
- interface LogEntry {
652
- /** Monotonic per-manager sequence number (1-based). */
653
- readonly seq: number;
654
- /** Wall-clock-ish timestamp (performance.now when available) — diagnostics only. */
655
- readonly timeMs: number;
656
- readonly level: LogLevel;
657
- /** The raw logged values, untouched — consumers format. */
658
- readonly parts: readonly unknown[];
659
- }
660
- /**
661
- * The engine's log channel (`engine.log`, `this.log` in a Behavior): a ring
662
- * buffer plus a live `added` signal, so debug overlays and headless test
663
- * harnesses can tail game logs without scraping the browser console.
664
- */
665
- declare class LogManager {
666
- /** Fires once per entry, after it is buffered. */
667
- readonly added: Signal<[LogEntry]>;
668
- private readonly buffer;
669
- private readonly capacity;
670
- private seq;
671
- constructor(capacity?: number);
672
- debug(...parts: unknown[]): void;
673
- info(...parts: unknown[]): void;
674
- warn(...parts: unknown[]): void;
675
- error(...parts: unknown[]): void;
676
- /** Buffered entries, oldest first (capped at capacity). */
677
- entries(): readonly LogEntry[];
678
- /** Empty the buffer. The sequence keeps counting (entries stay unique). */
679
- clear(): void;
680
- private push;
681
- }
682
- //#endregion
683
719
  //#region src/core/scene/scene.d.ts
684
720
  /**
685
721
  * A loaded, live scene: the node tree plus everything needed to round-trip
@@ -733,6 +769,13 @@ interface EngineStats {
733
769
  nodes: number;
734
770
  /** Whether the loop is scheduled (start() without stop()/dispose()). */
735
771
  running: boolean;
772
+ /**
773
+ * Errors swallowed to keep the game alive since start: nodes that quarantined
774
+ * themselves plus frames that threw. Non-zero means something IS broken and
775
+ * `engine.log` says what — a green-looking game with `errors: 7` is the case
776
+ * this counter exists for.
777
+ */
778
+ errors: number;
736
779
  }
737
780
  /** GPU-side counters from `renderer.stats()` — the LAST rendered frame. */
738
781
  interface RendererStats {
@@ -838,6 +881,25 @@ declare class Engine {
838
881
  /** Replace the active scene. The previous scene's root is freed. */
839
882
  setScene(scene: Scene): void;
840
883
  start(): void;
884
+ /**
885
+ * A frame threw somewhere outside a single node (a renderer, physics, a
886
+ * signal handler). Report the first one and count the rest: a broken frame
887
+ * repeats sixty times a second, and sixty identical traces a second is how
888
+ * you lose the first one.
889
+ */
890
+ private reportFrameError;
891
+ /** @internal A node quarantined itself; count it for `stats().errors`. */
892
+ _nodeErrored(_node: unknown): void;
893
+ private errorCount;
894
+ private lastFrameError;
895
+ /**
896
+ * Every node with something quarantined — its own update, its script, or
897
+ * both — so a tool can list them and put them back. Walks on demand; there is
898
+ * no per-frame bookkeeping for this.
899
+ */
900
+ erroredNodes(): Node[];
901
+ /** Un-skip every quarantined node — the "try again" after you fix the code. */
902
+ resumeErroredNodes(): number;
841
903
  stop(): void;
842
904
  /**
843
905
  * Live performance counters, queryable at ANY time. fps/frameMs come from a
@@ -973,4 +1035,4 @@ declare function registeredBehaviors(): string[];
973
1035
  /** Test isolation helper. */
974
1036
  declare function clearBehaviors(): void;
975
1037
  //#endregion
976
- export { Node as $, SfxPlayOptions as A, spatialPan as B, RendererStats as C, LogManager as D, LogLevel as E, ROLLOFF_MODELS as F, SynthOptions as G, SFX_PRESET_NAMES as H, RolloffModel as I, MusicManager as J, synthSfx as K, SpatialParams as L, Voice as M, VoicePreset as N, InputMap as O, Listener as P, BusName as Q, Vec3 as R, GameStats as S, LogEntry as T, SfxParams as U, SFX_PRESETS as V, SfxWave as W, PlayMusicOptions as X, MusicTrack as Y, AudioBuses as Z, registeredTypes as _, registerBehavior as a, Scheduler as b, PropDef as c, createNode as d, NodeLifecycle as et, getNodeSchema as f, registerNode as g, mergeStaticSignals as h, getBehavior as i, isAudioContextAvailable as j, SfxEngine as k, PropSchema as l, getNodeType as m, BehaviorCtor as n, SignalListener as nt, registeredBehaviors as o, getNodeSignals as p, MusicBackend as q, clearBehaviors as r, SceneTree as rt, NodeCtor as s, Behavior as t, Signal as tt, clearRegistry as u, Engine as v, Scene as w, EngineStats as x, EngineOptions as y, spatialGain as z };
1038
+ export { LogEntry as $, VoicePreset as A, SfxParams as B, RendererStats as C, SfxPlayOptions as D, SfxEngine as E, Vec3 as F, MusicManager as G, SynthOptions as H, spatialGain as I, AudioBuses as J, MusicTrack as K, spatialPan as L, ROLLOFF_MODELS as M, RolloffModel as N, isAudioContextAvailable as O, SpatialParams as P, SceneTree as Q, SFX_PRESETS as R, GameStats as S, InputMap as T, synthSfx as U, SfxWave as V, MusicBackend as W, Node as X, BusName as Y, NodeLifecycle as Z, registeredTypes as _, registerBehavior as a, Scheduler as b, PropDef as c, createNode as d, LogLevel as et, getNodeSchema as f, registerNode as g, mergeStaticSignals as h, getBehavior as i, Listener as j, Voice as k, PropSchema as l, getNodeType as m, BehaviorCtor as n, Signal as nt, registeredBehaviors as o, getNodeSignals as p, PlayMusicOptions as q, clearBehaviors as r, SignalListener as rt, NodeCtor as s, Behavior as t, LogManager as tt, clearRegistry as u, Engine as v, Scene as w, EngineStats as x, EngineOptions as y, SFX_PRESET_NAMES as z };
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { _ as registerBehavior, n as loadScene } from "./loader-BZqOKfI2.js";
3
- import { l as AudioPlayer, u as Engine } from "./register-BTg0EM7s.js";
2
+ import { m as diagnose, n as loadScene, y as registerBehavior } from "./loader-BlaRQGaA.js";
3
+ import { l as AudioPlayer, u as Engine } from "./register-Dzkd6-os.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
6
6
  import { a as poseFromRenderer, i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
7
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BftxM_It.js";
8
8
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
9
- import { T as Camera3D, _ as DirectionalLight3D, h as ModelInstance3D, t as registerNodes3D } from "./register-DJ0SByQg.js";
10
- import { n as enablePhysics3D } from "./physics-3d-CeRH-Ff_.js";
9
+ import { E as Camera3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets--DigHNg4.js";
10
+ import { n as enablePhysics3D } from "./physics-3d-CnPygVGo.js";
11
11
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
12
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -25,15 +25,47 @@ import { Sky } from "three/examples/jsm/objects/Sky.js";
25
25
  * @pixiv/three-vrm (VRM 0.x facing fixed via rotateVRM0).
26
26
  */
27
27
  var AssetStore3D = class {
28
+ engine;
28
29
  models = /* @__PURE__ */ new Map();
29
30
  animations = /* @__PURE__ */ new Map();
30
31
  modelKeys = /* @__PURE__ */ new Map();
31
32
  animationKeys = /* @__PURE__ */ new Map();
32
33
  loader;
33
- constructor() {
34
+ /**
35
+ * `engine` is optional only so a bare store still constructs in tests. Give it
36
+ * one and every load failure lands in `engine.log` — where the debug overlay,
37
+ * `runScript()` and any agent tool can see it. Without it a 404'd GLB was a
38
+ * console line and an invisible model.
39
+ */
40
+ constructor(engine = null) {
41
+ this.engine = engine;
34
42
  this.loader = new GLTFLoader();
35
43
  this.loader.register((parser) => new VRMLoaderPlugin(parser));
36
44
  }
45
+ /**
46
+ * Every asset that failed, with the reason — the answer to "why is my model
47
+ * not there". Empty is the healthy case.
48
+ */
49
+ errors() {
50
+ const out = [];
51
+ for (const [ref, url] of this.modelKeys) {
52
+ const entry = this.models.get(url);
53
+ if (entry?.error) out.push({
54
+ ref,
55
+ url,
56
+ error: String(entry.error)
57
+ });
58
+ }
59
+ for (const [ref, decl] of this.animationKeys) {
60
+ const entry = this.animations.get(decl.url);
61
+ if (entry?.error) out.push({
62
+ ref,
63
+ url: decl.url,
64
+ error: String(entry.error)
65
+ });
66
+ }
67
+ return out;
68
+ }
37
69
  /** Map a scene's `assets` header. */
38
70
  load(assets) {
39
71
  for (const [key, def] of Object.entries(assets)) {
@@ -83,7 +115,7 @@ var AssetStore3D = class {
83
115
  }, void 0, (error) => {
84
116
  entry.status = "error";
85
117
  entry.error = error instanceof Error ? error.message : String(error);
86
- console.error(`incanto: failed to load model '${url}':`, error);
118
+ diagnose(this.engine, "error", `incanto: failed to load model '${url}':`, error);
87
119
  });
88
120
  return entry;
89
121
  }
@@ -103,11 +135,11 @@ var AssetStore3D = class {
103
135
  entry.clips = gltf.animations ?? [];
104
136
  entry.scene = gltf.scene;
105
137
  entry.status = "ready";
106
- if (entry.clips.length === 0) console.warn(`incanto: animation asset '${def.url}' contains no clips`);
138
+ if (entry.clips.length === 0) diagnose(this.engine, "warn", `incanto: animation asset '${def.url}' contains no clips`);
107
139
  }, void 0, (error) => {
108
140
  entry.status = "error";
109
141
  entry.error = error instanceof Error ? error.message : String(error);
110
- console.error(`incanto: failed to load animation '${def.url}':`, error);
142
+ diagnose(this.engine, "error", `incanto: failed to load animation '${def.url}':`, error);
111
143
  });
112
144
  return entry;
113
145
  }
@@ -884,37 +916,6 @@ function lerp(a, b, t) {
884
916
  return a + (b - a) * t;
885
917
  }
886
918
  //#endregion
887
- //#region src/3d/environment-presets.ts
888
- /**
889
- * drei-compatible environment presets: each name maps to a 1k HDRI (the exact
890
- * Poly Haven CC0 files @react-three/drei ships), so `preset="sunset"` lights the
891
- * same here. Served from the agent8 CDN (the only sanctioned external host, same
892
- * as the terrain splat) — set `environment.hdri` to override with your own URL.
893
- */
894
- const CDN = "https://agent8-games.verse8.io/assets/3D/default/textures/hdri";
895
- const ENVIRONMENT_PRESETS = {
896
- apartment: "lebombo_1k.hdr",
897
- city: "potsdamer_platz_1k.hdr",
898
- dawn: "kiara_1_dawn_1k.hdr",
899
- forest: "forest_slope_1k.hdr",
900
- lobby: "st_fagans_interior_1k.hdr",
901
- night: "dikhololo_night_1k.hdr",
902
- park: "rooitou_park_1k.hdr",
903
- studio: "studio_small_03_1k.hdr",
904
- sunset: "venice_sunset_1k.hdr",
905
- warehouse: "empty_warehouse_01_1k.hdr"
906
- };
907
- /** Resolve `environment.preset` / `environment.hdri` to the HDR url (or null). */
908
- function resolveEnvironmentHdri(env) {
909
- if (typeof env.hdri === "string" && env.hdri !== "") return env.hdri;
910
- if (typeof env.preset === "string") {
911
- const file = ENVIRONMENT_PRESETS[env.preset];
912
- if (!file) throw new Error(`Unknown environment preset '${env.preset}'. Available: ${Object.keys(ENVIRONMENT_PRESETS).join(", ")}.`);
913
- return `${CDN}/${file}`;
914
- }
915
- return null;
916
- }
917
- //#endregion
918
919
  //#region src/3d/environment-3d.ts
919
920
  /** Shadow ortho half-extent when the env header forces a light to cast and the
920
921
  * light node configured nothing itself. One static box around the origin — no
@@ -1499,6 +1500,11 @@ var Renderer3D = class {
1499
1500
  engine;
1500
1501
  disconnect;
1501
1502
  canvas;
1503
+ /**
1504
+ * The model/animation store. Public so a game (and an agent's tool) can ask
1505
+ * `renderer.assets.errors()` why a model is not there — it used to be private,
1506
+ * so a 404'd GLB was unanswerable from outside.
1507
+ */
1502
1508
  assets;
1503
1509
  ownsAssets;
1504
1510
  loadedAssetScenes = /* @__PURE__ */ new WeakSet();
@@ -1545,7 +1551,7 @@ var Renderer3D = class {
1545
1551
  this.canvas = opts.canvas;
1546
1552
  this.engine = opts.engine;
1547
1553
  this.ownsAssets = !opts.assets;
1548
- this.assets = opts.assets ?? new AssetStore3D();
1554
+ this.assets = opts.assets ?? new AssetStore3D(this.engine);
1549
1555
  const rendering = resolveRendering(opts.engine.scene?.environment, {
1550
1556
  antialias: true,
1551
1557
  pixelRatio: Math.min(globalThis.devicePixelRatio ?? 1, 2)
@@ -2265,6 +2271,9 @@ async function createGame3D(opts) {
2265
2271
  sourceJson,
2266
2272
  renderer,
2267
2273
  physics,
2274
+ assetErrors() {
2275
+ return renderer.assets?.errors() ?? [];
2276
+ },
2268
2277
  stats() {
2269
2278
  return {
2270
2279
  triangles: 0,
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { _ as registerBehavior, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-BZqOKfI2.js";
3
- import { l as AudioPlayer, u as Engine } from "./register-BTg0EM7s.js";
2
+ import { m as diagnose, n as loadScene, o as computeViewport, s as resolveViewport, y as registerBehavior } from "./loader-BlaRQGaA.js";
3
+ import { l as AudioPlayer, u as Engine } from "./register-Dzkd6-os.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
6
6
  import { i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DWcWq4QG.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-BftxM_It.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-tkR_8tWg.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-3kOQCtgd.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-D9wquBvK.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -17,9 +17,16 @@ import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSe
17
17
  * `"$key"`. The texture loader is injectable so tests run headless.
18
18
  */
19
19
  var AssetStore2D = class {
20
+ engine;
20
21
  entries = /* @__PURE__ */ new Map();
21
22
  loader;
22
- constructor(loader) {
23
+ /**
24
+ * `engine` is optional so a bare store still constructs in tests. Given one,
25
+ * a texture that fails to load lands in `engine.log` — where the overlay and
26
+ * `runScript()` can see it — instead of only in the browser console.
27
+ */
28
+ constructor(loader, engine = null) {
29
+ this.engine = engine;
23
30
  this.loader = loader ?? ((url, callbacks) => new TextureLoader().load(url, () => callbacks?.onLoad?.(), void 0, (err) => callbacks?.onError?.(err)));
24
31
  }
25
32
  /** Load (or extend with) a scene's `assets` declarations. Hard-validates each entry. */
@@ -43,7 +50,7 @@ var AssetStore2D = class {
43
50
  },
44
51
  onError: (err) => {
45
52
  state.status = "error";
46
- console.error(`[incanto] failed to load texture '${key}' (${decl.url}):`, err);
53
+ diagnose(this.engine, "error", `[incanto] failed to load texture '${key}' (${decl.url}):`, err);
47
54
  }
48
55
  });
49
56
  if (previous && previous.texture !== texture) previous.texture.dispose();
@@ -251,6 +258,7 @@ var Renderer2D = class {
251
258
  worldCam = new OrthographicCamera(-1, 1, 1, -1, -1e3, 1e3);
252
259
  uiCam = new OrthographicCamera(-1, 1, 1, -1, -1e3, 1e3);
253
260
  engine;
261
+ /** The texture store. Public so `renderer.assets.status(key)` is reachable. */
254
262
  assets;
255
263
  loadedAssetScenes = /* @__PURE__ */ new WeakSet();
256
264
  disconnect;
@@ -258,7 +266,7 @@ var Renderer2D = class {
258
266
  constructor(opts) {
259
267
  this.canvas = opts.canvas;
260
268
  this.engine = opts.engine;
261
- this.assets = opts.assets ?? new AssetStore2D();
269
+ this.assets = opts.assets ?? new AssetStore2D(void 0, this.engine);
262
270
  const rendering = resolveRendering(opts.engine.scene?.environment, {
263
271
  antialias: true,
264
272
  pixelRatio: 1
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as RendererStats, E as LogLevel, v as Engine } from "./behavior-BAc0erXF.js";
1
+ import { C as RendererStats, et as LogLevel, v as Engine } from "./behavior-D_jMpFh8.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */