incanto 0.65.0 → 0.67.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 (88) hide show
  1. package/bin/_behaviors-loader.mjs +87 -0
  2. package/bin/incanto-editor.mjs +39 -2
  3. package/bin/incanto-feel.mjs +4 -1
  4. package/bin/incanto-frame.mjs +52 -3
  5. package/bin/incanto-logs.mjs +33 -3
  6. package/bin/incanto-multiplay.mjs +50 -1
  7. package/bin/incanto-play.mjs +3 -1
  8. package/bin/incanto-playtest.mjs +3 -25
  9. package/bin/incanto-verify.mjs +15 -2
  10. package/dist/2d.d.ts +76 -4
  11. package/dist/2d.js +3 -3
  12. package/dist/3d.d.ts +213 -14
  13. package/dist/3d.js +6 -6
  14. package/dist/{audio-player-BrEHwbK1.d.ts → audio-player-BNUHo0JF.d.ts} +1 -1
  15. package/dist/{behavior-CmMuQ6CL.d.ts → behavior-ZnfzKDo3.d.ts} +58 -1
  16. package/dist/{create-game-DIyh5Zy9.js → create-game-DMetd3vR.js} +14 -8
  17. package/dist/{create-game-W2xzTJhs.js → create-game-FAXdHta3.js} +138 -11
  18. package/dist/debug.d.ts +1 -1
  19. package/dist/debug.js +3 -2
  20. package/dist/{duplicate-CqSAtdrh.js → duplicate-B8QnX2-r.js} +1 -1
  21. package/dist/editor.js +7 -2
  22. package/dist/{environment-presets-BvHs6HEk.js → environment-presets-C-5g2pMh.js} +113 -9
  23. package/dist/{gameplay-D-H986A4.js → gameplay-CGSv-9XR.js} +295 -12
  24. package/dist/gameplay.d.ts +102 -6
  25. package/dist/gameplay.js +2 -2
  26. package/dist/index.d.ts +50 -6
  27. package/dist/index.js +7 -6
  28. package/dist/{loader-CTB441z4.d.ts → loader-DyT30ArB.d.ts} +1 -1
  29. package/dist/{loader-lQDCwNag.js → loader-KZESOZmE.js} +148 -1
  30. package/dist/net.d.ts +2 -2
  31. package/dist/net.js +1 -1
  32. package/dist/{physics-2d-CmnunaR2.js → physics-2d-_B6zj3zU.js} +57 -4
  33. package/dist/{physics-3d-DULB1lD1.js → physics-3d-i1IeAY0F.js} +74 -5
  34. package/dist/{quiet-rapier-BAJ4K94N.js → quiet-rapier-C6fW4zcW.js} +14 -1
  35. package/dist/react.d.ts +1 -1
  36. package/dist/react.js +1 -1
  37. package/dist/{register-CgeR4ckF.js → register-Cs53CY7s.js} +110 -16
  38. package/dist/{register-DO5_Qp0Q.js → register-vIxU3Xbv.js} +38 -4
  39. package/dist/rendering-options-ByAdi8PF.js +184 -0
  40. package/dist/{replay-CEw2xAhG.js → replay-BkpDV51O.js} +4 -3
  41. package/dist/{replay-CThvwkcQ.d.ts → replay-D0GcEHYU.d.ts} +1 -1
  42. package/dist/{split-screen-DxZzC1nm.d.ts → split-screen-DD5eYIgW.d.ts} +2 -2
  43. package/dist/{split-screen-gb0c3xdb.js → split-screen-DS8RaUV_.js} +2 -2
  44. package/dist/{src-DYffzHbd.js → src-BLlsGJW9.js} +1 -1
  45. package/dist/{teardown-BordHL9o.js → teardown-LB9cTnXJ.js} +2 -2
  46. package/dist/{test-CXZuXs8k.js → test-81uZ967a.js} +127 -24
  47. package/dist/test.d.ts +7 -5
  48. package/dist/test.js +2 -2
  49. package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
  50. package/dist/vite.d.ts +0 -7
  51. package/dist/vite.js +13 -3
  52. package/editor/assets/{agent8-DO5qzIOD.js → agent8-C2jtNKbx.js} +1 -1
  53. package/editor/assets/debug-DoU3KeqS.js +3 -0
  54. package/editor/assets/{index-CqEUVaPN.js → index-CNqHojcC.js} +92 -92
  55. package/editor/index.html +1 -1
  56. package/package.json +1 -1
  57. package/schemas/scene.schema.json +20 -1
  58. package/skills/incanto-building-3d-games.md +19 -0
  59. package/skills/incanto-editor.md +5 -1
  60. package/skills/incanto-environment.md +2 -2
  61. package/skills/incanto-gameplay-behaviors.md +86 -7
  62. package/skills/incanto-hud.md +14 -1
  63. package/skills/incanto-multiplayer.md +14 -0
  64. package/skills/incanto-node-reference.md +60 -2
  65. package/skills/incanto-performance.md +35 -0
  66. package/skills/incanto-physics-and-input.md +69 -0
  67. package/skills/incanto-playtesting.md +15 -1
  68. package/skills/incanto-save-slots.md +21 -1
  69. package/skills/incanto-verifying-your-game.md +19 -0
  70. package/skills/incanto-web-integration.md +24 -3
  71. package/skills/incanto-your-first-game.md +4 -4
  72. package/templates-app/beacon-isle-3d/package.json +1 -1
  73. package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/game.scene.json +99 -53
  76. package/templates-app/star-survivor/package.json +1 -1
  77. package/templates-app/star-survivor/src/game.scene.json +31 -17
  78. package/templates-app/tps-3d/package.json +1 -1
  79. package/templates-app/tps-3d/src/game.scene.json +14 -8
  80. package/templates-app/village-quest-3d/package.json +1 -1
  81. package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
  82. package/templates-app/village-quest-3d/src/village.scene.json +10 -7
  83. package/editor/assets/debug-DntG9CVD.js +0 -3
  84. package/templates-app/beacon-isle-3d/coverage.json +0 -9
  85. package/templates-app/platformer-2d/coverage.json +0 -5
  86. package/templates-app/star-survivor/coverage.json +0 -5
  87. package/templates-app/tps-3d/coverage.json +0 -5
  88. package/templates-app/village-quest-3d/coverage.json +0 -9
