incanto 0.52.0 → 0.54.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 (67) hide show
  1. package/bin/incanto-check.mjs +56 -1
  2. package/dist/2d.d.ts +95 -4
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +111 -6
  5. package/dist/3d.js +10 -6
  6. package/dist/{behavior-uPEuZrUB.d.ts → behavior-CyQoSu4n.d.ts} +212 -10
  7. package/dist/{create-game-B_e9hJf7.js → create-game-BRt6XKmP.js} +31 -12
  8. package/dist/{create-game-CGnoypjL.js → create-game-Czzp6ZuE.js} +26 -14
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/debug.js +1 -1
  11. package/dist/{duplicate-B-OtSRFL.js → duplicate-MNLMAcbz.js} +1 -1
  12. package/dist/editor.js +14 -0
  13. package/dist/{environment-presets-DSZwsKPs.js → environment-presets-BQ_QsIBY.js} +222 -11
  14. package/dist/{gameplay-B6jqvYeM.js → gameplay-CZ2yq37J.js} +32 -23
  15. package/dist/gameplay.d.ts +8 -3
  16. package/dist/gameplay.js +1 -1
  17. package/dist/index.d.ts +178 -6
  18. package/dist/index.js +9 -9
  19. package/dist/{loader-B-Gft32x.js → loader-BTkHYrQn.js} +54 -18
  20. package/dist/{loader-B9iTqs27.d.ts → loader-DhI1jFW_.d.ts} +1 -1
  21. package/dist/{log-report-lxrQY9cH.js → log-report-CPFm4OXf.js} +0 -0
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +1 -1
  24. package/dist/{particle-sim-BzJ1yxoE.d.ts → particle-sim-C5OfBbmU.d.ts} +11 -0
  25. package/dist/{pathfinding-CAR9DjQQ.d.ts → pathfinding-CXGCpRQe.d.ts} +24 -1
  26. package/dist/{physics-2d-Dns-oZlE.js → physics-2d-CfWAggJ1.js} +2 -2
  27. package/dist/{physics-3d-BhI0ehpe.js → physics-3d-Bes-GIuR.js} +3 -3
  28. package/dist/react.d.ts +1 -1
  29. package/dist/react.js +1 -1
  30. package/dist/{register-Trx7WHnD.js → register-en63AEZO.js} +163 -4
  31. package/dist/{register-DVwlnZAZ.js → register-p48lHE2o.js} +433 -36
  32. package/dist/{replay-BU1CCM15.d.ts → replay-ePMz26jw.d.ts} +1 -1
  33. package/dist/{replay-BicPOMX0.js → replay-t1pP0gQg.js} +2 -2
  34. package/dist/{split-screen-paxkQs_q.d.ts → split-screen-BsdOHbzP.d.ts} +1 -1
  35. package/dist/{split-screen-CZ9ccBBQ.js → split-screen-CSb_uZ6W.js} +2 -2
  36. package/dist/{sprite-animation-D_p28jwU.js → sprite-animation-7qvUxF6Z.js} +19 -0
  37. package/dist/{src-5gbZO47I.js → src-vNPQeQ1W.js} +1 -1
  38. package/dist/{teardown-ks3d5W9n.js → teardown-C7uVSJvx.js} +30 -1
  39. package/dist/{test-DAojuFdb.js → test-Tnf1nQl3.js} +25 -13
  40. package/dist/test.d.ts +4 -4
  41. package/dist/test.js +2 -2
  42. package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
  43. package/dist/vite.js +2 -2
  44. package/editor/assets/{agent8-CCvckvbw.js → agent8-BWaW-D85.js} +1 -1
  45. package/editor/assets/{debug-CLNCOnbc.js → debug-DLF8rUtr.js} +2 -2
  46. package/editor/assets/{index-Cb5Brupb.js → index-BYfiwbQx.js} +91 -91
  47. package/editor/index.html +1 -1
  48. package/package.json +1 -1
  49. package/schemas/scene.schema.json +221 -0
  50. package/skills/incanto-assets.md +45 -2
  51. package/skills/incanto-audio.md +97 -10
  52. package/skills/incanto-building-2d-games.md +55 -3
  53. package/skills/incanto-building-3d-games.md +3 -1
  54. package/skills/incanto-environment.md +3 -1
  55. package/skills/incanto-gameplay-behaviors.md +51 -4
  56. package/skills/incanto-hud.md +7 -0
  57. package/skills/incanto-node-reference.md +36 -0
  58. package/skills/incanto-performance.md +47 -0
  59. package/skills/incanto-physics-and-input.md +38 -0
  60. package/skills/incanto-verifying-your-game.md +50 -1
  61. package/skills/incanto-web-integration.md +1 -0
  62. package/templates-app/beacon-isle-3d/index.html +3 -3
  63. package/templates-app/beacon-isle-3d/package.json +1 -1
  64. package/templates-app/tps-3d/index.html +3 -3
  65. package/templates-app/tps-3d/package.json +1 -1
  66. package/templates-app/village-quest-3d/index.html +3 -3
  67. package/templates-app/village-quest-3d/package.json +1 -1
