incanto 0.69.0 → 0.71.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 (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
@@ -1,15 +1,15 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { d as computeViewport, f as resolveViewport, k as diagnose, n as loadScene, z as registerBehavior } from "./loader-DZE7B07H.js";
3
- import { _ as AudioPlayer, v as Engine } from "./register-mmfugpaQ.js";
1
+ import { o as __exportAll } from "./json-CfTjpvW8.js";
2
+ import { D as computeViewport, O as resolveViewport, Y as diagnose, h as loadScene, ot as registerBehavior } from "./save-slots-Bvuh2p_r.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine } from "./register-DuVdzxbq.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { n as attachTouchControls } from "./touch-DEAmqGdf.js";
6
- import { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-Z5_g8x9H.js";
7
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-CMqFVxO9.js";
8
- import { o as frameStats } from "./frame-report-Dlq13Gyj.js";
9
- import { n as registerGameplayBehaviors } from "./gameplay-D1KixWK8.js";
10
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DbVArsns.js";
5
+ import { n as attachTouchControls } from "./touch-BnCyPA0G.js";
6
+ import { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-C8aDbGmR.js";
7
+ import { a as devServerLibrary, c as applyDriveStep, d as audioErrors, i as crossFade, l as wireDevChannel, n as teardown, o as openBundledEditor, r as pauseWhenHidden, t as nodeFromHits, u as claimCanvasGestures } from "./picking-DVo7fI13.js";
8
+ import { o as frameStats } from "./frame-report-D-_7YF2G.js";
9
+ import { n as registerGameplayBehaviors } from "./gameplay-CNULJvwh.js";
10
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BYQCBySi.js";
11
11
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
12
- import { n as enablePhysics2D } from "./physics-2d-CF5yyjbj.js";
12
+ import { n as enablePhysics2D } from "./physics-2d-FhrXUc6B.js";
13
13
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
14
14
  //#region src/2d/assets.ts
15
15
  /**
@@ -101,6 +101,20 @@ var AssetStore2D = class {
101
101
  getTexture(ref) {
102
102
  return this.resolve(ref).texture;
103
103
  }
104
+ /**
105
+ * Hand the GPU back.
106
+ *
107
+ * `AssetStore3D` has had this since it was written and the 2D twin never got
108
+ * one, so a game that swapped scenes or unmounted kept every texture it had
109
+ * ever decoded. `Renderer2D.dispose` walked its scene graph and disposed
110
+ * `material.map` instead — which frees only what is CURRENTLY drawn (a sheet
111
+ * loaded and never used stayed resident), and disposes the STORE'S texture
112
+ * out from under a caller who injected their own store.
113
+ */
114
+ dispose() {
115
+ for (const entry of this.entries.values()) entry.texture?.dispose();
116
+ this.entries.clear();
117
+ }
104
118
  getSheet(ref) {
105
119
  const entry = this.resolve(ref);
106
120
  if (entry.type !== "spritesheet") throw new IncantoError("BAD_FORMAT", `Asset '${ref}' is a plain texture; a spritesheet (with frameWidth/frameHeight) is required here.`);
@@ -433,18 +447,32 @@ var Renderer2D = class {
433
447
  engine;
434
448
  /** The texture store. Public so `renderer.assets.status(key)` is reachable. */
435
449
  assets;
450
+ ownsAssets;
436
451
  loadedAssetScenes = /* @__PURE__ */ new WeakSet();
437
452
  disconnect;
438
453
  canvas;
439
454
  constructor(opts) {
440
455
  this.canvas = opts.canvas;
441
456
  this.engine = opts.engine;
457
+ const cw = opts.canvas.clientWidth || opts.canvas.width || 1;
458
+ const ch = opts.canvas.clientHeight || opts.canvas.height || 1;
459
+ this.lastView = {
460
+ cx: cw / 2,
461
+ cy: ch / 2,
462
+ zoom: 1,
463
+ w: cw,
464
+ h: ch
465
+ };
442
466
  this.assets = opts.assets ?? new AssetStore2D(void 0, this.engine);
467
+ this.ownsAssets = !opts.assets;
443
468
  const rendering = resolveRendering(opts.engine.scene?.environment, {
444
469
  antialias: true,
445
470
  pixelRatio: 1,
446
471
  preserveDrawingBuffer: false
447
472
  }, globalThis.devicePixelRatio ?? 1, opts);
473
+ const tier = qualityRendering(opts.engine.settings.get("quality"));
474
+ rendering.antialias = rendering.antialias && tier.antialias;
475
+ rendering.pixelRatio = Math.min(rendering.pixelRatio, tier.pixelRatio);
448
476
  this.webgl = withWebGLContext(() => new WebGLRenderer({
449
477
  canvas: opts.canvas,
450
478
  antialias: rendering.antialias,
@@ -452,6 +480,20 @@ var Renderer2D = class {
452
480
  }));
453
481
  this.webgl.info.autoReset = false;
454
482
  this.engine.picker = (x, y) => this.pick(x, y);
483
+ this.engine.toWorld = (x, y) => {
484
+ if (!this.hasRendered) return null;
485
+ const w = this.worldFromScreen(x, y);
486
+ return [w.x, w.y];
487
+ };
488
+ this.engine.toScreen = (world) => {
489
+ if (!this.hasRendered) return null;
490
+ const s = this.screenFromWorld(world[0] ?? 0, world[1] ?? 0);
491
+ return {
492
+ x: s.x,
493
+ y: s.y,
494
+ behind: false
495
+ };
496
+ };
455
497
  this.basePixelRatio = rendering.pixelRatio;
456
498
  this.webgl.setPixelRatio(rendering.pixelRatio);
457
499
  this.webgl.info.autoReset = false;
@@ -568,6 +610,7 @@ var Renderer2D = class {
568
610
  h
569
611
  };
570
612
  this.lastViewport = view;
613
+ this.hasRendered = true;
571
614
  if (view && (view.offsetX > .5 || view.offsetY > .5)) {
572
615
  this.webgl.setScissorTest(false);
573
616
  this.webgl.setViewport(0, 0, w, h);
@@ -595,13 +638,29 @@ var Renderer2D = class {
595
638
  h: 1
596
639
  };
597
640
  lastViewport = null;
598
- lastView = {
599
- cx: 0,
600
- cy: 0,
601
- zoom: 1,
602
- w: 1,
603
- h: 1
604
- };
641
+ /**
642
+ * The view the LAST render used — and, before there has been one, the
643
+ * documented default: the whole canvas, centred, at zoom 1.
644
+ *
645
+ * It started as `{cx: 0, cy: 0, w: 1, h: 1}`, a one-pixel canvas nobody has,
646
+ * and `worldFromScreen` answered from it with a straight face. Measured in a
647
+ * browser on a 960x540 canvas with the camera at (160, 80), before the first
648
+ * frame: `toWorld(480, 270)` returned `[479.5, 269.5]` — the arithmetic of a
649
+ * 1x1 window, and a confident number rather than a refusal.
650
+ */
651
+ lastView;
652
+ /**
653
+ * Has a frame been DRAWN?
654
+ *
655
+ * `Renderer3D` refuses every screen-space question until it has one
656
+ * (`if (!this.lastCamera) return null`, in `pick`, `rayFromScreen` and
657
+ * `worldFromScreen`); the flat renderer had no equivalent, so the engine-level
658
+ * `toWorld`/`toScreen` and `pick` answered from a view no frame had used. The
659
+ * hazard is real in exactly the place agents work: a background tab does not
660
+ * run rAF, so `await createGame2D(...)` there is followed by any number of
661
+ * questions with no frame behind them.
662
+ */
663
+ hasRendered = false;
605
664
  /** The world view used by the LAST render (center/zoom/canvas size). */
606
665
  view() {
607
666
  return { ...this.lastView };
@@ -630,11 +689,13 @@ var Renderer2D = class {
630
689
  * Hits resolve through `userData.incantoNode` and prefer higher renderOrder.
631
690
  */
632
691
  pick(sx, sy) {
692
+ if (!this.hasRendered) return null;
633
693
  const { w, h } = this.lastView;
634
694
  const ox = this.lastViewport?.offsetX ?? 0;
635
695
  const oy = this.lastViewport?.offsetY ?? 0;
636
696
  const cw = this.lastViewport?.contentW ?? w;
637
697
  const ch = this.lastViewport?.contentH ?? h;
698
+ if (sx < ox || sx > ox + cw || sy < oy || sy > oy + ch) return null;
638
699
  pickNdc.set((sx - ox) / cw * 2 - 1, -((sy - oy) / ch * 2 - 1));
639
700
  const ui = this.hit(this.uiScene, this.uiCam);
640
701
  if (ui) return ui;
@@ -642,20 +703,7 @@ var Renderer2D = class {
642
703
  }
643
704
  hit(scene, cam) {
644
705
  raycaster.setFromCamera(pickNdc, cam);
645
- const hits = raycaster.intersectObjects(scene.children, true);
646
- let best = null;
647
- for (const hit of hits) {
648
- let obj = hit.object;
649
- while (obj && !obj.userData.incantoNode) obj = obj.parent;
650
- const node = obj?.userData.incantoNode;
651
- if (!node) continue;
652
- const order = hit.object.renderOrder ?? 0;
653
- if (!best || order >= best.order) best = {
654
- node,
655
- order
656
- };
657
- }
658
- return best?.node ?? null;
706
+ return nodeFromHits(raycaster.intersectObjects(scene.children, true), { byRenderOrder: true });
659
707
  }
660
708
  /** Does the UI pass draw this node (i.e. is it under a UILayer)? */
661
709
  isUiNode(node) {
@@ -714,16 +762,16 @@ var Renderer2D = class {
714
762
  }
715
763
  dispose() {
716
764
  if (this.engine.picker) this.engine.picker = null;
765
+ if (this.engine.toWorld) this.engine.toWorld = null;
766
+ if (this.engine.toScreen) this.engine.toScreen = null;
717
767
  this.disconnect();
718
768
  for (const scene of [this.worldScene, this.uiScene]) scene.traverse((obj) => {
719
769
  const mesh = obj;
720
770
  if (!mesh.isMesh) return;
721
771
  const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
722
- for (const mat of mats) {
723
- mat?.map?.dispose();
724
- mat?.dispose();
725
- }
772
+ for (const mat of mats) mat?.dispose();
726
773
  });
774
+ if (this.ownsAssets) this.assets.dispose();
727
775
  this.webgl.dispose();
728
776
  }
729
777
  };
@@ -775,10 +823,18 @@ async function createGame2D(opts) {
775
823
  const cleanups = [];
776
824
  const keyboard = opts.keyboard ?? (typeof window !== "undefined" ? window : false);
777
825
  if (keyboard) engine.input.attachKeyboard(keyboard);
778
- if (opts.pointer) {
826
+ let pointerAttached = false;
827
+ const attachPointerIfWanted = () => {
828
+ if (pointerAttached || opts.pointer === false) return;
829
+ const root = engine.scene?.root;
830
+ if (!root) return;
831
+ if (opts.pointer === void 0 && !wantsPointer(root)) return;
832
+ pointerAttached = true;
779
833
  const lockOnClick = typeof opts.pointer === "object" ? opts.pointer.lockOnClick ?? false : false;
780
834
  engine.input.attachPointer(opts.canvas, { lockOnClick });
781
- }
835
+ };
836
+ attachPointerIfWanted();
837
+ cleanups.push(engine.sceneChanged.connect(() => attachPointerIfWanted()));
782
838
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
783
839
  cleanups.push(engine.settings.bindAudio(engine.audio));
784
840
  cleanups.push(pauseWhenHidden(engine));
@@ -807,6 +863,7 @@ async function createGame2D(opts) {
807
863
  canvas: opts.canvas,
808
864
  engine,
809
865
  ...opts.pixelRatio !== void 0 ? { pixelRatio: opts.pixelRatio } : {},
866
+ ...opts.preserveDrawingBuffer !== void 0 ? { preserveDrawingBuffer: opts.preserveDrawingBuffer } : {},
810
867
  ...opts.antialias !== void 0 ? { antialias: opts.antialias } : {}
811
868
  });
812
869
  cleanups.push(engine.settings.bindFrameCap((fps) => {
@@ -831,26 +888,15 @@ async function createGame2D(opts) {
831
888
  requestAnimationFrame(tick);
832
889
  };
833
890
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(tick);
834
- for (const step of steps) switch (step.kind) {
835
- case "press":
836
- engine.input.pressAction(step.action);
837
- break;
838
- case "release":
839
- engine.input.releaseAction(step.action);
840
- break;
841
- case "vector":
842
- engine.input.setActionVector(step.action, step.x, step.y);
843
- break;
844
- case "key":
845
- engine.input.handleKey(step.code, step.down);
846
- break;
847
- case "pointer":
848
- engine.input.handlePointerMove(step.dx, step.dy);
849
- break;
850
- case "step":
851
- await new Promise((r) => setTimeout(r, step.ms));
852
- break;
853
- }
891
+ const target = {
892
+ engine,
893
+ project: (x, y) => {
894
+ const at = renderer.screenFromWorld;
895
+ return at ? at.call(renderer, x, y) : null;
896
+ },
897
+ wait: (ms) => new Promise((r) => setTimeout(r, ms))
898
+ };
899
+ for (const step of steps) await applyDriveStep(target, step);
854
900
  watching = false;
855
901
  return frames;
856
902
  };
@@ -876,10 +922,11 @@ async function createGame2D(opts) {
876
922
  if (!editorSwitch) return;
877
923
  const open = editorSwitch.open ?? openBundledEditor;
878
924
  const reboot = async (working) => {
879
- await createGame2D({
925
+ const next = await createGame2D({
880
926
  ...opts,
881
927
  scene: working
882
928
  });
929
+ editorSwitch?.onReboot?.(next);
883
930
  };
884
931
  await crossFade(async () => {
885
932
  disposeGame();
@@ -892,7 +939,7 @@ async function createGame2D(opts) {
892
939
  ...editorSwitch.library ? { library: editorSwitch.library === true ? devServerLibrary() : editorSwitch.library } : {}
893
940
  });
894
941
  } catch (error) {
895
- console.error("[incanto] could not open the editor:", error);
942
+ diagnose(engine, "error", "[incanto] could not open the editor:", error);
896
943
  await reboot(sourceJson);
897
944
  }
898
945
  }, "opening the editor…");
@@ -912,7 +959,22 @@ async function createGame2D(opts) {
912
959
  }
913
960
  }
914
961
  engine.start();
962
+ /**
963
+ * Has this handle already been torn down?
964
+ *
965
+ * After one ✎→▶ editor round trip the handle `createGame*` returned is
966
+ * ORPHANED: `reboot()` builds a replacement and the app still holds the old
967
+ * one. Calling `game.openEditor()` on it then booted a THIRD game on top of
968
+ * the live one, and `game.dispose()` tore down a game that was already gone
969
+ * while leaving the live one running. A second `dispose()` also re-entered
970
+ * Rapier's already-nulled `world.free()` and printed an engine error.
971
+ *
972
+ * A stale handle does nothing rather than something destructive.
973
+ */
974
+ let disposed = false;
915
975
  function disposeGame() {
976
+ if (disposed) return;
977
+ disposed = true;
916
978
  teardown([
917
979
  ["physics", () => physics?.dispose()],
918
980
  ["app cleanups", () => {
@@ -954,7 +1016,10 @@ async function createGame2D(opts) {
954
1016
  ...renderer.stats?.()
955
1017
  };
956
1018
  },
957
- openEditor: editorSwitch ? editorAction.run : null,
1019
+ openEditor: editorSwitch ? async () => {
1020
+ if (disposed) return;
1021
+ await editorAction.run();
1022
+ } : null,
958
1023
  dispose: disposeGame
959
1024
  };
960
1025
  }
@@ -1013,5 +1078,23 @@ function wireAudioUnlock(engine, root, canvas) {
1013
1078
  for (const s of sources) s.target.addEventListener?.(s.type, unlock, { passive: true });
1014
1079
  return detachAll;
1015
1080
  }
1081
+ /**
1082
+ * Does this tree have anything the CURSOR drives?
1083
+ *
1084
+ * `physics: 'auto'` has always meant "on iff the tree needs it"; the pointer had
1085
+ * no such default, so a board of `Clickable`s booted without `pointer: true` did
1086
+ * nothing when clicked — and the skill that introduces the behaviour says it
1087
+ * "needs no code at all". This makes that true.
1088
+ *
1089
+ * Only `Clickable`, deliberately: a behaviour of your own that reads
1090
+ * `pointerPosition()` is invisible from here, and guessing wider would attach a
1091
+ * pointer to games that never asked (and, with `lockOnClick`, capture a cursor
1092
+ * nobody wanted).
1093
+ */
1094
+ function wantsPointer(root) {
1095
+ if (root.behavior?.constructor.name === "Clickable") return true;
1096
+ for (const child of root.children) if (wantsPointer(child)) return true;
1097
+ return false;
1098
+ }
1016
1099
  //#endregion
1017
1100
  export { AssetStore2D as a, syncTree2D as i, create_game_exports as n, Renderer2D as r, createGame2D as t };
@@ -1,16 +1,16 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { c as parseEnvironment3D, k as diagnose, n as loadScene, s as horizonColorFromSky, u as sunDirectionFromSky, z as registerBehavior } from "./loader-DZE7B07H.js";
3
- import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-mmfugpaQ.js";
1
+ import { o as __exportAll } from "./json-CfTjpvW8.js";
2
+ import { C as sunDirectionFromSky, Y as diagnose, b as horizonColorFromSky, h as loadScene, ot as registerBehavior, x as parseEnvironment3D } from "./save-slots-Bvuh2p_r.js";
3
+ import { C as qualityRendering, S as qualityEnvironment, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-DuVdzxbq.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { n as attachTouchControls } from "./touch-DEAmqGdf.js";
6
- import { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-Z5_g8x9H.js";
7
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-CMqFVxO9.js";
8
- import { o as frameStats } from "./frame-report-Dlq13Gyj.js";
9
- import { J as createCausticsQuad, K as Node3D, U as PhysicsBody3D, n as registerGameplayBehaviors } from "./gameplay-D1KixWK8.js";
5
+ import { n as attachTouchControls } from "./touch-BnCyPA0G.js";
6
+ import { n as withWebGLContext, r as resolveRendering } from "./webgl-unavailable-C8aDbGmR.js";
7
+ import { a as devServerLibrary, c as applyDriveStep, d as audioErrors, i as crossFade, l as wireDevChannel, n as teardown, o as openBundledEditor, r as pauseWhenHidden, s as poseFromRenderer, t as nodeFromHits, u as claimCanvasGestures } from "./picking-DVo7fI13.js";
8
+ import { o as frameStats } from "./frame-report-D-_7YF2G.js";
9
+ import { J as createCausticsQuad, K as Node3D, U as PhysicsBody3D, n as registerGameplayBehaviors } from "./gameplay-CNULJvwh.js";
10
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
11
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-BO-7XoFV.js";
12
- import { n as enablePhysics3D } from "./physics-3d-DbYaEcZy.js";
13
- 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";
11
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-99BzP_L-.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-CFEGxBXZ.js";
13
+ import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, LoadingManager, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
14
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
15
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
16
16
  import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
@@ -72,7 +72,12 @@ var AssetStore3D = class {
72
72
  */
73
73
  constructor(engine = null) {
74
74
  this.engine = engine;
75
- this.loader = new GLTFLoader();
75
+ const manager = new LoadingManager();
76
+ manager.onError = (url) => {
77
+ if (this.models.has(url) || this.animations.has(url)) return;
78
+ this.textures.noteError(url, "a model needs this texture and it did not load");
79
+ };
80
+ this.loader = new GLTFLoader(manager);
76
81
  this.loader.register((parser) => new VRMLoaderPlugin(parser));
77
82
  this.textures = new TextureCache3D({ diagnostics: engine ? { warn: (...parts) => diagnose(engine, "warn", ...parts) } : null });
78
83
  }
@@ -1353,7 +1358,7 @@ function walk(node, parentObj, state) {
1353
1358
  for (const child of node.children) walk(child, nextParent, state);
1354
1359
  if (node instanceof Node3D) {
1355
1360
  const obj = node._ensureObject3D();
1356
- if (node.static && !state.ignoreStatic) {
1361
+ if (node.static && !state.ignoreStatic && node._staticReady(state.assets ?? null)) {
1357
1362
  obj.userData.incantoStaticSynced = true;
1358
1363
  obj.userData.incantoStatic = true;
1359
1364
  warnStaticHazards(node);
@@ -1374,7 +1379,7 @@ function warnStaticHazards(root) {
1374
1379
  for (const c of n.children) scan(c);
1375
1380
  };
1376
1381
  scan(root);
1377
- if (hazards.length > 0) console.warn(`[incanto] static subtree '${root.name}' freezes animated/per-frame nodes: ${hazards.join(", ")} — they will stop updating. Unmark static or move them out.`);
1382
+ if (hazards.length > 0) diagnose(root.tree?.engine ?? null, "warn", `[incanto] static subtree '${root.name}' freezes animated/per-frame nodes: ${hazards.join(", ")} — they will stop updating. Unmark static or move them out.`);
1378
1383
  }
1379
1384
  function prune(obj, visited) {
1380
1385
  const ch = obj.children;
@@ -1494,6 +1499,16 @@ var Renderer3D = class {
1494
1499
  }));
1495
1500
  this.webgl.info.autoReset = false;
1496
1501
  this.engine.picker = (x, y) => this.pick(x, y);
1502
+ this.engine.toWorld = (x, y) => {
1503
+ const w = this.worldFromScreen(x, y);
1504
+ return w ? [
1505
+ w.x,
1506
+ w.y,
1507
+ w.z
1508
+ ] : null;
1509
+ };
1510
+ this.engine.toScreen = (world) => this.screenFromWorld(world[0] ?? 0, world[1] ?? 0, world[2] ?? 0);
1511
+ this.engine.screenRay = (x, y) => this.rayFromScreen(x, y);
1497
1512
  this.basePixelRatio = rendering.pixelRatio;
1498
1513
  this.webgl.setPixelRatio(rendering.pixelRatio);
1499
1514
  this.adaptive = opts.adaptiveResolution === false ? null : new AdaptiveResolution();
@@ -2067,6 +2082,7 @@ var Renderer3D = class {
2067
2082
  rayFromScreen(sx, sy) {
2068
2083
  const cam = this.lastCamera;
2069
2084
  if (!cam) return null;
2085
+ if (sx < 0 || sx > this.lastSize.w || sy < 0 || sy > this.lastSize.h) return null;
2070
2086
  pickNdc.set(sx / this.lastSize.w * 2 - 1, -(sy / this.lastSize.h * 2 - 1));
2071
2087
  pickRaycaster.setFromCamera(pickNdc, cam);
2072
2088
  const o = pickRaycaster.ray.origin;
@@ -2095,14 +2111,7 @@ var Renderer3D = class {
2095
2111
  if (!cam) return null;
2096
2112
  pickNdc.set(sx / this.lastSize.w * 2 - 1, -(sy / this.lastSize.h * 2 - 1));
2097
2113
  pickRaycaster.setFromCamera(pickNdc, cam);
2098
- const hits = pickRaycaster.intersectObjects(this.threeScene.children, true);
2099
- for (const hit of hits) {
2100
- let obj = hit.object;
2101
- while (obj && !obj.userData.incantoNode) obj = obj.parent;
2102
- const node = obj?.userData.incantoNode;
2103
- if (node) return node;
2104
- }
2105
- return null;
2114
+ return nodeFromHits(pickRaycaster.intersectObjects(this.threeScene.children, true));
2106
2115
  }
2107
2116
  /**
2108
2117
  * GPU counters for the last rendered frame — EVERY pass of it, summed (see
@@ -2154,6 +2163,9 @@ var Renderer3D = class {
2154
2163
  }
2155
2164
  dispose() {
2156
2165
  if (this.engine.picker) this.engine.picker = null;
2166
+ if (this.engine.toWorld) this.engine.toWorld = null;
2167
+ if (this.engine.toScreen) this.engine.toScreen = null;
2168
+ if (this.engine.screenRay) this.engine.screenRay = null;
2157
2169
  this.disconnect();
2158
2170
  this.threeScene.traverse((obj) => {
2159
2171
  const mesh = obj;
@@ -2257,10 +2269,18 @@ async function createGame3D(opts) {
2257
2269
  const cleanups = [];
2258
2270
  const keyboard = opts.keyboard ?? (typeof window !== "undefined" ? window : false);
2259
2271
  if (keyboard) engine.input.attachKeyboard(keyboard);
2260
- if (opts.pointer) {
2272
+ let pointerAttached = false;
2273
+ const attachPointerIfWanted = () => {
2274
+ if (pointerAttached || opts.pointer === false) return;
2275
+ const root = engine.scene?.root;
2276
+ if (!root) return;
2277
+ if (opts.pointer === void 0 && !wantsPointer(root)) return;
2278
+ pointerAttached = true;
2261
2279
  const lockOnClick = typeof opts.pointer === "object" ? opts.pointer.lockOnClick ?? true : true;
2262
2280
  engine.input.attachPointer(opts.canvas, { lockOnClick });
2263
- }
2281
+ };
2282
+ attachPointerIfWanted();
2283
+ cleanups.push(engine.sceneChanged.connect(() => attachPointerIfWanted()));
2264
2284
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
2265
2285
  if (!opts.pageGestures) claimCanvasGestures(opts.canvas);
2266
2286
  const touchMode = opts.touch ?? "auto";
@@ -2305,10 +2325,11 @@ async function createGame3D(opts) {
2305
2325
  const camera = poseFromRenderer(renderer);
2306
2326
  const open = editorSwitch.open ?? openBundledEditor;
2307
2327
  const reboot = async (working) => {
2308
- await createGame3D({
2328
+ const next = await createGame3D({
2309
2329
  ...opts,
2310
2330
  scene: working
2311
2331
  });
2332
+ editorSwitch?.onReboot?.(next);
2312
2333
  };
2313
2334
  await crossFade(async () => {
2314
2335
  disposeGame();
@@ -2322,7 +2343,7 @@ async function createGame3D(opts) {
2322
2343
  ...editorSwitch.library ? { library: editorSwitch.library === true ? devServerLibrary() : editorSwitch.library } : {}
2323
2344
  });
2324
2345
  } catch (error) {
2325
- console.error("[incanto] could not open the editor:", error);
2346
+ diagnose(engine, "error", "[incanto] could not open the editor:", error);
2326
2347
  await reboot(sourceJson);
2327
2348
  }
2328
2349
  }, "opening the editor…");
@@ -2355,8 +2376,12 @@ async function createGame3D(opts) {
2355
2376
  } catch {}
2356
2377
  cleanups.push(engine.settings.bindAudio(engine.audio));
2357
2378
  cleanups.push(engine.settings.bindLocale(engine.locale));
2358
- cleanups.push(engine.settings.bindQuality((patch) => {
2379
+ const applyTier = (patch) => {
2359
2380
  applyQualityTier3D(engine, patch);
2381
+ };
2382
+ cleanups.push(engine.settings.bindQuality(applyTier));
2383
+ cleanups.push(engine.sceneChanged.connect(() => {
2384
+ applyTier(qualityEnvironment(engine.settings.get("quality")));
2360
2385
  }));
2361
2386
  cleanups.push(engine.settings.bindFrameCap((fps) => {
2362
2387
  engine.maxFps = fps;
@@ -2386,26 +2411,15 @@ async function createGame3D(opts) {
2386
2411
  requestAnimationFrame(tick);
2387
2412
  };
2388
2413
  requestAnimationFrame(tick);
2389
- for (const s of steps) switch (s.kind) {
2390
- case "press":
2391
- engine.input.pressAction(s.action);
2392
- break;
2393
- case "release":
2394
- engine.input.releaseAction(s.action);
2395
- break;
2396
- case "vector":
2397
- engine.input.setActionVector(s.action, s.x, s.y);
2398
- break;
2399
- case "key":
2400
- engine.input.handleKey(s.code, s.down);
2401
- break;
2402
- case "pointer":
2403
- engine.input.handlePointerMove(s.dx, s.dy);
2404
- break;
2405
- case "step":
2406
- await new Promise((r) => setTimeout(r, s.ms));
2407
- break;
2408
- }
2414
+ const target = {
2415
+ engine,
2416
+ project: (x, y, z) => {
2417
+ const at = renderer.screenFromWorld;
2418
+ return at ? at.call(renderer, x, y, z) : null;
2419
+ },
2420
+ wait: (ms) => new Promise((r) => setTimeout(r, ms))
2421
+ };
2422
+ for (const s of steps) await applyDriveStep(target, s);
2409
2423
  watching = false;
2410
2424
  return frames;
2411
2425
  };
@@ -2418,7 +2432,22 @@ async function createGame3D(opts) {
2418
2432
  }));
2419
2433
  cleanups.push(pauseWhenHidden(engine));
2420
2434
  await report(1, "ready");
2435
+ /**
2436
+ * Has this handle already been torn down?
2437
+ *
2438
+ * After one ✎→▶ editor round trip the handle `createGame*` returned is
2439
+ * ORPHANED: `reboot()` builds a replacement and the app still holds the old
2440
+ * one. Calling `game.openEditor()` on it then booted a THIRD game on top of
2441
+ * the live one, and `game.dispose()` tore down a game that was already gone
2442
+ * while leaving the live one running. A second `dispose()` also re-entered
2443
+ * Rapier's already-nulled `world.free()` and printed an engine error.
2444
+ *
2445
+ * A stale handle does nothing rather than something destructive.
2446
+ */
2447
+ let disposed = false;
2421
2448
  function disposeGame() {
2449
+ if (disposed) return;
2450
+ disposed = true;
2422
2451
  teardown([
2423
2452
  ["physics", () => physics?.dispose()],
2424
2453
  ["app cleanups", () => {
@@ -2460,7 +2489,10 @@ async function createGame3D(opts) {
2460
2489
  ...renderer.stats?.()
2461
2490
  };
2462
2491
  },
2463
- openEditor: editorSwitch ? editorAction.run : null,
2492
+ openEditor: editorSwitch ? async () => {
2493
+ if (disposed) return;
2494
+ await editorAction.run();
2495
+ } : null,
2464
2496
  dispose: disposeGame
2465
2497
  };
2466
2498
  }
@@ -2519,5 +2551,23 @@ function wireAudioUnlock(engine, root, canvas) {
2519
2551
  for (const s of sources) s.target.addEventListener?.(s.type, unlock, { passive: true });
2520
2552
  return detachAll;
2521
2553
  }
2554
+ /**
2555
+ * Does this tree have anything the CURSOR drives?
2556
+ *
2557
+ * `physics: 'auto'` has always meant "on iff the tree needs it"; the pointer had
2558
+ * no such default, so a board of `Clickable`s booted without `pointer: true` did
2559
+ * nothing when clicked — and the skill that introduces the behaviour says it
2560
+ * "needs no code at all". This makes that true.
2561
+ *
2562
+ * Only `Clickable`, deliberately: a behaviour of your own that reads
2563
+ * `pointerPosition()` is invisible from here, and guessing wider would attach a
2564
+ * pointer to games that never asked (and, with `lockOnClick`, capture a cursor
2565
+ * nobody wanted).
2566
+ */
2567
+ function wantsPointer(root) {
2568
+ if (root.behavior?.constructor.name === "Clickable") return true;
2569
+ for (const child of root.children) if (wantsPointer(child)) return true;
2570
+ return false;
2571
+ }
2522
2572
  //#endregion
2523
2573
  export { Environment3D as a, AssetStore3D as c, syncTree as i, create_game_exports as n, applyQualityTier3D as o, Renderer3D as r, setEnvironment3D as s, createGame3D as t };
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-rm8vVzu_.js";
1
+ import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-Dcz0fr1S.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
package/dist/debug.js CHANGED
@@ -1,6 +1,5 @@
1
- import { t as jsonClone } from "./json-CwwhxQgb.js";
2
- import { s as mergeStaticProps } from "./registry-CF70EArN.js";
3
- import { r as capturePointer } from "./touch-DEAmqGdf.js";
1
+ import { t as jsonClone } from "./json-CfTjpvW8.js";
2
+ import { d as mergeStaticProps, r as capturePointer } from "./touch-BnCyPA0G.js";
4
3
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
5
4
  //#region src/debug/capture.ts
6
5
  /**
@@ -0,0 +1,12 @@
1
+ //#region src/core/diagnostics.d.ts
2
+ /** The minimum an engine has to look like to receive a diagnostic. */
3
+ interface DiagnosticSink {
4
+ log: {
5
+ warn(...parts: unknown[]): void;
6
+ error(...parts: unknown[]): void;
7
+ info(...parts: unknown[]): void;
8
+ debug(...parts: unknown[]): void;
9
+ };
10
+ }
11
+ //#endregion
12
+ export { DiagnosticSink as t };