@@ -1,14 +1,15 @@
1
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-lQDCwNag.js";
3
- import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-CgeR4ckF.js";
2
+ import { c as parseEnvironment3D, k as diagnose, n as loadScene, s as horizonColorFromSky, u as sunDirectionFromSky, z as registerBehavior } from "./loader-KZESOZmE.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-Cs53CY7s.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- 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-BordHL9o.js";
5
+ import { n as attachTouchControls } from "./touch-DEAmqGdf.js";
6
+ import { t as resolveRendering } from "./rendering-options-ByAdi8PF.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-LB9cTnXJ.js";
7
8
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { K as createCausticsQuad, V as PhysicsBody3D, W as Node3D, n as registerGameplayBehaviors } from "./gameplay-D-H986A4.js";
9
+ import { J as createCausticsQuad, K as Node3D, U as PhysicsBody3D, n as registerGameplayBehaviors } from "./gameplay-CGSv-9XR.js";
9
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-BvHs6HEk.js";
11
- import { n as enablePhysics3D } from "./physics-3d-DULB1lD1.js";
11
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-C-5g2pMh.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-i1IeAY0F.js";
12
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";
13
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
14
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -27,6 +28,31 @@ import { Sky } from "three/examples/jsm/objects/Sky.js";
27
28
  */
