incanto 0.12.0 → 0.13.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 (82) hide show
  1. package/README.md +22 -0
  2. package/bin/incanto-new.mjs +87 -0
  3. package/dist/2d.d.ts +2 -2
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +194 -71
  6. package/dist/3d.js +74 -4
  7. package/dist/{behavior-Bod1AyJS.d.ts → behavior-CPibUfnH.d.ts} +4 -2
  8. package/dist/{create-game-C_xf4gm9.js → create-game-Ct86zczq.js} +5 -5
  9. package/dist/{create-game-B2fewJUy.js → create-game-J6g4QInv.js} +5 -5
  10. package/dist/debug.d.ts +1 -1
  11. package/dist/{duplicate-BEvGBtb_.js → duplicate-BINwP0WG.js} +1 -1
  12. package/dist/{gameplay-Dn3yidJw.js → gameplay-sSqrUcnz.js} +75 -3
  13. package/dist/gameplay.d.ts +37 -2
  14. package/dist/gameplay.js +2 -2
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +4 -4
  17. package/dist/{loader-B4OEXDZ8.js → loader-BZqOKfI2.js} +3 -1
  18. package/dist/{loader-BcUDfNHn.d.ts → loader-DILt9PGC.d.ts} +1 -1
  19. package/dist/net.d.ts +1 -1
  20. package/dist/net.js +3 -3
  21. package/dist/{pathfinding-BSiXjd6W.d.ts → pathfinding-RWYkNKx9.d.ts} +1 -1
  22. package/dist/{physics-2d-B6D864oZ.js → physics-2d-BiIdl51r.js} +2 -2
  23. package/dist/{physics-3d-RLNaxRyw.js → physics-3d-DhaAaKJo.js} +2 -2
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-Cwc-yCYx.js → register-BFFE1Mh1.js} +2 -2
  27. package/dist/{register-ysLR3WQS.js → register-BFw4c83z.js} +508 -123
  28. package/dist/{register-BD6zgrv_.js → register-Djwckzgx.js} +2 -2
  29. package/dist/{register-DJZXxwAn.js → register-nObreUQR.js} +1 -1
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-D_4QY8Aq.js → agent8-BF8Jfbkl.js} +1 -1
  33. package/editor/assets/{index-s0DZuroe.js → index-DSq0gdhE.js} +74 -74
  34. package/editor/index.html +1 -1
  35. package/package.json +4 -2
  36. package/schemas/scene.schema.json +263 -0
  37. package/skills/incanto-3d-character.md +35 -0
  38. package/skills/incanto-building-3d-games.md +48 -0
  39. package/skills/incanto-gameplay-behaviors.md +23 -1
  40. package/skills/incanto-node-reference.md +51 -0
  41. package/templates-app/beacon-isle-3d/PROJECT/Context.md +44 -0
  42. package/templates-app/beacon-isle-3d/PROJECT/Requirements.md +16 -0
  43. package/templates-app/beacon-isle-3d/PROJECT/Status.md +38 -0
  44. package/templates-app/beacon-isle-3d/PROJECT/Structure.md +24 -0
  45. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +38 -0
  46. package/templates-app/beacon-isle-3d/generate-world.ts +814 -0
  47. package/templates-app/beacon-isle-3d/index.html +68 -0
  48. package/templates-app/beacon-isle-3d/package.json +26 -0
  49. package/templates-app/beacon-isle-3d/src/behaviors.ts +570 -0
  50. package/templates-app/beacon-isle-3d/src/game.scene.json +2992 -0
  51. package/templates-app/beacon-isle-3d/src/main.ts +33 -0
  52. package/templates-app/beacon-isle-3d/tsconfig.json +13 -0
  53. package/templates-app/beacon-isle-3d/verify.ts +285 -0
  54. package/templates-app/beacon-isle-3d/vite.config.ts +5 -0
  55. package/templates-app/tps-3d/PROJECT/Context.md +49 -0
  56. package/templates-app/tps-3d/PROJECT/Requirements.md +38 -0
  57. package/templates-app/tps-3d/PROJECT/Status.md +41 -0
  58. package/templates-app/tps-3d/PROJECT/Structure.md +44 -0
  59. package/templates-app/tps-3d/docs/project-3d-rules.md +38 -0
  60. package/templates-app/tps-3d/index.html +244 -0
  61. package/templates-app/tps-3d/package.json +25 -0
  62. package/templates-app/tps-3d/src/behaviors.ts +405 -0
  63. package/templates-app/tps-3d/src/game.scene.json +677 -0
  64. package/templates-app/tps-3d/src/main.ts +64 -0
  65. package/templates-app/tps-3d/tsconfig.json +13 -0
  66. package/templates-app/tps-3d/verify.ts +167 -0
  67. package/templates-app/tps-3d/vite.config.ts +5 -0
  68. package/templates-app/village-quest-3d/PROJECT/Context.md +39 -0
  69. package/templates-app/village-quest-3d/PROJECT/Requirements.md +36 -0
  70. package/templates-app/village-quest-3d/PROJECT/Status.md +26 -0
  71. package/templates-app/village-quest-3d/PROJECT/Structure.md +30 -0
  72. package/templates-app/village-quest-3d/docs/project-3d-rules.md +38 -0
  73. package/templates-app/village-quest-3d/index.html +68 -0
  74. package/templates-app/village-quest-3d/package.json +25 -0
  75. package/templates-app/village-quest-3d/src/behaviors.ts +529 -0
  76. package/templates-app/village-quest-3d/src/grove.scene.json +826 -0
  77. package/templates-app/village-quest-3d/src/main.ts +35 -0
  78. package/templates-app/village-quest-3d/src/quest.ts +18 -0
  79. package/templates-app/village-quest-3d/src/village.scene.json +680 -0
  80. package/templates-app/village-quest-3d/tsconfig.json +13 -0
  81. package/templates-app/village-quest-3d/verify.ts +254 -0
  82. package/templates-app/village-quest-3d/vite.config.ts +5 -0