@@ -58,6 +58,61 @@ function collectScenes(entry) {
58
58
  return out;
59
59
  }
60
60
 
61
+ /**
62
+ * Local art the project does not contain.
63
+ *
64
+ * A scene asset pointing at a file nobody copied loads fine, validates fine and
65
+ * draws NOTHING — the failure exists only in a browser, in `assetErrors()`,
66
+ * which needs a running game and someone to look at it. This command has the
67
+ * scene's path and runs after every edit, and the file is either there or it is
68
+ * not. A WARNING, not an error: a url can legitimately be produced at runtime,
69
+ * and refusing to load a scene over a guess would be worse than the bug.
70
+ */
71
+ function missingArt(file, json) {
72
+ const assets = json && typeof json === 'object' ? json.assets : null;
73
+ if (!assets || typeof assets !== 'object') return [];
74
+ const sceneDir = dirname(file);
75
+ const root = projectRoot(sceneDir);
76
+ const out = [];
77
+ for (const [key, decl] of Object.entries(assets)) {
78
+ const url = decl && typeof decl === 'object' ? decl.url : null;
79
+ if (typeof url !== 'string' || url === '') continue;
80
+ // Remote and inline are not this command's business to guess about.
81
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//')) continue;
82
+ const rel = url.replace(/^\//, '');
83
+ const candidates = [
84
+ join(root, 'public', rel),
85
+ join(root, rel),
86
+ join(sceneDir, rel),
87
+ join(sceneDir, '..', rel),
88
+ ];
89
+ if (candidates.some((p) => existsSync(p))) continue;
90
+ out.push(
91
+ `$${key} → ${url} is not in the project ` +
92
+ `(looked in ${relativeish(root, join(root, 'public'))}, ${relativeish(root, root)}` +
93
+ `, and beside the scene). It will draw nothing.`,
94
+ );
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** Nearest ancestor with a package.json, else the scene's own directory. */
100
+ function projectRoot(from) {
101
+ let dir = resolve(from);
102
+ for (let i = 0; i < 8; i++) {
103
+ if (existsSync(join(dir, 'package.json'))) return dir;
104
+ const up = dirname(dir);
105
+ if (up === dir) break;
106
+ dir = up;
107
+ }
108
+ return resolve(from);
109
+ }
110
+
111
+ function relativeish(root, p) {
112
+ const r = p.slice(root.length).replace(/^[\\/]/, '');
113
+ return r === '' ? './' : `${r}/`;
114
+ }
115
+
61
116
  const roots = args.files.length > 0 ? args.files : ['.'];
62
117
  for (const r of roots) {
63
118
  if (!existsSync(resolve(r))) {
@@ -84,7 +139,7 @@ const results = files.map((file) => {
84
139
  strictBehaviors: args.strict,
85
140
  resolveScene: (p) => JSON.parse(readFileSync(resolve(dirname(file), p), 'utf-8')),
86
141
  });
87
- if (res.ok) return { file, ok: true, warnings: auditScene(json) };
142
+ if (res.ok) return { file, ok: true, warnings: [...auditScene(json), ...missingArt(file, json)] };
88
143
  return {
89
144
  file,
90
145
  ok: false,
package/dist/2d.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { Dt as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-uPEuZrUB.js";
1
+ import { At as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-CyQoSu4n.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-B9iTqs27.js";
4
+ import { t as LoadSceneOptions } from "./loader-DhI1jFW_.js";
5
5
  import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
6
- import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
+ import { r as ParticleView, t as ParticleSim } from "./particle-sim-C5OfBbmU.js";
7
7
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
8
8
  import * as RapierNs from "@dimforge/rapier2d-compat";
9
9
 
@@ -304,7 +304,21 @@ interface CreateGame2DOptions {
304
304
  * false disables. They overlay `touchContainer` (default: the canvas's
305
305
  * parent element — give it position: relative).
306
306
  */
307
+ /**
308
+ * Attach pointer input (mouse, finger or pen) to the canvas: positions for
309
+ * `input.pointerPosition()`, buttons for `input.mousePressed()`.
310
+ * `{ lockOnClick: true }` captures the cursor — rare in 2D, so it is off.
311
+ */
312
+ pointer?: boolean | {
313
+ lockOnClick?: boolean;
314
+ };
307
315
  touch?: "auto" | boolean;
316
+ /**
317
+ * Leave the browser's touch gestures (scroll/zoom/pull-to-refresh) on the
318
+ * canvas. Default false: the GAME owns them, or a drag on a phone scrolls
319
+ * the page instead of playing.
320
+ */
321
+ pageGestures?: boolean;
308
322
  touchContainer?: HTMLElement;
309
323
  /** @internal Test seam — replaces `document` for the touch overlay. */
310
324
  _touchDoc?: {
@@ -344,10 +358,32 @@ interface CreateGame2DOptions {
344
358
  _scheduler?: Scheduler;
345
359
  }
346
360
  /** What createGame needs from a renderer (stats is optional for test stubs). */
361
+ /** What a renderer must do for the engine. The test seam supplies this much. */
347
362
  interface GameRenderer {
348
363
  dispose(): void;
349
364
  stats?(): RendererStats;
350
365
  }
366
+ /**
367
+ * What a GAME can ask its renderer for.
368
+ *
369
+ * The two coordinate conversions live here rather than only on `Renderer2D`,
370
+ * because `game.renderer` is what a consumer holds: a tap needs screen→world
371
+ * and a pinned DOM element needs world→screen, both are documented in the
372
+ * skills, and neither typechecked through the narrow interface. Found by
373
+ * following my own documentation in a fresh project.
374
+ */
375
+ interface Game2DRenderer extends GameRenderer {
376
+ /** Canvas px → world px, through the design viewport and the camera. */
377
+ worldFromScreen(sx: number, sy: number): {
378
+ x: number;
379
+ y: number;
380
+ };
381
+ /** World px → canvas px (pin DOM to a node). */
382
+ screenFromWorld(wx: number, wy: number): {
383
+ x: number;
384
+ y: number;
385
+ };
386
+ }
351
387
  interface Game2D {
352
388
  engine: Engine;
353
389
  scene: Scene$1;
@@ -356,13 +392,25 @@ interface Game2D {
356
392
  * A running tree has moved; the editor gets what was written down.
357
393
  */
358
394
  sourceJson: SceneJson;
359
- renderer: GameRenderer;
395
+ renderer: Game2DRenderer;
360
396
  physics: Physics2D | null;
361
397
  /**
362
398
  * The node under a screen pixel, or null. The raycast the renderer already
363
399
  * does — reachable now without holding the renderer.
364
400
  */
365
401
  pick(x: number, y: number): Node | null;
402
+ /**
403
+ * Assets that did not arrive — a texture that 404'd, a sound that cannot be
404
+ * played — with the ref and the reason.
405
+ *
406
+ * The verify loop names this as one of its four signals, and 2D never had it:
407
+ * a 2D game could not ask the question at all.
408
+ */
409
+ assetErrors(): Array<{
410
+ ref: string;
411
+ url: string;
412
+ error: string;
413
+ }>;
366
414
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
367
415
  stats(): GameStats;
368
416
  /**
@@ -469,6 +517,16 @@ declare class AnimatedSprite2D extends Sprite2D {
469
517
  stop(): void;
470
518
  override onReady(): void;
471
519
  override update(dt: number): void;
520
+ /** Sheets already reported, so a bad grid says so ONCE, not every frame. */
521
+ private gridChecked;
522
+ /**
523
+ * Tell the author their grid does not fit, the first time it is drawn.
524
+ *
525
+ * Both halves of this were silent: a frame size that does not divide the
526
+ * sheet slices every row further off centre, and an animation naming a frame
527
+ * past the end of the grid draws whatever is at the wrong end of the image.
528
+ */
529
+ private checkGrid;
472
530
  protected override resolveTexture(assets: AssetStore2D | null): ResolvedSpriteTexture | null;
473
531
  }
474
532
  //#endregion
@@ -651,6 +709,18 @@ declare class Particles2D extends Node2D {
651
709
  static override readonly props: PropSchema;
652
710
  preset: string;
653
711
  emitting: boolean;
712
+ /**
713
+ * Where a particle LIVES once it is born.
714
+ *
715
+ * `false` (default) — the emitter's local space: the plume moves with the
716
+ * node, which is what a torch on a moving platform wants.
717
+ *
718
+ * `true` — the world: a particle keeps the place it was born, so dust is left
719
+ * BEHIND a running player instead of glued to it, and moving a one-shot to
720
+ * the next explosion (the documented `p.position = …; p.replay()` recipe)
721
+ * stops dragging the previous one across the level.
722
+ */
723
+ worldSpace: boolean;
654
724
  rate: number;
655
725
  burst: number;
656
726
  lifetime: number[];
@@ -680,9 +750,30 @@ declare class Particles2D extends Node2D {
680
750
  private finishedEmitted;
681
751
  /** @internal Lazily applies the preset + builds the deterministic sim. */
682
752
  _ensureSim(): ParticleSim;
753
+ /** Particles alive this frame — an emitter that is running, told from one
754
+ * that is not. `framing()` says where it IS; this says whether it is going. */
755
+ get aliveCount(): number;
756
+ /** A snapshot of the live particles — positions are in whichever space
757
+ * `worldSpace` selects, relative to the emitter. */
758
+ particles(): ParticleView[];
759
+ /** The emitter's world position last frame, for world-space counter-motion. */
760
+ private lastWorld;
761
+ /** Last frame's `emitting`, so turning one ON is a reportable event. */
762
+ private wasEmitting;
683
763
  override update(dt: number): void;
764
+ /**
765
+ * Undo the emitter's own motion on everything already alive.
766
+ *
767
+ * Ancestor-summed like the physics helpers, so it follows a moving PARENT
768
+ * (dust under a player) as well as the node's own position.
769
+ */
770
+ private holdWorldSpace;
771
+ /** Tell the engine an effect fired — the visual half of the audio record. */
772
+ private report;
684
773
  /** Restart a one-shot (re-burst + allow finished to fire again). */
685
774
  replay(): void;
775
+ /** `replay()` asked for this burst, so `emitting: false` does not hold it. */
776
+ private armedByReplay;
686
777
  private mesh;
687
778
  /** Parsed `paletteColors`, reused; rebuilt only when the hex list changes. */
688
779
  private paletteCache;
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-B_e9hJf7.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-Trx7WHnD.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-Dns-oZlE.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-BRt6XKmP.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-en63AEZO.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CfWAggJ1.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
package/dist/3d.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { D as QualityCaps, Dt as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-uPEuZrUB.js";
1
+ import { At as Node, D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-CyQoSu4n.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson$1, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-B9iTqs27.js";
5
- import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
4
+ import { t as LoadSceneOptions } from "./loader-DhI1jFW_.js";
5
+ import { r as ParticleView, t as ParticleSim } from "./particle-sim-C5OfBbmU.js";
6
6
  import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-DZ70IY26.js";
7
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-CAR9DjQQ.js";
7
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-CXGCpRQe.js";
8
8
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
9
9
  import { VRM } from "@pixiv/three-vrm";
10
10
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -61,6 +61,15 @@ declare class TextureCache3D {
61
61
  */
62
62
  acquireOwn(url: string, spec?: TextureSpec): Texture | null;
63
63
  /**
64
+ * File a failure this store did not load itself.
65
+ *
66
+ * `Tree3D` keeps its own cache on purpose (it decodes off-thread and its
67
+ * page-session lifetime is load-bearing for the editor), which left its bark
68
+ * and leaf textures outside the one place `assetErrors()` reads. A 404'd leaf
69
+ * texture is a forest of bare branches and no message anywhere.
70
+ */
71
+ noteError(url: string, error: unknown): void;
72
+ /**
64
73
  * What 404'd, by URL — reported raw, never by cache key, because an agent
65
74
  * greps this against the URL it wrote.
66
75
  */
@@ -563,6 +572,12 @@ interface CreateGame3DOptions {
563
572
  * parent element — give it position: relative).
564
573
  */
565
574
  touch?: "auto" | boolean;
575
+ /**
576
+ * Leave the browser's touch gestures (scroll/zoom/pull-to-refresh) on the
577
+ * canvas. Default false: the GAME owns them, or a drag on a phone scrolls
578
+ * the page instead of playing.
579
+ */
580
+ pageGestures?: boolean;
566
581
  touchContainer?: HTMLElement;
567
582
  /** @internal Test seam — replaces `document` for the touch overlay. */
568
583
  _touchDoc?: {
@@ -1853,6 +1868,15 @@ declare class LoftMesh3D extends Node3D {
1853
1868
  private geometryKey;
1854
1869
  protected override _createObject3D(): Object3D;
1855
1870
  override _syncObject3D(): void;
1871
+ /**
1872
+ * Hand the GPU back.
1873
+ *
1874
+ * Every other node that builds geometry disposes it here; these four did not,
1875
+ * so a scene swap left their buffers and materials resident and a game with
1876
+ * level transitions climbed until the tab died — with `stats().geometries` the
1877
+ * only witness, and only if someone was watching it.
1878
+ */
1879
+ override free(): void;
1856
1880
  }
1857
1881
  //#endregion
1858
1882
  //#region src/3d/nodes/model-instance-3d.d.ts
@@ -2024,6 +2048,18 @@ declare class Particles3D extends Node3D {
2024
2048
  static override readonly props: PropSchema;
2025
2049
  preset: string;
2026
2050
  emitting: boolean;
2051
+ /**
2052
+ * Where a particle LIVES once it is born.
2053
+ *
2054
+ * `false` (default) — the emitter's local space: the plume moves with the
2055
+ * node, which is what a torch on a moving platform wants.
2056
+ *
2057
+ * `true` — the world: a particle keeps the place it was born, so dust is left
2058
+ * BEHIND a running player instead of glued to it, and moving a one-shot to
2059
+ * the next explosion (the documented `p.position = …; p.replay()` recipe)
2060
+ * stops dragging the previous one across the level.
2061
+ */
2062
+ worldSpace: boolean;
2027
2063
  rate: number;
2028
2064
  burst: number;
2029
2065
  lifetime: number[];
@@ -2059,9 +2095,30 @@ declare class Particles3D extends Node3D {
2059
2095
  private finishedEmitted;
2060
2096
  /** @internal */
2061
2097
  _ensureSim(): ParticleSim;
2098
+ /** Particles alive this frame — an emitter that is running, told from one
2099
+ * that is not. `framing()` says where it IS; this says whether it is going. */
2100
+ get aliveCount(): number;
2101
+ /** A snapshot of the live particles — positions are in whichever space
2102
+ * `worldSpace` selects, relative to the emitter. */
2103
+ particles(): ParticleView[];
2104
+ /** The emitter's world position last frame, for world-space counter-motion. */
2105
+ private lastWorld;
2106
+ /** Last frame's `emitting`, so turning one ON is a reportable event. */
2107
+ private wasEmitting;
2062
2108
  override update(dt: number): void;
2109
+ /**
2110
+ * Undo the emitter's own motion on everything already alive.
2111
+ *
2112
+ * Ancestor-summed like the physics helpers, so it follows a moving PARENT
2113
+ * (dust under a player) as well as the node's own position.
2114
+ */
2115
+ private holdWorldSpace;
2116
+ /** Tell the engine an effect fired — the visual half of the audio record. */
2117
+ private report;
2063
2118
  /** Restart a one-shot (re-burst + allow finished to fire again). */
2064
2119
  replay(): void;
2120
+ /** `replay()` asked for this burst, so `emitting: false` does not hold it. */
2121
+ private armedByReplay;
2065
2122
  private points;
2066
2123
  private positions;
2067
2124
  private colors;
@@ -2074,6 +2131,15 @@ declare class Particles3D extends Node3D {
2074
2131
  * uses the colorStart→colorEnd ramp. */
2075
2132
  private refreshPalette;
2076
2133
  private syncParticles;
2134
+ /**
2135
+ * Hand the GPU back.
2136
+ *
2137
+ * Every other node that builds geometry disposes it here; these four did not,
2138
+ * so a scene swap left their buffers and materials resident and a game with
2139
+ * level transitions climbed until the tab died — with `stats().geometries` the
2140
+ * only witness, and only if someone was watching it.
2141
+ */
2142
+ override free(): void;
2077
2143
  }
2078
2144
  //#endregion
2079
2145
  //#region src/3d/terrain/heightmap.d.ts
@@ -2750,10 +2816,30 @@ declare class Trail3D extends Node3D {
2750
2816
  private mesh;
2751
2817
  private lastColor;
2752
2818
  protected override _createObject3D(): Object3D;
2819
+ /** Last frame's `emitting`, so laying ribbon is a reportable event. */
2820
+ private wasEmitting;
2753
2821
  override update(dt: number): void;
2754
2822
  _onRender3D(ctx: RenderContext3D): void;
2755
- /** Test hook: recorded point count. */
2823
+ /**
2824
+ * How many points the ribbon is made of.
2825
+ *
2826
+ * ZERO with no renderer, and that is not a bug: the ribbon is laid in the
2827
+ * render sync, because each point needs the node's real world transform (a
2828
+ * sword arc is a ROTATING parent, which an ancestor position sum cannot see).
2829
+ * Assert it from a browser check; read a headless 0 as "nothing rendered"
2830
+ * rather than "my trail is broken", and use the `trail` effect record for the
2831
+ * wiring question.
2832
+ */
2756
2833
  get pointCount(): number;
2834
+ /**
2835
+ * Hand the GPU back.
2836
+ *
2837
+ * Every other node that builds geometry disposes it here; these four did not,
2838
+ * so a scene swap left their buffers and materials resident and a game with
2839
+ * level transitions climbed until the tab died — with `stats().geometries` the
2840
+ * only witness, and only if someone was watching it.
2841
+ */
2842
+ override free(): void;
2757
2843
  }
2758
2844
  //#endregion
2759
2845
  //#region src/3d/vegetation/tree-blueprint.d.ts
@@ -2853,7 +2939,17 @@ declare class Tree3D extends Node3D {
2853
2939
  /** @internal Every grown variant (medium/high tiers; empty for 'simple'). */
2854
2940
  _variantMeshes(): TreeVariant[];
2855
2941
  override update(dt: number): void;
2856
- override _syncObject3D(): void;
2942
+ /** The scene's asset store, so a 404'd bark or leaf reaches assetErrors(). */
2943
+ private assets;
2944
+ override _syncObject3D(alpha?: number, assets?: AssetStore3D | null): void;
2945
+ /**
2946
+ * File a texture failure where the ladder reads it.
2947
+ *
2948
+ * This node keeps its own cache on purpose, and that left its textures
2949
+ * outside the one place `assetErrors()` looks — so a leaf URL that 404s was a
2950
+ * forest of bare branches with no message anywhere.
2951
+ */
2952
+ private noteTextureError;
2857
2953
  /** Re-grow only when a grove-shaping prop changed (the JSON IS the grove). */
2858
2954
  private rebuildIfNeeded;
2859
2955
  /** Resolve the drape target + cache the grove's world position for `bedY`. */
@@ -2924,6 +3020,15 @@ declare class VoxelGrid3D extends Node3D {
2924
3020
  blocks(): VoxelBlock[];
2925
3021
  protected override _createObject3D(): Object3D;
2926
3022
  override update(): void;
3023
+ /**
3024
+ * Hand the GPU back.
3025
+ *
3026
+ * Every other node that builds geometry disposes it here; these four did not,
3027
+ * so a scene swap left their buffers and materials resident and a game with
3028
+ * level transitions climbed until the tab died — with `stats().geometries` the
3029
+ * only witness, and only if someone was watching it.
3030
+ */
3031
+ override free(): void;
2927
3032
  }
2928
3033
  //#endregion
2929
3034
  //#region src/3d/water/interaction.d.ts
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
- import { F as CharacterBody3D, H as WATER_MAX_RIPPLES, I as PhysicsBody3D, L as RigidBody3D, M as WATER_CUTOUT_MAX, N as WaterCutout3D, P as Area3D, R as StaticBody3D, j as Water3D, z as Node3D } from "./gameplay-B6jqvYeM.js";
2
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, 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, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ 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-DSZwsKPs.js";
3
- import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-CGnoypjL.js";
1
+ import { F as CharacterBody3D, H as WATER_MAX_RIPPLES, I as PhysicsBody3D, L as RigidBody3D, M as WATER_CUTOUT_MAX, N as WaterCutout3D, P as Area3D, R as StaticBody3D, j as Water3D, z as Node3D } from "./gameplay-CZ2yq37J.js";
2
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, 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, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ 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-BQ_QsIBY.js";
3
+ import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-Czzp6ZuE.js";
4
4
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-njybhZon.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-BhI0ehpe.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-Bes-GIuR.js";
7
7
  //#region src/3d/model-verdict.ts
8
8
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
9
9
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -50,6 +50,10 @@ const BODY = {
50
50
  radius: .34,
51
51
  height: 1
52
52
  };
53
+ /** Three decimals — enough for millimetres, and no float dust in pasted JSON. */
54
+ function round3(n) {
55
+ return Math.round(n * 1e3) / 1e3;
56
+ }
53
57
  /**
54
58
  * A found character, as a thing that walks.
55
59
  *
@@ -122,10 +126,10 @@ function characterJson(target) {
122
126
  type: "ModelInstance3D",
123
127
  props: {
124
128
  model: `$${target.key}`,
125
- targetHeight: BODY.height + 2 * BODY.radius,
129
+ targetHeight: round3(BODY.height + 2 * BODY.radius),
126
130
  position: [
127
131
  0,
128
- -(BODY.height / 2 + BODY.radius),
132
+ round3(-(BODY.height / 2 + BODY.radius)),
129
133
  0
130
134
  ],
131
135
  castShadow: true