28
29
  var AssetStore3D = class {
29
30
  engine;
31
+ /**
32
+ * One geometry and one material per distinct SHAPE and per distinct look.
33
+ *
34
+ * `MeshInstance3D._createObject3D` used to `new BoxGeometry()` and
35
+ * `new MeshPhysicalMaterial()` for every node, even when byte-identical.
36
+ * Three identical nodes reported `geometry shared? false false / material
37
+ * shared? false false`, with distinct uuids. Measured at 3,200 units, all
38
+ * three runs at identical 3,209 draw calls / 1,741,932 triangles and an
39
+ * identical screenshot:
40
+ *
41
+ * ```
42
+ * per-node geometry + material 31.0 ms wall (renderMs 27.3)
43
+ * share one geometry 27.0 ms
44
+ * + share two materials 15.4 ms <- 2x the headroom
45
+ * ```
46
+ *
47
+ * 13.1 ms of that 26.5 ms cliff was GPU state churn from per-node objects,
48
+ * and none of it was visible headlessly: with no renderer `_syncObject3D`
49
+ * never runs, so the bench measured 55 ns/node instead of ~2,000.
50
+ *
51
+ * Renderer-scoped, deliberately — module-level state is the predecessor's
52
+ * fatal flaw and two games in one page must not share a cache.
53
+ */
54
+ meshGeometries = /* @__PURE__ */ new Map();
55
+ meshMaterials = /* @__PURE__ */ new Map();
30
56
  models = /* @__PURE__ */ new Map();
31
57
  animations = /* @__PURE__ */ new Map();
32
58
  modelKeys = /* @__PURE__ */ new Map();
@@ -253,10 +279,27 @@ function yieldToFrame(maxWaitMs = DEFAULT_MAX_WAIT_MS, raf = typeof requestAnima
253
279
  * - hard-validates the merged result NOW (in your call stack), so a bad patch
254
280
  * throws where you wrote it — never inside the render loop
255
281
  */
256
- function setEnvironment3D(engine, patch) {
282
+ /**
283
+ * The environment stages a quality tier is allowed to touch.
284
+ *
285
+ * These are the ones `qualityEnvironment` names, and the ones that must snap
286
+ * back to what the scene file says rather than accumulating.
287
+ */
288
+ const TIER_KEYS = [
289
+ "shadows",
290
+ "bloom",
291
+ "post",
292
+ "clouds"
293
+ ];
294
+ function setEnvironment3D(engine, patch, opts) {
257
295
  const scene = engine.scene;
258
296
  if (!scene) throw new IncantoError("BAD_FORMAT", "setEnvironment3D: the engine has no scene.");
259
297
  const merged = { ...scene.environment ?? {} };
298
+ if (opts?.rebaseTierKeys) {
299
+ const authored = scene.source?.environment ?? {};
300
+ for (const key of TIER_KEYS) if (key in authored) merged[key] = authored[key];
301
+ else delete merged[key];
302
+ }
260
303
  for (const [key, value] of Object.entries(patch)) if (value === null) delete merged[key];
261
304
  else if (typeof value === "object" && !Array.isArray(value) && typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) merged[key] = {
262
305
  ...merged[key],
@@ -266,6 +309,24 @@ function setEnvironment3D(engine, patch) {
266
309
  parseEnvironment3D(merged);
267
310
  scene.environment = merged;
268
311
  }
312
+ /**
313
+ * Apply a quality tier — the only caller that may RESET as well as patch.
314
+ *
315
+ * A tier is re-applied every time the player moves the slider, so it cannot
316
+ * compose with what the last one left behind: `low` strips the author's bloom,
317
+ * and `high` then has nothing to put back. This re-derives the four tier-owned
318
+ * stages from the scene FILE first, so every tier is reversible, and so a stage
319
+ * the author never wrote can never appear — the contract `settings.ts` states
320
+ * and `incanto-performance.md` promises: *the tier can only take things away.*
321
+ *
322
+ * Measured before this, on a scene with no `bloom` and no `post` in its JSON:
323
+ * `high` and `medium` both rendered five GL passes at 3.3 ms where `low`
324
+ * rendered one at 1.24 ms — 62% of the frame on a chain the game never asked
325
+ * for, and `environment.bloom` read back `{strength: 0.8, threshold: 1}`.
326
+ */
327
+ function applyQualityTier3D(engine, patch) {
328
+ setEnvironment3D(engine, patch, { rebaseTierKeys: true });
329
+ }
269
330
  //#endregion
270
331
  //#region src/3d/adaptive-resolution.ts
271
332
  const DEFAULTS = {
@@ -1967,6 +2028,67 @@ var Renderer3D = class {
1967
2028
  };
1968
2029
  }
1969
2030
  /**
2031
+ * The WORLD POINT under canvas pixel (sx, sy) — the other half of
2032
+ * `screenFromWorld`, and the call `incanto-web-integration.md` has always
2033
+ * offered "for taps and clicks".
2034
+ *
2035
+ * It did not exist in 3D. `Renderer2D` has had it since the beginning, the
2036
+ * doc covers both runtimes, and an agent building a tower defense went looking
2037
+ * for it and measured `typeof game.renderer.worldFromScreen === "undefined"`.
2038
+ * `pick()` gives you the NODE under a pixel but never the POINT, so "put the
2039
+ * thing where the player clicked on the ground" was not expressible and that
2040
+ * game discretised its whole map into clickable pads instead.
2041
+ *
2042
+ * A pixel in 3D is a RAY, not a point, so this needs a surface to land on and
2043
+ * takes the horizontal plane at `planeY` (default 0 — the ground). Returns
2044
+ * null when the ray runs parallel to that plane or would only meet it behind
2045
+ * the camera, which is what "the sky" looks like from here.
2046
+ *
2047
+ * For anything not flat — terrain, a hillside, a stack of crates — raycast
2048
+ * physics instead: `rayFromScreen` gives you the origin and direction to feed
2049
+ * `physics.castRay`.
2050
+ */
2051
+ worldFromScreen(sx, sy, planeY = 0) {
2052
+ const ray = this.rayFromScreen(sx, sy);
2053
+ if (!ray) return null;
2054
+ const dy = ray.dir[1];
2055
+ if (Math.abs(dy) < 1e-6) return null;
2056
+ const t = (planeY - ray.origin[1]) / dy;
2057
+ if (t < 0) return null;
2058
+ return {
2059
+ x: ray.origin[0] + ray.dir[0] * t,
2060
+ y: planeY,
2061
+ z: ray.origin[2] + ray.dir[2] * t
2062
+ };
2063
+ }
2064
+ /**
2065
+ * The camera ray through canvas pixel (sx, sy) — origin and unit direction,
2066
+ * ready for `physics.castRay(origin, dir, maxLen)`.
2067
+ *
2068
+ * This is the honest 3D answer to "where did the player click": on uneven
2069
+ * ground the only thing that knows is the collision world.
2070
+ */
2071
+ rayFromScreen(sx, sy) {
2072
+ const cam = this.lastCamera;
2073
+ if (!cam) return null;
2074
+ pickNdc.set(sx / this.lastSize.w * 2 - 1, -(sy / this.lastSize.h * 2 - 1));
2075
+ pickRaycaster.setFromCamera(pickNdc, cam);
2076
+ const o = pickRaycaster.ray.origin;
2077
+ const d = pickRaycaster.ray.direction;
2078
+ return {
2079
+ origin: [
2080
+ o.x,
2081
+ o.y,
2082
+ o.z
2083
+ ],
2084
+ dir: [
2085
+ d.x,
2086
+ d.y,
2087
+ d.z
2088
+ ]
2089
+ };
2090
+ }
2091
+ /**
1970
2092
  * The node rendered under canvas pixel (sx, sy), or null — raycasts the last
1971
2093
  * rendered camera into the scene and resolves the hit up to its `incantoNode`
1972
2094
  * (every Node3D stamps that in `_ensureObject3D`). 3D depth-sorts, so the
@@ -2119,6 +2241,7 @@ async function createGame3D(opts) {
2119
2241
  const engine = new Engine({
2120
2242
  seed: opts.seed,
2121
2243
  fixedHz: opts.fixedHz,
2244
+ ...opts.settings ? { settings: opts.settings } : {},
2122
2245
  ...opts._scheduler ? { scheduler: opts._scheduler } : {}
2123
2246
  });
2124
2247
  const sourceJson = cloneSceneJson(opts.scene);
@@ -2233,7 +2356,7 @@ async function createGame3D(opts) {
2233
2356
  cleanups.push(engine.settings.bindAudio(engine.audio));
2234
2357
  cleanups.push(engine.settings.bindLocale(engine.locale));
2235
2358
  cleanups.push(engine.settings.bindQuality((patch) => {
2236
- setEnvironment3D(engine, patch);
2359
+ applyQualityTier3D(engine, patch);
2237
2360
  }));
2238
2361
  cleanups.push(engine.settings.bindFrameCap((fps) => {
2239
2362
  engine.maxFps = fps;
@@ -2307,7 +2430,11 @@ async function createGame3D(opts) {
2307
2430
  }
2308
2431
  return {
2309
2432
  engine,
2310
- scene,
2433
+ get scene() {
2434
+ const live = engine.scene;
2435
+ if (!live) throw new IncantoError("TREE_VIOLATION", "game.scene: the engine has no scene.");
2436
+ return live;
2437
+ },
2311
2438
  sourceJson,
2312
2439
  renderer,
2313
2440
  physics,
@@ -2393,4 +2520,4 @@ function wireAudioUnlock(engine, root, canvas) {
2393
2520
  return detachAll;
2394
2521
  }
2395
2522
  //#endregion
2396
- export { Environment3D as a, syncTree as i, create_game_exports as n, setEnvironment3D as o, Renderer3D as r, AssetStore3D as s, createGame3D as t };
2523
+ 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-CmMuQ6CL.js";
1
+ import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-ZnfzKDo3.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,5 +1,6 @@
1
1
  import { t as jsonClone } from "./json-CwwhxQgb.js";
2
2
  import { s as mergeStaticProps } from "./registry-CF70EArN.js";
3
+ import { r as capturePointer } from "./touch-DEAmqGdf.js";
3
4
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
4
5
  //#region src/debug/capture.ts
5
6
  /**
@@ -369,7 +370,7 @@ var FloatingPanel = class {
369
370
  active = e.pointerId;
370
371
  lastX = e.clientX;
371
372
  lastY = e.clientY;
372
- grip.setPointerCapture?.(e.pointerId);
373
+ capturePointer(grip, e.pointerId);
373
374
  });
374
375
  grip.addEventListener("pointermove", (e) => {
375
376
  if (e.pointerId !== active) return;
@@ -565,7 +566,7 @@ function openRegionSelect(opts) {
565
566
  applyStyle(popup, { display: "none" });
566
567
  applyStyle(box, { cursor: next === "move" ? "grabbing" : "crosshair" });
567
568
  const id = event.pointerId;
568
- if (id !== void 0) layer.setPointerCapture?.(id);
569
+ if (id !== void 0) capturePointer(layer, id);
569
570
  render();
570
571
  }
571
572
  function render() {
@@ -1,4 +1,4 @@
1
- import { a as serializeNode, t as buildNodeJson } from "./loader-lQDCwNag.js";
1
+ import { a as serializeNode, t as buildNodeJson } from "./loader-KZESOZmE.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
package/dist/editor.js CHANGED
@@ -2380,7 +2380,8 @@ var Vn = [
2380
2380
  "connections",
2381
2381
  "input",
2382
2382
  "multiplayer",
2383
- "strings"
2383
+ "strings",
2384
+ "environment"
2384
2385
  ];
2385
2386
  function Hn(e, t, n) {
2386
2387
  e.appendChild(Tr("scene")), e.appendChild(Ar("name", String(t.working.name ?? ""), (e) => {
@@ -7842,6 +7843,10 @@ var ss = class {
7842
7843
  return !0;
7843
7844
  }) === !0;
7844
7845
  }
7846
+ resume(e) {
7847
+ let t = this.original;
7848
+ this.reset(e), this.original = t;
7849
+ }
7845
7850
  reset(e) {
7846
7851
  this.working = e, this.working.root && cs(this.working.root), this.original = JSON.stringify(this.working), this.undoStack = [], this.selection = [], this.emit();
7847
7852
  }
@@ -9794,7 +9799,7 @@ async function xc(e = {}) {
9794
9799
  let t = e.target.matches("input, textarea, select");
9795
9800
  (e.metaKey || e.ctrlKey) && e.key === "z" ? (e.preventDefault(), u.undo()) : (e.metaKey || e.ctrlKey) && e.key === "s" ? (e.preventDefault(), p.disabled || p.click()) : !t && e.code === "KeyF" ? P.zoomToFit() : !t && (e.code === "Digit0" || e.code === "Numpad0") ? P.gameView() : !t && e.code === "KeyW" ? P.setMode("move") : !t && e.code === "KeyE" ? P.setMode("rotate") : !t && e.code === "KeyR" && P.setMode("scale");
9796
9801
  };
9797
- return window.addEventListener("keydown", Me), n.push(() => window.removeEventListener("keydown", Me)), F(), e.camera && P.setEditCamera(e.camera.position, e.camera.target), kt.ready(C, w, o.version), {
9802
+ return window.addEventListener("keydown", Me), n.push(() => window.removeEventListener("keydown", Me)), F(), e.scene && u.resume(e.scene), e.camera && P.setEditCamera(e.camera.position, e.camera.target), kt.ready(C, w, o.version), {
9798
9803
  working: () => u.working,
9799
9804
  camera: () => P.editCamera(),
9800
9805
  showError: O,
@@ -1,11 +1,11 @@
1
- import { k as diagnose } from "./loader-lQDCwNag.js";
2
- import { L as translateOn, t as registerCoreNodes } from "./register-CgeR4ckF.js";
1
+ import { k as diagnose } from "./loader-KZESOZmE.js";
2
+ import { L as translateOn, t as registerCoreNodes } from "./register-Cs53CY7s.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
7
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
8
- import { B as CharacterBody3D, F as Water3D, H as RigidBody3D, J as colliderFootDrop, L as WaterCutout3D, R as rejectScale, U as StaticBody3D, V as PhysicsBody3D, W as Node3D, w as tolerateUnknownAction, z as Area3D } from "./gameplay-D-H986A4.js";
8
+ import { B as rejectScale, G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, X as colliderFootDrop, w as tolerateUnknownAction, z as WaterCutout3D } from "./gameplay-CGSv-9XR.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
10
10
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
11
11
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
@@ -387,6 +387,10 @@ var MeshInstance3D = class extends Node3D {
387
387
  validateMeshMaterial(node.material, node.name);
388
388
  }
389
389
  geometryKey = "";
390
+ materialKey = "";
391
+ /** True while this node is using a cache entry it must not dispose. */
392
+ sharedGeometry = false;
393
+ sharedMaterial = false;
390
394
  textureKey = "";
391
395
  loadedTextures = [];
392
396
  _createObject3D() {
@@ -395,12 +399,48 @@ var MeshInstance3D = class extends Node3D {
395
399
  _syncObject3D(alpha = 1, assets = null) {
396
400
  super._syncObject3D(alpha, assets);
397
401
  const m = this._ensureObject3D();
398
- const key = `${this.mesh}:${JSON.stringify(this.size)}`;
402
+ const key = `${this.mesh}:${this.size.join(",")}`;
399
403
  if (key !== this.geometryKey) {
400
- m.geometry.dispose();
401
- m.geometry = buildMeshGeometry(this.mesh, this.size);
404
+ if (!this.sharedGeometry) m.geometry.dispose();
405
+ const cached = assets?.meshGeometries.get(key);
406
+ if (cached) {
407
+ m.geometry = cached;
408
+ this.sharedGeometry = true;
409
+ } else {
410
+ const built = buildMeshGeometry(this.mesh, this.size);
411
+ m.geometry = built;
412
+ this.sharedGeometry = Boolean(assets);
413
+ assets?.meshGeometries.set(key, built);
414
+ }
402
415
  this.geometryKey = key;
403
416
  }
417
+ const look = JSON.stringify(this.material);
418
+ if (assets && look !== this.materialKey) {
419
+ const shared = assets.meshMaterials.get(look);
420
+ if (shared) {
421
+ if (!this.sharedMaterial) m.material.dispose();
422
+ m.material = shared;
423
+ this.sharedMaterial = true;
424
+ this.materialKey = look;
425
+ this.textureKey = "shared";
426
+ m.castShadow = this.castShadow;
427
+ m.receiveShadow = this.receiveShadow;
428
+ return;
429
+ }
430
+ if (this.sharedMaterial) {
431
+ m.material = new MeshPhysicalMaterial();
432
+ this.sharedMaterial = false;
433
+ this.textureKey = "";
434
+ }
435
+ this.materialKey = look;
436
+ const mine = m.material;
437
+ applyMeshMaterial(mine, this.material);
438
+ this.syncTextures(mine, assets);
439
+ assets.meshMaterials.set(look, mine);
440
+ m.castShadow = this.castShadow;
441
+ m.receiveShadow = this.receiveShadow;
442
+ return;
443
+ }
404
444
  const mat = m.material;
405
445
  applyMeshMaterial(mat, this.material);
406
446
  this.syncTextures(mat, assets);
@@ -742,7 +782,8 @@ var Joint3D = class extends Node3D {
742
782
  ] },
743
783
  length: { default: 0 },
744
784
  stiffness: { default: 50 },
745
- damping: { default: 5 }
785
+ damping: { default: 5 },
786
+ collide: { default: null }
746
787
  };
747
788
  type = "spherical";
748
789
  target = "";
@@ -760,6 +801,31 @@ var Joint3D = class extends Node3D {
760
801
  length = 0;
761
802
  stiffness = 50;
762
803
  damping = 5;
804
+ /**
805
+ * Do the two linked bodies still collide with EACH OTHER?
806
+ *
807
+ * `null` (default) decides from the joint type, because the two families want
808
+ * opposite answers:
809
+ *
810
+ * - **pivots** (`fixed`, `spherical`, `revolute`) → **off**. A hinge's bodies
811
+ * overlap at the pivot by construction, so the contact solver fights the
812
+ * joint and flings them. A puzzle builder's first hinge produced a bar that
813
+ * spun chaotically forever and threw a 40 kg box thirty metres, and they
814
+ * nearly filed "spherical joints inject energy":
815
+ *
816
+ * ```
817
+ * t= 0 Plank r=[0,0,0] Box [ 0.00, 5.50, 2.40]
818
+ * t=3000 Plank r=[-66,24,64] Box [-29.70, 0.40, -4.91]
819
+ * ```
820
+ *
821
+ * - **tethers** (`rope`, `spring`) → **on**. These anchor a thing to the world,
822
+ * and the thing has to rest on what it is tethered to. Defaulting these off
823
+ * drops a barrel roped to the floor straight through it — measured, in this
824
+ * repo's own conformance room.
825
+ *
826
+ * Set `true`/`false` to say it outright.
827
+ */
828
+ collide = null;
763
829
  onEnterTree() {
764
830
  if (!(this.parent instanceof PhysicsBody3D)) throw new IncantoError("TREE_VIOLATION", `Joint3D '${this.name}' must be a child of a physics body (its body A); parent is '${this.parent?.name ?? "none"}'.`);
765
831
  if (this.target === "") throw new IncantoError("BAD_FORMAT", `Joint3D '${this.name}' needs a "target" node path to the body it links to.`);
@@ -2641,6 +2707,7 @@ var CharacterController3D = class extends Node3D {
2641
2707
  /** Gravity multiplier while falling — >1 is the "snappy" arc. */
2642
2708
  fallGravity: { default: 2.5 },
2643
2709
  floatHeight: { default: .01 },
2710
+ stepHeight: { default: .35 },
2644
2711
  mouseLook: { default: true },
2645
2712
  zoomMin: { default: 0 },
2646
2713
  zoomMax: { default: 0 },
@@ -2673,6 +2740,25 @@ var CharacterController3D = class extends Node3D {
2673
2740
  airControl = .2;
2674
2741
  fallGravity = 2.5;
2675
2742
  floatHeight = .01;
2743
+ /**
2744
+ * How high a step the PLAYER walks up without jumping (0 = off).
2745
+ *
2746
+ * The hover spring holds the capsule a fixed distance above whatever is
2747
+ * DIRECTLY below it, so a vertical face stopped the player dead at about
2748
+ * 0.2 m — while `CharacterBody3D.stepHeight` let every enemy climb 0.35 m.
2749
+ * The docs said the player did not need this ("it floats over small ledges
2750
+ * already"); it floats over 18 cm ones.
2751
+ *
2752
+ * This probes just AHEAD of the capsule in the direction of travel: if the
2753
+ * ground there is higher, but by no more than `stepHeight`, the spring aims
2754
+ * at THAT surface and lifts the character onto it. A wall taller than the
2755
+ * step is not a step and still stops you.
2756
+ *
2757
+ * Do not reach for `floatHeight` instead. It does raise the character over
2758
+ * ledges, and it silently makes it WEIGHTLESS: at `floatHeight: 0.28` a
2759
+ * 200 kg character does not move a seesaw a 70 kg crate tips.
2760
+ */
2761
+ stepHeight = .35;
2676
2762
  mouseLook = true;
2677
2763
  zoomMin = 0;
2678
2764
  zoomMax = 0;
@@ -2803,8 +2889,26 @@ var CharacterController3D = class extends Node3D {
2803
2889
  0,
2804
2890
  -vel[2] * DRAG_DAMPING * mass
2805
2891
  ]);
2806
- if (toi !== null && toi < floatingDis * 2 && !this.jumping) {
2807
- const spring = SPRING_K * (floatingDis - toi) - vel[1] * SPRING_DAMPING;
2892
+ let springToi = toi;
2893
+ if (this.stepHeight > 0 && moving && toi !== null && !this.jumping && (vel[1] ?? 0) <= .5) {
2894
+ const ahead = radius + .12;
2895
+ const probeTop = originY + this.stepHeight + .05;
2896
+ const stepHit = physics.castRay([
2897
+ pos[0] + mx * ahead,
2898
+ probeTop,
2899
+ pos[2] + mz * ahead
2900
+ ], [
2901
+ 0,
2902
+ -1,
2903
+ 0
2904
+ ], this.stepHeight + .05 + rayLen, body);
2905
+ if (stepHit) {
2906
+ const rise = probeTop - stepHit.distance - (originY - toi);
2907
+ if (rise > .02 && rise <= this.stepHeight + .02) springToi = toi - rise;
2908
+ }
2909
+ }
2910
+ if (springToi !== null && springToi < floatingDis * 2 && !this.jumping) {
2911
+ const spring = SPRING_K * (floatingDis - springToi) - vel[1] * SPRING_DAMPING;
2808
2912
  body.applyImpulse([
2809
2913
  0,
2810
2914
  spring * mass,