@@ -1,12 +1,12 @@
1
- import { f as Node } from "./loader-B4OEXDZ8.js";
2
- import { t as registerCoreNodes } from "./register-DJZXxwAn.js";
1
+ import { f as Node } from "./loader-BZqOKfI2.js";
2
+ import { t as registerCoreNodes } from "./register-nObreUQR.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-BVJ2HbCn.js";
6
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
7
  import { i as applyParticlePreset, o as effectiveOrder, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CyUU7HVU.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CroQPEER.js";
9
- import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DepthTexture, DirectionalLight, DoubleSide, Euler, FloatType, Frustum, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, Object3D, PerspectiveCamera, Plane, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, Sphere, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector2, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
9
+ import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DepthTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, FloatType, Frustum, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, Object3D, PerspectiveCamera, Plane, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, Sphere, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector2, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
11
11
  //#region src/3d/camera-rig.ts
12
12
  const QUARTER_PITCH = Math.atan(1 / Math.SQRT2);
@@ -1950,8 +1950,8 @@ var BoneAttachment3D = class extends Node3D {
1950
1950
  boneObj.updateWorldMatrix(true, false);
1951
1951
  parent.updateWorldMatrix(true, false);
1952
1952
  obj.matrixAutoUpdate = false;
1953
- boneObj.matrixWorld.decompose(bonePosScratch, boneQuatScratch, boneScaleScratch);
1954
- boneScratch.compose(bonePosScratch, boneQuatScratch, unitScale);
1953
+ boneObj.matrixWorld.decompose(bonePosScratch$1, boneQuatScratch, boneScaleScratch);
1954
+ boneScratch.compose(bonePosScratch$1, boneQuatScratch, unitScale);
1955
1955
  obj.updateMatrix();
1956
1956
  offsetScratch.copy(obj.matrix);
1957
1957
  obj.matrix.copy(parent.matrixWorld).invert().multiply(boneScratch).multiply(offsetScratch);
@@ -1963,11 +1963,125 @@ var BoneAttachment3D = class extends Node3D {
1963
1963
  };
1964
1964
  const offsetScratch = new Matrix4();
1965
1965
  const boneScratch = new Matrix4();
1966
- const bonePosScratch = new Vector3();
1966
+ const bonePosScratch$1 = new Vector3();
1967
1967
  const boneQuatScratch = new Quaternion();
1968
1968
  const boneScaleScratch = new Vector3();
1969
1969
  const unitScale = new Vector3(1, 1, 1);
1970
1970
  //#endregion
1971
+ //#region src/3d/nodes/bone-look-at-3d.ts
1972
+ /**
1973
+ * Look-at IK for one bone — the character's head (or chest, or turret) turns
1974
+ * toward a target node ON TOP of whatever the animation is doing. Runs after
1975
+ * the mixer each rendered frame, blends in/out smoothly, and DISENGAGES when
1976
+ * the target leaves the comfort cone (no owl necks).
1977
+ *
1978
+ * { "name": "HeadTrack", "type": "BoneLookAt3D",
1979
+ * "props": { "target": "../Skin", "bone": "Head", "lookAt": "%Player" } }
1980
+ *
1981
+ * `forwardAxis` names the bone's local facing axis (mixamo heads face +z,
1982
+ * the default). Purely visual — headless it's a no-op.
1983
+ */
1984
+ var BoneLookAt3D = class extends Node3D {
1985
+ static typeName = "BoneLookAt3D";
1986
+ static props = {
1987
+ /** Node path to the ModelInstance3D whose bone to steer. */
1988
+ target: { default: "" },
1989
+ /** Bone name ("Head", "Neck", "Spine2" — Mixamo spellings matched). */
1990
+ bone: { default: "Head" },
1991
+ /** Node path to LOOK AT ('' = idle). */
1992
+ lookAt: { default: "" },
1993
+ /** Comfort cone half-angle, degrees — beyond it the head returns to the anim. */
1994
+ maxAngleDeg: { default: 75 },
1995
+ /** Blend weight 0..1 (how far toward the target the bone turns). */
1996
+ weight: { default: .85 },
1997
+ /** The bone's local facing axis. */
1998
+ forwardAxis: {
1999
+ default: "+z",
2000
+ options: [
2001
+ "+x",
2002
+ "-x",
2003
+ "+y",
2004
+ "-y",
2005
+ "+z",
2006
+ "-z"
2007
+ ]
2008
+ }
2009
+ };
2010
+ target = "";
2011
+ bone = "Head";
2012
+ lookAt = "";
2013
+ maxAngleDeg = 75;
2014
+ weight = .85;
2015
+ forwardAxis = "+z";
2016
+ boneObj = null;
2017
+ lookedUpFor = "";
2018
+ /** 0..1 engage blend — eases in/out so tracking never snaps. */
2019
+ engage = 0;
2020
+ /** Test hook. */
2021
+ get attachedBone() {
2022
+ return this.boneObj;
2023
+ }
2024
+ _onRender3D(_ctx) {
2025
+ if (this.target === "" || this.bone === "") return;
2026
+ const key = `${this.target} ${this.bone}`;
2027
+ if (this.lookedUpFor !== key || this.boneObj === null || !this.boneObj.parent) {
2028
+ this.lookedUpFor = key;
2029
+ this.boneObj = null;
2030
+ const source = this.getNodeOrNull(this.target);
2031
+ if (source && typeof source.findBone === "function") this.boneObj = source.findBone(this.bone);
2032
+ }
2033
+ const bone = this.boneObj;
2034
+ if (!bone?.parent) return;
2035
+ const targetNode = this.lookAt === "" ? null : this.getNodeOrNull(this.lookAt);
2036
+ let engaged = false;
2037
+ if (targetNode && Array.isArray(targetNode.position)) {
2038
+ bone.updateWorldMatrix(true, false);
2039
+ bone.getWorldPosition(bonePosScratch);
2040
+ targetScratch.set(targetNode.position[0] ?? 0, targetNode.position[1] ?? 0, targetNode.position[2] ?? 0);
2041
+ dirScratch$1.copy(targetScratch).sub(bonePosScratch);
2042
+ if (dirScratch$1.lengthSq() > 1e-6) {
2043
+ dirScratch$1.normalize();
2044
+ axisScratch.copy(AXES[this.forwardAxis] ?? PLUS_Z);
2045
+ animForwardScratch.copy(axisScratch).applyQuaternion(bone.getWorldQuaternion(quatScratch$2));
2046
+ if (animForwardScratch.angleTo(dirScratch$1) < this.maxAngleDeg * Math.PI / 180) {
2047
+ engaged = true;
2048
+ lookMatrixScratch.lookAt(bonePosScratch, targetScratch, UP$1);
2049
+ desiredScratch.setFromRotationMatrix(lookMatrixScratch);
2050
+ correctionScratch.setFromUnitVectors(axisScratch, MINUS_Z);
2051
+ desiredScratch.multiply(correctionScratch);
2052
+ bone.parent.getWorldQuaternion(parentScratch).invert();
2053
+ desiredScratch.premultiply(parentScratch);
2054
+ const k = this.weight * this.engage;
2055
+ bone.quaternion.slerp(desiredScratch, k);
2056
+ }
2057
+ }
2058
+ }
2059
+ this.engage += ((engaged ? 1 : 0) - this.engage) * ENGAGE_RATE;
2060
+ }
2061
+ };
2062
+ const ENGAGE_RATE = .18;
2063
+ const AXES = {
2064
+ "+x": new Vector3(1, 0, 0),
2065
+ "-x": new Vector3(-1, 0, 0),
2066
+ "+y": new Vector3(0, 1, 0),
2067
+ "-y": new Vector3(0, -1, 0),
2068
+ "+z": new Vector3(0, 0, 1),
2069
+ "-z": new Vector3(0, 0, -1)
2070
+ };
2071
+ const UP$1 = new Vector3(0, 1, 0);
2072
+ const PLUS_Z = new Vector3(0, 0, 1);
2073
+ const MINUS_Z = new Vector3(0, 0, -1);
2074
+ const bonePosScratch = new Vector3();
2075
+ const targetScratch = new Vector3();
2076
+ const dirScratch$1 = new Vector3();
2077
+ const axisScratch = new Vector3();
2078
+ const animForwardScratch = new Vector3();
2079
+ const lookMatrixScratch = new Matrix4();
2080
+ const desiredScratch = new Quaternion();
2081
+ const correctionScratch = new Quaternion();
2082
+ const parentScratch = new Quaternion();
2083
+ const quatScratch$2 = new Quaternion();
2084
+ //#endregion
1971
2085
  //#region src/3d/nodes/camera-3d.ts
1972
2086
  /**
1973
2087
  * A perspective camera. Mark exactly one camera `current: true`; with none
@@ -2022,7 +2136,7 @@ const CAM_GROUND_CLEARANCE = .3;
2022
2136
  * near-plane band around the boom (the Emberwood hedge-maze lesson). */
2023
2137
  const CAM_PROBE_RADIUS = .25;
2024
2138
  const eulerScratch = new Euler();
2025
- const quatScratch = new Quaternion();
2139
+ const quatScratch$1 = new Quaternion();
2026
2140
  const matrixScratch$3 = new Matrix4();
2027
2141
  const camVecScratch = new Vector3();
2028
2142
  const eyeVecScratch = new Vector3();
@@ -2309,12 +2423,12 @@ var CharacterController3D = class extends Node3D {
2309
2423
  camVecScratch.set(camera.position[0] ?? 0, camera.position[1] ?? 0, camera.position[2] ?? 0);
2310
2424
  eyeVecScratch.set(eye[0], eye[1], eye[2]);
2311
2425
  matrixScratch$3.lookAt(camVecScratch, eyeVecScratch, UP_VEC);
2312
- quatScratch.setFromRotationMatrix(matrixScratch$3);
2313
- eulerScratch.setFromQuaternion(quatScratch, "XYZ");
2426
+ quatScratch$1.setFromRotationMatrix(matrixScratch$3);
2427
+ eulerScratch.setFromQuaternion(quatScratch$1, "XYZ");
2314
2428
  } else {
2315
2429
  eulerScratch.set(pose.rotation[0], pose.rotation[1], pose.rotation[2], "YXZ");
2316
- quatScratch.setFromEuler(eulerScratch);
2317
- eulerScratch.setFromQuaternion(quatScratch, "XYZ");
2430
+ quatScratch$1.setFromEuler(eulerScratch);
2431
+ eulerScratch.setFromQuaternion(quatScratch$1, "XYZ");
2318
2432
  }
2319
2433
  camera.rotation = [
2320
2434
  eulerScratch.x * RAD2DEG,
@@ -3110,8 +3224,8 @@ var Flowers3D = class extends Node3D {
3110
3224
  const p = plants[i];
3111
3225
  positionScratch$2.set(p.x, this.bedY(p.x, p.z), p.z);
3112
3226
  quaternionScratch$2.setFromAxisAngle(Y_AXIS$2, p.rotY);
3113
- scaleScratch$2.setScalar(this.height * p.scale);
3114
- matrixScratch$2.compose(positionScratch$2, quaternionScratch$2, scaleScratch$2);
3227
+ scaleScratch$3.setScalar(this.height * p.scale);
3228
+ matrixScratch$2.compose(positionScratch$2, quaternionScratch$2, scaleScratch$3);
3115
3229
  structureMesh.setMatrixAt(i, matrixScratch$2);
3116
3230
  petalMesh.setMatrixAt(i, matrixScratch$2);
3117
3231
  const pick = Math.min(Math.floor(p.color01 * palette.length), palette.length - 1);
@@ -3183,7 +3297,7 @@ const Y_AXIS$2 = new Vector3(0, 1, 0);
3183
3297
  const matrixScratch$2 = new Matrix4();
3184
3298
  const positionScratch$2 = new Vector3();
3185
3299
  const quaternionScratch$2 = new Quaternion();
3186
- const scaleScratch$2 = new Vector3();
3300
+ const scaleScratch$3 = new Vector3();
3187
3301
  const colorScratch$1 = new Color();
3188
3302
  /**
3189
3303
  * Nearest-k benders whose influence circle touches the field rectangle,
@@ -4672,8 +4786,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4672
4786
  const bladeHeight = this.height * regional * (.55 + p.scale * .6);
4673
4787
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4674
4788
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4675
- scaleScratch$1.set(widthJitter, bladeHeight, widthJitter);
4676
- mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4789
+ scaleScratch$2.set(widthJitter, bladeHeight, widthJitter);
4790
+ mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4677
4791
  const leanDir = clump.leanDir + rng.range(-.6, .6);
4678
4792
  const lean = (MESH_LEAN_BASE + MESH_LEAN_RANGE * (.5 * clump.lean + .5 * rng.next())) * (1 + MESH_TALL_LEAN_BOOST * tall);
4679
4793
  blade[i * 4] = Math.cos(leanDir) * lean;
@@ -4711,8 +4825,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4711
4825
  const stem = this.height * regional * (.85 + rng.next() * .35);
4712
4826
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4713
4827
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4714
- scaleScratch$1.set(1, stem, 1);
4715
- mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4828
+ scaleScratch$2.set(1, stem, 1);
4829
+ mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4716
4830
  const pick = rng.next();
4717
4831
  const color = FLOWER_COLORS[pick < .45 ? 0 : pick < .75 ? 1 : 2];
4718
4832
  mesh.setColorAt(i, colorScratchC$1.set(color));
@@ -4764,8 +4878,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4764
4878
  const cardW = cardH * TUFT_ASPECT * rng.range(TUFT_WIDTH_JITTER[0], TUFT_WIDTH_JITTER[1]);
4765
4879
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4766
4880
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4767
- scaleScratch$1.set(cardW, cardH, cardW);
4768
- mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4881
+ scaleScratch$2.set(cardW, cardH, cardW);
4882
+ mesh.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4769
4883
  const clump = clumpAt(p.x, p.z);
4770
4884
  const t = Math.min(1, Math.max(0, .5 + .35 * clump.hue + (rng.next() - .5) * .3));
4771
4885
  const dry = Math.min(1, Math.max(0, dryFieldAt(p.x, p.z, this.seed) * .8 - .1));
@@ -4822,11 +4936,11 @@ var Foliage3D = class Foliage3D extends Node3D {
4822
4936
  for (let i = 0; i < count; i++) {
4823
4937
  const p = placements[i];
4824
4938
  positionScratch$1.set(p.x, this.bedY(p.x, p.z), p.z);
4825
- scaleScratch$1.setScalar(p.scale);
4939
+ scaleScratch$2.setScalar(p.scale);
4826
4940
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY);
4827
- first.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4941
+ first.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4828
4942
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, p.rotY + Math.PI / 2);
4829
- second.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1));
4943
+ second.setMatrixAt(i, matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2));
4830
4944
  }
4831
4945
  first.instanceMatrix.needsUpdate = true;
4832
4946
  second.instanceMatrix.needsUpdate = true;
@@ -4851,8 +4965,8 @@ var Foliage3D = class Foliage3D extends Node3D {
4851
4965
  const sz = rng.range(-halfZ, halfZ);
4852
4966
  positionScratch$1.set(sx, this.bedY(sx, sz), sz);
4853
4967
  quaternionScratch$1.setFromAxisAngle(Y_AXIS$1, rng.range(0, Math.PI * 2));
4854
- scaleScratch$1.set(1, this.height * rng.range(HEIGHT_JITTER$1[0], HEIGHT_JITTER$1[1]), 1);
4855
- matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$1);
4968
+ scaleScratch$2.set(1, this.height * rng.range(HEIGHT_JITTER$1[0], HEIGHT_JITTER$1[1]), 1);
4969
+ matrixScratch$1.compose(positionScratch$1, quaternionScratch$1, scaleScratch$2);
4856
4970
  mesh.setMatrixAt(i, matrixScratch$1);
4857
4971
  mesh.setColorAt(i, colorScratchC$1.copy(colorA).lerp(colorB, rng.next()));
4858
4972
  }
@@ -5001,88 +5115,12 @@ const Y_AXIS$1 = new Vector3(0, 1, 0);
5001
5115
  const matrixScratch$1 = new Matrix4();
5002
5116
  const positionScratch$1 = new Vector3();
5003
5117
  const quaternionScratch$1 = new Quaternion();
5004
- const scaleScratch$1 = new Vector3();
5118
+ const scaleScratch$2 = new Vector3();
5005
5119
  const colorScratchA$1 = new Color();
5006
5120
  const colorScratchB$1 = new Color();
5007
5121
  const colorScratchC$1 = new Color();
5008
5122
  const tuftDryScratch = new Color();
5009
5123
  //#endregion
5010
- //#region src/3d/nodes/lights-3d.ts
5011
- /** Sun-style light. Direction comes from the node's rotation. */
5012
- var DirectionalLight3D = class extends Node3D {
5013
- static typeName = "DirectionalLight3D";
5014
- static props = {
5015
- color: { default: "#ffffff" },
5016
- intensity: { default: 1 },
5017
- castShadow: { default: false },
5018
- shadowArea: { default: 75 },
5019
- shadowMapSize: { default: 2048 },
5020
- shadowFollowsCamera: { default: false }
5021
- };
5022
- color = "#ffffff";
5023
- intensity = 1;
5024
- castShadow = false;
5025
- /** Half-extent of the directional shadow frustum (template default ±75). */
5026
- shadowArea = 75;
5027
- shadowMapSize = 2048;
5028
- /**
5029
- * Re-center the shadow frustum on the camera every frame. A directional sun's
5030
- * shadow map covers a fixed ortho box; in a big roaming world that box must be
5031
- * huge (and so low-resolution that the player's own shadow nearly vanishes).
5032
- * With this on, a SMALL `shadowArea` tracks the camera, so contact shadows stay
5033
- * crisp wherever you go. The renderer supplies the camera position.
5034
- */
5035
- shadowFollowsCamera = false;
5036
- _createObject3D() {
5037
- return new DirectionalLight();
5038
- }
5039
- _syncObject3D() {
5040
- super._syncObject3D();
5041
- const light = this._ensureObject3D();
5042
- light.color.set(this.color);
5043
- light.intensity = this.intensity;
5044
- light.castShadow = this.castShadow;
5045
- if (this.castShadow && "shadow" in light) {
5046
- const cam = light.shadow.camera;
5047
- if ("left" in cam) {
5048
- cam.left = -this.shadowArea;
5049
- cam.right = this.shadowArea;
5050
- cam.top = this.shadowArea;
5051
- cam.bottom = -this.shadowArea;
5052
- cam.near = .5;
5053
- cam.far = 500;
5054
- cam.updateProjectionMatrix();
5055
- }
5056
- light.shadow.mapSize.set(this.shadowMapSize, this.shadowMapSize);
5057
- light.shadow.bias = -2e-4;
5058
- light.shadow.normalBias = .002;
5059
- }
5060
- }
5061
- };
5062
- /** Point light with a range (Godot's OmniLight3D). */
5063
- var OmniLight3D = class extends Node3D {
5064
- static typeName = "OmniLight3D";
5065
- static props = {
5066
- color: { default: "#ffffff" },
5067
- intensity: { default: 1 },
5068
- range: { default: 0 }
5069
- };
5070
- color = "#ffffff";
5071
- intensity = 1;
5072
- /** 0 = unlimited (three `distance` semantics). */
5073
- range = 0;
5074
- _createObject3D() {
5075
- return new PointLight();
5076
- }
5077
- _syncObject3D() {
5078
- super._syncObject3D();
5079
- const light = this._ensureObject3D();
5080
- light.color.set(this.color);
5081
- light.intensity = this.intensity;
5082
- light.distance = this.range;
5083
- }
5084
- };
5085
- //#endregion
5086
5124
  //#region src/3d/nodes/mesh-instance-3d.ts
5087
5125
  /** Every key `material` accepts — validation lists these on a typo. */
5088
5126
  const MATERIAL_KEYS = [
@@ -5165,7 +5203,7 @@ var MeshInstance3D = class extends Node3D {
5165
5203
  const key = `${this.mesh}:${JSON.stringify(this.size)}`;
5166
5204
  if (key !== this.geometryKey) {
5167
5205
  m.geometry.dispose();
5168
- m.geometry = buildGeometry(this.mesh, this.size);
5206
+ m.geometry = buildMeshGeometry(this.mesh, this.size);
5169
5207
  this.geometryKey = key;
5170
5208
  }
5171
5209
  const mat = m.material;
@@ -5284,7 +5322,8 @@ function validateMeshMaterial(material, name, typeName = "MeshInstance3D") {
5284
5322
  if (value !== void 0 && typeof value !== "boolean") fail(`material.${key} must be a boolean, got ${JSON.stringify(value)}.`);
5285
5323
  }
5286
5324
  }
5287
- function buildGeometry(kind, size) {
5325
+ /** Geometry factory shared by MeshInstance3D and InstancedMesh3D. */
5326
+ function buildMeshGeometry(kind, size) {
5288
5327
  const x = size[0] ?? 1;
5289
5328
  const y = size[1] ?? 1;
5290
5329
  const z = size[2] ?? 1;
@@ -5303,6 +5342,206 @@ function buildGeometry(kind, size) {
5303
5342
  }
5304
5343
  }
5305
5344
  //#endregion
5345
+ //#region src/3d/nodes/instanced-mesh-3d.ts
5346
+ /**
5347
+ * Hundreds of copies of one mesh in ONE draw call — scattered rocks, fence
5348
+ * posts, pillars, crates, gravestones. Same `mesh`/`size`/`material` surface
5349
+ * as MeshInstance3D; `transforms` places the copies:
5350
+ *
5351
+ * { "name": "Rocks", "type": "InstancedMesh3D",
5352
+ * "props": { "mesh": "gem", "size": [0.7, 0.5, 0.7],
5353
+ * "material": { "color": "#8a8f98", "roughness": 0.9, "flatShading": true },
5354
+ * "transforms": [[12, 0.2, -8, 40, 1.3], [-6, 0.1, 14, 210, 0.8]] } }
5355
+ *
5356
+ * Each row is `[x, y, z, yawDeg?, scale?]` (yaw 0 and scale 1 when omitted) —
5357
+ * dense enough to generate hundreds inline. REPLACE the array to update
5358
+ * (mutations are not watched).
5359
+ */
5360
+ var InstancedMesh3D = class extends Node3D {
5361
+ static typeName = "InstancedMesh3D";
5362
+ static props = {
5363
+ mesh: {
5364
+ default: "box",
5365
+ options: [
5366
+ "box",
5367
+ "sphere",
5368
+ "capsule",
5369
+ "plane",
5370
+ "cylinder",
5371
+ "gem"
5372
+ ]
5373
+ },
5374
+ size: { default: [
5375
+ 1,
5376
+ 1,
5377
+ 1
5378
+ ] },
5379
+ material: { default: {} },
5380
+ /** `[x, y, z, yawDeg?, scale?]` per instance. */
5381
+ transforms: { default: [] },
5382
+ castShadow: { default: false },
5383
+ receiveShadow: { default: false }
5384
+ };
5385
+ mesh = "box";
5386
+ size = [
5387
+ 1,
5388
+ 1,
5389
+ 1
5390
+ ];
5391
+ material = {};
5392
+ castShadow = false;
5393
+ receiveShadow = false;
5394
+ _transforms = [];
5395
+ transformsDirty = true;
5396
+ get transforms() {
5397
+ return this._transforms;
5398
+ }
5399
+ set transforms(value) {
5400
+ this._transforms = value;
5401
+ this.transformsDirty = true;
5402
+ }
5403
+ instanced = null;
5404
+ builtKey = "";
5405
+ static validateJson(node) {
5406
+ const self = node;
5407
+ validateMeshMaterial(self.material, node.name);
5408
+ for (const [i, row] of self._transforms.entries()) if (!Array.isArray(row) || row.length < 3 || row.length > 5 || row.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `InstancedMesh3D '${node.name}': transforms[${i}] must be [x, y, z, yawDeg?, scale?] (numbers), got ${JSON.stringify(row)}.`);
5409
+ }
5410
+ /** Test hook: the live instance count. */
5411
+ get instanceCount() {
5412
+ return this.instanced?.count ?? 0;
5413
+ }
5414
+ _syncObject3D(alpha) {
5415
+ super._syncObject3D(alpha);
5416
+ const root = this._ensureObject3D();
5417
+ const key = `${this.mesh} ${JSON.stringify(this.size)} ${this._transforms.length}`;
5418
+ if (key !== this.builtKey) {
5419
+ this.builtKey = key;
5420
+ if (this.instanced) {
5421
+ root.remove(this.instanced);
5422
+ this.instanced.geometry.dispose();
5423
+ this.instanced.material.dispose();
5424
+ this.instanced = null;
5425
+ }
5426
+ if (this._transforms.length > 0) {
5427
+ const geometry = buildMeshGeometry(this.mesh, this.size);
5428
+ const material = new MeshPhysicalMaterial();
5429
+ this.instanced = new InstancedMesh(geometry, material, this._transforms.length);
5430
+ this.instanced.instanceMatrix.setUsage(DynamicDrawUsage);
5431
+ root.add(this.instanced);
5432
+ }
5433
+ this.transformsDirty = true;
5434
+ }
5435
+ const inst = this.instanced;
5436
+ if (!inst) return;
5437
+ inst.castShadow = this.castShadow;
5438
+ inst.receiveShadow = this.receiveShadow;
5439
+ applyMeshMaterial(inst.material, this.material);
5440
+ if (this.transformsDirty) {
5441
+ this.transformsDirty = false;
5442
+ for (let i = 0; i < this._transforms.length; i++) {
5443
+ const row = this._transforms[i];
5444
+ posScratch.set(row[0] ?? 0, row[1] ?? 0, row[2] ?? 0);
5445
+ quatScratch.setFromAxisAngle(UP, (row[3] ?? 0) * Math.PI / 180);
5446
+ const s = row[4] ?? 1;
5447
+ scaleScratch$1.set(s, s, s);
5448
+ matScratch.compose(posScratch, quatScratch, scaleScratch$1);
5449
+ inst.setMatrixAt(i, matScratch);
5450
+ }
5451
+ inst.instanceMatrix.needsUpdate = true;
5452
+ inst.computeBoundingSphere();
5453
+ }
5454
+ }
5455
+ free() {
5456
+ if (this.instanced) {
5457
+ this.instanced.geometry.dispose();
5458
+ this.instanced.material.dispose();
5459
+ }
5460
+ super.free();
5461
+ }
5462
+ };
5463
+ const UP = new Vector3(0, 1, 0);
5464
+ const posScratch = new Vector3();
5465
+ const quatScratch = new Quaternion();
5466
+ const scaleScratch$1 = new Vector3();
5467
+ const matScratch = new Matrix4();
5468
+ //#endregion
5469
+ //#region src/3d/nodes/lights-3d.ts
5470
+ /** Sun-style light. Direction comes from the node's rotation. */
5471
+ var DirectionalLight3D = class extends Node3D {
5472
+ static typeName = "DirectionalLight3D";
5473
+ static props = {
5474
+ color: { default: "#ffffff" },
5475
+ intensity: { default: 1 },
5476
+ castShadow: { default: false },
5477
+ shadowArea: { default: 75 },
5478
+ shadowMapSize: { default: 2048 },
5479
+ shadowFollowsCamera: { default: false }
5480
+ };
5481
+ color = "#ffffff";
5482
+ intensity = 1;
5483
+ castShadow = false;
5484
+ /** Half-extent of the directional shadow frustum (template default ±75). */
5485
+ shadowArea = 75;
5486
+ shadowMapSize = 2048;
5487
+ /**
5488
+ * Re-center the shadow frustum on the camera every frame. A directional sun's
5489
+ * shadow map covers a fixed ortho box; in a big roaming world that box must be
5490
+ * huge (and so low-resolution that the player's own shadow nearly vanishes).
5491
+ * With this on, a SMALL `shadowArea` tracks the camera, so contact shadows stay
5492
+ * crisp wherever you go. The renderer supplies the camera position.
5493
+ */
5494
+ shadowFollowsCamera = false;
5495
+ _createObject3D() {
5496
+ return new DirectionalLight();
5497
+ }
5498
+ _syncObject3D() {
5499
+ super._syncObject3D();
5500
+ const light = this._ensureObject3D();
5501
+ light.color.set(this.color);
5502
+ light.intensity = this.intensity;
5503
+ light.castShadow = this.castShadow;
5504
+ if (this.castShadow && "shadow" in light) {
5505
+ const cam = light.shadow.camera;
5506
+ if ("left" in cam) {
5507
+ cam.left = -this.shadowArea;
5508
+ cam.right = this.shadowArea;
5509
+ cam.top = this.shadowArea;
5510
+ cam.bottom = -this.shadowArea;
5511
+ cam.near = .5;
5512
+ cam.far = 500;
5513
+ cam.updateProjectionMatrix();
5514
+ }
5515
+ light.shadow.mapSize.set(this.shadowMapSize, this.shadowMapSize);
5516
+ light.shadow.bias = -2e-4;
5517
+ light.shadow.normalBias = .002;
5518
+ }
5519
+ }
5520
+ };
5521
+ /** Point light with a range (Godot's OmniLight3D). */
5522
+ var OmniLight3D = class extends Node3D {
5523
+ static typeName = "OmniLight3D";
5524
+ static props = {
5525
+ color: { default: "#ffffff" },
5526
+ intensity: { default: 1 },
5527
+ range: { default: 0 }
5528
+ };
5529
+ color = "#ffffff";
5530
+ intensity = 1;
5531
+ /** 0 = unlimited (three `distance` semantics). */
5532
+ range = 0;
5533
+ _createObject3D() {
5534
+ return new PointLight();
5535
+ }
5536
+ _syncObject3D() {
5537
+ super._syncObject3D();
5538
+ const light = this._ensureObject3D();
5539
+ light.color.set(this.color);
5540
+ light.intensity = this.intensity;
5541
+ light.distance = this.range;
5542
+ }
5543
+ };
5544
+ //#endregion
5306
5545
  //#region src/3d/nodes/loft-mesh-3d.ts
5307
5546
  /**
5308
5547
  * A smooth swept hull ("loft"): superellipse cross-sections interpolated
@@ -5482,6 +5721,42 @@ function buildLoftGeometry(sections, slices, smooth) {
5482
5721
  return geo;
5483
5722
  }
5484
5723
  //#endregion
5724
+ //#region src/3d/animation/layering.ts
5725
+ /**
5726
+ * Clip masking for two-layer animation (upper-body attack over lower-body
5727
+ * locomotion). three has no per-bone action weights, so layering is done at
5728
+ * the CLIP level: the upper action gets only the tracks under the layer root
5729
+ * (e.g. Spine), and the base action gets everything else — no track overlap,
5730
+ * no half-blended bones.
5731
+ */
5732
+ /** Bone name a track drives — "mixamorigSpine.quaternion" → "mixamorigSpine". */
5733
+ function trackBone(trackName) {
5734
+ const dot = trackName.lastIndexOf(".");
5735
+ return dot === -1 ? trackName : trackName.slice(0, dot);
5736
+ }
5737
+ /**
5738
+ * A copy of `clip` with only the tracks whose bone IS (mode 'keep') or is
5739
+ * NOT (mode 'drop') in `bones`. Track arrays are shared (cheap); the clip
5740
+ * duration is preserved so layered actions stay time-aligned.
5741
+ */
5742
+ function maskClip(clip, bones, mode) {
5743
+ const tracks = clip.tracks.filter((t) => {
5744
+ const inSet = bones.has(trackBone(t.name));
5745
+ return mode === "keep" ? inSet : !inSet;
5746
+ });
5747
+ return new AnimationClip(`${clip.name}#${mode}`, clip.duration, tracks);
5748
+ }
5749
+ /** Collect the bone-name subtree from `root` (inclusive) — the layer mask. */
5750
+ function boneSubtree(root) {
5751
+ const out = /* @__PURE__ */ new Set();
5752
+ const walk = (o) => {
5753
+ out.add(o.name);
5754
+ for (const c of o.children) walk(c);
5755
+ };
5756
+ walk(root);
5757
+ return out;
5758
+ }
5759
+ //#endregion
5485
5760
  //#region src/3d/animation/rigmap.ts
5486
5761
  /**
5487
5762
  * Mixamo rig name → VRM humanoid bone name (the vibe-starter-3d /
@@ -5615,6 +5890,8 @@ function retargetClipToVrm(clip, sourceScene, vrm) {
5615
5890
  //#region src/3d/nodes/model-instance-3d.ts
5616
5891
  /** Crossfade duration (s) between animation clips — smooths idle↔walk↔run↔jump. */
5617
5892
  const ANIM_CROSSFADE = .2;
5893
+ /** Upper-layer fade — snappier than locomotion blends (attacks must feel instant). */
5894
+ const UPPER_FADE = .12;
5618
5895
  /**
5619
5896
  * A GLB/glTF/VRM model. `model` is a `$key` into the scene's assets header
5620
5897
  * ({type:"model", url}) or a direct URL. `targetHeight` (>0) uniformly scales
@@ -5637,6 +5914,12 @@ var ModelInstance3D = class extends Node3D {
5637
5914
  targetHeight: { default: 0 },
5638
5915
  castShadow: { default: false },
5639
5916
  animationLoop: { default: true },
5917
+ /** Upper-body layer clip ('' = off). One-shots auto-clear on finish. */
5918
+ animationUpper: { default: "" },
5919
+ /** Loop the upper layer (false = one-shot: attack, wave, throw). */
5920
+ animationUpperLoop: { default: false },
5921
+ /** Bone whose subtree the upper layer drives (forgiving lookup). */
5922
+ upperBodyRoot: { default: "Spine" },
5640
5923
  receiveShadow: { default: false },
5641
5924
  tint: { default: "" },
5642
5925
  metalness: { default: -1 },
@@ -5648,6 +5931,16 @@ var ModelInstance3D = class extends Node3D {
5648
5931
  castShadow = false;
5649
5932
  /** false = play the clip once, then emit `animationFinished`. */
5650
5933
  animationLoop = true;
5934
+ /**
5935
+ * Two-layer animation: play this clip on the UPPER BODY ONLY (the
5936
+ * `upperBodyRoot` bone subtree) while `animation` keeps driving the rest —
5937
+ * attack while running, wave while walking. One-shot by default
5938
+ * (`animationUpperLoop: false`): finishes, emits `animationFinished` with
5939
+ * this key, and auto-clears back to ''.
5940
+ */
5941
+ animationUpper = "";
5942
+ animationUpperLoop = false;
5943
+ upperBodyRoot = "Spine";
5651
5944
  receiveShadow = false;
5652
5945
  /** Hex colour multiplied into every material — RESKIN one shared GLB into many
5653
5946
  * variants (e.g. a sickly-green zombie from the base human). '' = untinted. */
@@ -5666,6 +5959,12 @@ var ModelInstance3D = class extends Node3D {
5666
5959
  mixer = null;
5667
5960
  playing = "";
5668
5961
  currentAction = null;
5962
+ playingUpper = "";
5963
+ upperAction = null;
5964
+ /** Which variant the BASE action is playing ('lower' while a layer is on). */
5965
+ baseMode = "full";
5966
+ /** Masked-clip cache: source clip uuid + layer root → {upper, lower}. */
5967
+ maskCache = /* @__PURE__ */ new Map();
5669
5968
  /** action → the `animation` key that played it, so `animationFinished` reports
5670
5969
  * the clip that ACTUALLY finished (a crossfaded-away one-shot can finish a few
5671
5970
  * frames after `this.animation` already moved on). */
@@ -5759,6 +6058,12 @@ var ModelInstance3D = class extends Node3D {
5759
6058
  this.mixer.addEventListener("finished", (e) => {
5760
6059
  const action = e.action;
5761
6060
  const key = (action && this.actionKeys.get(action)) ?? this.animation;
6061
+ if (action && action === this.upperAction) {
6062
+ action.fadeOut(UPPER_FADE);
6063
+ this.upperAction = null;
6064
+ this.playingUpper = "";
6065
+ this.animationUpper = "";
6066
+ }
5762
6067
  this.emit("animationFinished", key);
5763
6068
  });
5764
6069
  this.playing = "";
@@ -5801,6 +6106,7 @@ var ModelInstance3D = class extends Node3D {
5801
6106
  this.applyMaterialMods();
5802
6107
  this.applyPoses();
5803
6108
  this.syncAnimation(assets);
6109
+ this.syncUpperAnimation(assets);
5804
6110
  }
5805
6111
  /** Apply per-instance material mods — `tint` (multiplied into colour) and the
5806
6112
  * `metalness`/`roughness` overrides — by cloning the captured originals
@@ -5844,33 +6150,68 @@ var ModelInstance3D = class extends Node3D {
5844
6150
  if (obj) obj.rotation.set(rot[0] * DEG, rot[1] * DEG, rot[2] * DEG);
5845
6151
  }
5846
6152
  }
6153
+ /** Resolve a clip ref: embedded name → '$asset' / URL (VRM-retargeted when
6154
+ * needed). 'loading' = asset still fetching, try again next frame. */
6155
+ resolveClip(ref, assets) {
6156
+ if (!this.entry) return null;
6157
+ const embedded = this.entry.animations.find((c) => c.name === ref);
6158
+ if (embedded) return embedded;
6159
+ if (!(ref.startsWith("$") || /\.(glb|gltf)$/i.test(ref))) return null;
6160
+ const anim = assets.getAnimation(ref);
6161
+ if (anim.status === "loading") return "loading";
6162
+ if (anim.status !== "ready" || !anim.scene) return null;
6163
+ const source = (anim.clip ? anim.clips.find((c) => c.name === anim.clip) : void 0) ?? anim.clips[0];
6164
+ if (!source) return null;
6165
+ if (this.entry.isVrm && this.entry.vrm) {
6166
+ const cached = this.entry.retargeted.get(ref);
6167
+ if (cached) return cached;
6168
+ const result = retargetClipToVrm(source, anim.scene, this.entry.vrm);
6169
+ if (result.skippedBones.length > 0) console.warn(`incanto: retarget '${ref}' skipped bones: ${result.skippedBones.join(", ")}`);
6170
+ this.entry.retargeted.set(ref, result.clip);
6171
+ return result.clip;
6172
+ }
6173
+ return source;
6174
+ }
6175
+ /** Masked upper/lower variants of a clip for two-layer playback (cached). */
6176
+ layerVariants(clip) {
6177
+ const rootBone = this.findBone(this.upperBodyRoot);
6178
+ if (!rootBone) return null;
6179
+ const key = `${clip.uuid} ${rootBone.name}`;
6180
+ let v = this.maskCache.get(key);
6181
+ if (!v) {
6182
+ const bones = boneSubtree(rootBone);
6183
+ v = {
6184
+ upper: maskClip(clip, bones, "keep"),
6185
+ lower: maskClip(clip, bones, "drop")
6186
+ };
6187
+ this.maskCache.set(key, v);
6188
+ }
6189
+ return v;
6190
+ }
6191
+ upperLayerActive() {
6192
+ return this.animationUpper !== "" || this.upperAction !== null;
6193
+ }
5847
6194
  syncAnimation(assets) {
5848
- if (!this.mixer || !this.entry || this.playing === this.animation) return;
6195
+ if (!this.mixer || !this.entry) return;
6196
+ const mode = this.upperLayerActive() ? "lower" : "full";
6197
+ if (this.playing === this.animation && this.baseMode === mode) return;
5849
6198
  if (this.animation === "") {
5850
6199
  if (this.currentAction) this.currentAction.fadeOut(ANIM_CROSSFADE);
5851
6200
  this.currentAction = null;
5852
6201
  this.playing = "";
6202
+ this.baseMode = mode;
5853
6203
  return;
5854
6204
  }
5855
- let clip = this.entry.animations.find((c) => c.name === this.animation) ?? null;
5856
- if (!clip && (this.animation.startsWith("$") || /\.(glb|gltf)$/i.test(this.animation))) {
5857
- const anim = assets.getAnimation(this.animation);
5858
- if (anim.status === "loading") return;
5859
- if (anim.status === "ready" && anim.scene) {
5860
- const source = (anim.clip ? anim.clips.find((c) => c.name === anim.clip) : void 0) ?? anim.clips[0];
5861
- if (source) if (this.entry.isVrm && this.entry.vrm) {
5862
- const cached = this.entry.retargeted.get(this.animation);
5863
- if (cached) clip = cached;
5864
- else {
5865
- const result = retargetClipToVrm(source, anim.scene, this.entry.vrm);
5866
- if (result.skippedBones.length > 0) console.warn(`incanto: retarget '${this.animation}' skipped bones: ${result.skippedBones.join(", ")}`);
5867
- this.entry.retargeted.set(this.animation, result.clip);
5868
- clip = result.clip;
5869
- }
5870
- } else clip = source;
5871
- }
6205
+ const resolved = this.resolveClip(this.animation, assets);
6206
+ if (resolved === "loading") return;
6207
+ let clip = resolved;
6208
+ if (clip && mode === "lower") {
6209
+ const variants = this.layerVariants(clip);
6210
+ if (variants) clip = variants.lower;
5872
6211
  }
6212
+ const samePlaying = this.playing === this.animation;
5873
6213
  this.playing = this.animation;
6214
+ this.baseMode = mode;
5874
6215
  if (clip) {
5875
6216
  const action = this.mixer.clipAction(clip);
5876
6217
  action.reset();
@@ -5883,12 +6224,50 @@ var ModelInstance3D = class extends Node3D {
5883
6224
  action.play();
5884
6225
  this.actionKeys.set(action, this.animation);
5885
6226
  if (this.currentAction && this.currentAction !== action) {
6227
+ if (samePlaying && clip.duration > 0) action.time = this.currentAction.time % clip.duration;
5886
6228
  const warp = clip.duration > 0 && this.currentAction.getClip().duration > 0;
5887
6229
  action.crossFadeFrom(this.currentAction, ANIM_CROSSFADE, warp);
5888
6230
  }
5889
6231
  this.currentAction = action;
5890
6232
  } else console.warn(`incanto: model '${this.model}' has no animation '${this.animation}' — embedded: ${this.entry.animations.map((c) => c.name).join(", ") || "(none)"}; assets: ${assets.animationRefs().join(", ") || "(none)"}`);
5891
6233
  }
6234
+ /** The upper-body layer: masked clip on the `upperBodyRoot` subtree. */
6235
+ syncUpperAnimation(assets) {
6236
+ if (!this.mixer || !this.entry || this.playingUpper === this.animationUpper) return;
6237
+ if (this.animationUpper === "") {
6238
+ this.upperAction?.fadeOut(UPPER_FADE);
6239
+ this.upperAction = null;
6240
+ this.playingUpper = "";
6241
+ return;
6242
+ }
6243
+ const resolved = this.resolveClip(this.animationUpper, assets);
6244
+ if (resolved === "loading") return;
6245
+ if (!resolved) {
6246
+ console.warn(`incanto: model '${this.model}' has no animation '${this.animationUpper}' for the upper layer.`);
6247
+ this.playingUpper = this.animationUpper;
6248
+ return;
6249
+ }
6250
+ const variants = this.layerVariants(resolved);
6251
+ if (!variants) {
6252
+ console.warn(`incanto: upperBodyRoot '${this.upperBodyRoot}' not found in '${this.model}' — upper layer skipped.`);
6253
+ this.playingUpper = this.animationUpper;
6254
+ return;
6255
+ }
6256
+ const action = this.mixer.clipAction(variants.upper);
6257
+ action.reset();
6258
+ if (this.animationUpperLoop) action.setLoop(LoopRepeat, Infinity);
6259
+ else {
6260
+ action.setLoop(LoopOnce, 1);
6261
+ action.clampWhenFinished = true;
6262
+ }
6263
+ action.setEffectiveWeight(1);
6264
+ action.fadeIn(UPPER_FADE);
6265
+ action.play();
6266
+ this.actionKeys.set(action, this.animationUpper);
6267
+ if (this.upperAction && this.upperAction !== action) this.upperAction.fadeOut(UPPER_FADE);
6268
+ this.upperAction = action;
6269
+ this.playingUpper = this.animationUpper;
6270
+ }
5892
6271
  update(dt) {
5893
6272
  this.mixer?.update(dt);
5894
6273
  if (this.entry?.isVrm && this.entry.claimedBy === this) this.entry.vrm?.update(dt);
@@ -5906,6 +6285,10 @@ var ModelInstance3D = class extends Node3D {
5906
6285
  this.fitGroup = null;
5907
6286
  this.mixer = null;
5908
6287
  this.currentAction = null;
6288
+ this.upperAction = null;
6289
+ this.playingUpper = "";
6290
+ this.baseMode = "full";
6291
+ this.maskCache.clear();
5909
6292
  this.entry = null;
5910
6293
  this.mountedRef = "";
5911
6294
  this.playing = "";
@@ -11716,6 +12099,8 @@ function registerNodes3D() {
11716
12099
  registerNode(Joint3D);
11717
12100
  registerNode(Trail3D);
11718
12101
  registerNode(BoneAttachment3D);
12102
+ registerNode(BoneLookAt3D);
12103
+ registerNode(InstancedMesh3D);
11719
12104
  registerNode(Terrain3D);
11720
12105
  registerNode(Water3D);
11721
12106
  registerNode(Foliage3D);
@@ -11731,4 +12116,4 @@ function registerNodes3D() {
11731
12116
  registerNode(CharacterBody3D);
11732
12117
  }
11733
12118
  //#endregion
11734
- export { PhysicsBody3D as A, Terrain3D as C, Joint3D as D, terrainThemeLayers as E, QUARTER_PITCH as F, cameraRelative as I, keyboardIntensity as L, StaticBody3D as M, Node3D as N, Area3D as O, validateCollider3D as P, movementState as R, Billboard3D as S, TERRAIN_THEMES as T, resolveFlowerDensity as _, VoxelGrid3D as a, Camera3D as b, Particles3D as c, MeshInstance3D as d, DirectionalLight3D as f, Flowers3D as g, DENSITY_PRESETS as h, VOXEL_PALETTE as i, RigidBody3D as j, CharacterBody3D as k, ModelInstance3D as l, Foliage3D as m, Water3D as n, Tree3D as o, OmniLight3D as p, WATER_MAX_RIPPLES as r, Trail3D as s, registerNodes3D as t, LoftMesh3D as u, FLOWER_VARIETIES as v, DEFAULT_TERRAIN_TEXTURE_BASE as w, BoneAttachment3D as x, CharacterController3D as y, rigPose as z };
12119
+ export { Area3D as A, movementState as B, BoneAttachment3D as C, TERRAIN_THEMES as D, DEFAULT_TERRAIN_TEXTURE_BASE as E, Node3D as F, validateCollider3D as I, QUARTER_PITCH as L, PhysicsBody3D as M, RigidBody3D as N, terrainThemeLayers as O, StaticBody3D as P, cameraRelative as R, BoneLookAt3D as S, Terrain3D as T, rigPose as V, Flowers3D as _, VoxelGrid3D as a, CharacterController3D as b, Particles3D as c, DirectionalLight3D as d, OmniLight3D as f, DENSITY_PRESETS as g, Foliage3D as h, VOXEL_PALETTE as i, CharacterBody3D as j, Joint3D as k, ModelInstance3D as l, MeshInstance3D as m, Water3D as n, Tree3D as o, InstancedMesh3D as p, WATER_MAX_RIPPLES as r, Trail3D as s, registerNodes3D as t, LoftMesh3D as u, resolveFlowerDensity as v, Billboard3D as w, Camera3D as x, FLOWER_VARIETIES as y, keyboardIntensity as z };