incanto 0.72.0 → 0.74.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 (84) hide show
  1. package/bin/incanto-check.mjs +114 -42
  2. package/bin/incanto-verify.mjs +9 -2
  3. package/dist/2d.d.ts +29 -4
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +137 -15
  6. package/dist/3d.js +6 -6
  7. package/dist/{agent8-BqZ0_O1u.js → agent8-CHTT4unP.js} +1 -1
  8. package/dist/{audio-player-ioZ7VKC6.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
  9. package/dist/{behavior-Bf9P9oB2.d.ts → behavior-J1cmVah0.d.ts} +8 -0
  10. package/dist/{create-game-CmFAtLZp.js → create-game-BAiA-FjP.js} +7 -7
  11. package/dist/{create-game-C9rWqqAC.js → create-game-ssvGvP7W.js} +7 -7
  12. package/dist/debug.d.ts +1 -1
  13. package/dist/debug.js +1 -1
  14. package/dist/editor.js +965 -893
  15. package/dist/{environment-presets-BeHoaGhy.js → environment-presets-BkqlWewf.js} +277 -60
  16. package/dist/{gameplay-BlQe-M07.js → gameplay-Bxe1sMVT.js} +611 -39
  17. package/dist/gameplay.d.ts +141 -7
  18. package/dist/gameplay.js +2 -2
  19. package/dist/index.d.ts +6 -4
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-CBfQzB6S.d.ts → loader-BAeWMYfF.d.ts} +1 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-DrIgTlLk.js → physics-2d-C-SfvjzH.js} +94 -11
  25. package/dist/{physics-3d-BzOCw1VS.js → physics-3d-rOzeg890.js} +202 -15
  26. package/dist/{picking-C9McVJrO.js → picking-BMV34Pjl.js} +2 -2
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
  30. package/dist/{register-C8HuRkOf.js → register-DRQyZKGU.js} +45 -9
  31. package/dist/{replay-datiAE-b.js → replay-B6BbgGM_.js} +42 -6
  32. package/dist/{replay-CBEChqXq.d.ts → replay-wZK5Ok5c.d.ts} +1 -1
  33. package/dist/{save-slots-CEuJPUle.js → save-slots-CEUGeLuB.js} +11 -2
  34. package/dist/{split-screen-Bm5DTElG.js → split-screen-BxZ5rBe7.js} +4 -4
  35. package/dist/{split-screen-DkqLng6q.d.ts → split-screen-D6sx8z3Z.d.ts} +2 -2
  36. package/dist/{sprite-animation-CqR2o3SA.js → sprite-animation-Bz3DHD_4.js} +1 -1
  37. package/dist/{src-DRSsRleD.js → src-CqtvYtSN.js} +1 -1
  38. package/dist/{test-Djuq3Vhq.js → test-CEl0y2Sw.js} +162 -30
  39. package/dist/test.d.ts +56 -5
  40. package/dist/test.js +2 -2
  41. package/dist/{touch-BnCyPA0G.js → touch-CioTZB-y.js} +1 -1
  42. package/dist/vite.js +3 -3
  43. package/editor/assets/{agent8-CMKz2cax.js → agent8-BlzturGi.js} +1 -1
  44. package/editor/assets/{debug-DLrQm-az.js → debug-BMUi8usj.js} +1 -1
  45. package/editor/assets/{index-BEKuxYcE.js → index-CrUCQoaB.js} +53 -53
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +634 -10
  49. package/skills/incanto-3d-character.md +113 -5
  50. package/skills/incanto-assets.md +9 -3
  51. package/skills/incanto-behaviors-and-scripts.md +26 -2
  52. package/skills/incanto-building-2d-games.md +9 -0
  53. package/skills/incanto-building-3d-games.md +45 -0
  54. package/skills/incanto-editor.md +6 -1
  55. package/skills/incanto-environment.md +7 -2
  56. package/skills/incanto-game-feel.md +16 -11
  57. package/skills/incanto-gameplay-behaviors.md +179 -16
  58. package/skills/incanto-node-reference.md +124 -45
  59. package/skills/incanto-physics-and-input.md +67 -5
  60. package/skills/incanto-scene-json-authoring.md +9 -3
  61. package/skills/incanto-verifying-your-game.md +19 -2
  62. package/templates-app/beacon-isle-3d/PROJECT/Status.md +8 -0
  63. package/templates-app/beacon-isle-3d/PROJECT/Structure.md +3 -0
  64. package/templates-app/beacon-isle-3d/package.json +1 -1
  65. package/templates-app/beacon-isle-3d/src/game.scene.json +136 -0
  66. package/templates-app/beacon-isle-3d/verify.ts +144 -0
  67. package/templates-app/molehill-2d/package.json +1 -1
  68. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  69. package/templates-app/platformer-2d/PROJECT/Structure.md +3 -0
  70. package/templates-app/platformer-2d/package.json +1 -1
  71. package/templates-app/platformer-2d/src/game.scene.json +126 -0
  72. package/templates-app/platformer-2d/verify.ts +54 -0
  73. package/templates-app/star-survivor/package.json +1 -1
  74. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  75. package/templates-app/tps-3d/PROJECT/Status.md +40 -1
  76. package/templates-app/tps-3d/PROJECT/Structure.md +19 -0
  77. package/templates-app/tps-3d/package.json +1 -1
  78. package/templates-app/tps-3d/src/behaviors.ts +9 -0
  79. package/templates-app/tps-3d/src/cover.scene.json +26 -0
  80. package/templates-app/tps-3d/src/game.scene.json +331 -75
  81. package/templates-app/tps-3d/src/main.ts +14 -0
  82. package/templates-app/tps-3d/src/pillar.scene.json +25 -0
  83. package/templates-app/tps-3d/verify.ts +582 -3
  84. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,14 +1,14 @@
1
- import { Y as diagnose } from "./save-slots-CEuJPUle.js";
2
- import { M as translateOn, t as registerCoreNodes } from "./register-5IYfooAN.js";
1
+ import { Y as diagnose } from "./save-slots-CEUGeLuB.js";
2
+ import { M as translateOn, t as registerCoreNodes } from "./register-CjociOtt.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-CDOMybym.js";
5
- import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-BnCyPA0G.js";
5
+ import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-CioTZB-y.js";
6
6
  import { t as createNoise2D } from "./noise-D3nPpmFg.js";
7
- import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimations, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CqR2o3SA.js";
8
- import { B as rejectScale, D as effectRng, G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, Q as worldPosition, T as tolerateUnknownAction, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, X as colliderFootDrop, z as WaterCutout3D } from "./gameplay-BlQe-M07.js";
7
+ import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimations, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-Bz3DHD_4.js";
8
+ import { E as tolerateUnknownAction, G as PhysicsBody3D, H as rejectScale, J as Node3D, K as RigidBody3D, O as effectRng, Q as colliderFootDrop, U as Area3D, V as WaterCutout3D, W as CharacterBody3D, et as worldPosition, q as StaticBody3D, z as Water3D } from "./gameplay-Bxe1sMVT.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
10
10
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-BYgD5Edk.js";
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";
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, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, SpotLight, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
12
12
  import { clone } from "three/addons/utils/SkeletonUtils.js";
13
13
  //#region src/3d/textures.ts
14
14
  /**
@@ -641,7 +641,10 @@ var InstancedMesh3D = class extends Node3D {
641
641
  transforms: { default: [] },
642
642
  castShadow: { default: false },
643
643
  receiveShadow: { default: false },
644
- collider: { default: null }
644
+ collider: {
645
+ default: null,
646
+ kinds: ["null", "object"]
647
+ }
645
648
  };
646
649
  mesh = "box";
647
650
  size = [
@@ -783,7 +786,10 @@ var Joint3D = class extends Node3D {
783
786
  length: { default: 0 },
784
787
  stiffness: { default: 50 },
785
788
  damping: { default: 5 },
786
- collide: { default: null }
789
+ collide: {
790
+ default: null,
791
+ kinds: ["null", "boolean"]
792
+ }
787
793
  };
788
794
  type = "spherical";
789
795
  target = "";
@@ -1583,12 +1589,18 @@ var Terrain3D = class extends Node3D {
1583
1589
  default: .95,
1584
1590
  range: { min: 0 }
1585
1591
  },
1586
- islandEdge: { default: null },
1592
+ islandEdge: {
1593
+ default: null,
1594
+ kinds: ["null", "boolean"]
1595
+ },
1587
1596
  layers: { default: [] },
1588
1597
  textureBase: { default: DEFAULT_TERRAIN_TEXTURE_BASE },
1589
1598
  basins: { default: [] },
1590
1599
  channels: { default: [] },
1591
- wetline: { default: null }
1600
+ wetline: {
1601
+ default: null,
1602
+ kinds: ["null", "object"]
1603
+ }
1592
1604
  };
1593
1605
  /** [width, depth] extent in meters, centered on the node. */
1594
1606
  size = [200, 200];
@@ -2666,7 +2678,7 @@ const correctionScratch = new Quaternion();
2666
2678
  const parentScratch = new Quaternion();
2667
2679
  const quatScratch$1 = new Quaternion();
2668
2680
  //#endregion
2669
- //#region src/3d/nodes/camera-3d.ts
2681
+ //#region src/3d/nodes/look-at.ts
2670
2682
  const RAD2DEG$1 = 180 / Math.PI;
2671
2683
  /** World up. The whole fix is that this argument exists. */
2672
2684
  const UP = new Vector3(0, 1, 0);
@@ -2675,6 +2687,50 @@ const targetScratch = new Vector3();
2675
2687
  const lookScratch = new Matrix4();
2676
2688
  const eulerScratch$1 = new Euler(0, 0, 0, "XYZ");
2677
2689
  /**
2690
+ * Aim a node's `rotation` at whatever its `lookAt` path names — the camera's
2691
+ * rule, shared with the spot light so that "point the searchlight at the
2692
+ * player" is one prop and not Euler arithmetic.
2693
+ *
2694
+ * Runs in `update` and not `_syncObject3D`: the second is the renderer's copy
2695
+ * pass and a static node is pruned from it, which silently freezes anything
2696
+ * driven there. `rotation` is written as an ordinary prop, so the editor, the
2697
+ * serializer and `describeFraming` all see what the node is actually doing.
2698
+ *
2699
+ * A path that resolves to nothing is an AUDIT finding, not a throw: every
2700
+ * other `nodePath` prop in the engine is reported by `auditScene` /
2701
+ * `incanto-check`, and throwing here would QUARANTINE the node. A target with
2702
+ * no world position (a light, a HudLayer, a plain `Node`) is warned about
2703
+ * once per path and the node keeps its authored rotation. Returns the path
2704
+ * that has been warned about, for the caller to remember.
2705
+ */
2706
+ function aimAt(node, warned, what) {
2707
+ if (node.lookAt === "") return warned;
2708
+ const target = node.getNodeOrNull(node.lookAt);
2709
+ if (!target) return warned;
2710
+ const to = worldPosition(target);
2711
+ if (to.length < 3) {
2712
+ if (warned !== node.lookAt) {
2713
+ diagnose(node.tree?.engine ?? null, "warn", `[incanto] ${node.getPath()}: lookAt '${node.lookAt}' resolves to a ${target.constructor.typeName ?? "Node"}, which has no world position — the ${what} keeps its authored rotation. Point it at a Node3D.`);
2714
+ return node.lookAt;
2715
+ }
2716
+ return warned;
2717
+ }
2718
+ const from = worldPosition(node);
2719
+ eyeScratch.set(from[0] ?? 0, from[1] ?? 0, from[2] ?? 0);
2720
+ targetScratch.set(to[0] ?? 0, to[1] ?? 0, to[2] ?? 0);
2721
+ if (eyeScratch.distanceToSquared(targetScratch) < 1e-12) return warned;
2722
+ lookScratch.lookAt(eyeScratch, targetScratch, UP);
2723
+ eulerScratch$1.setFromRotationMatrix(lookScratch, "XYZ");
2724
+ node.rotation = [
2725
+ eulerScratch$1.x * RAD2DEG$1,
2726
+ eulerScratch$1.y * RAD2DEG$1,
2727
+ eulerScratch$1.z * RAD2DEG$1
2728
+ ];
2729
+ return warned;
2730
+ }
2731
+ //#endregion
2732
+ //#region src/3d/nodes/camera-3d.ts
2733
+ /**
2678
2734
  * A perspective camera. Mark exactly one camera `current: true`; with none
2679
2735
  * marked, the renderer falls back to the first camera in tree order.
2680
2736
  * Aspect ratio is managed by the renderer on resize.
@@ -2722,38 +2778,13 @@ var Camera3D = class extends Node3D {
2722
2778
  current = false;
2723
2779
  lookAt = "";
2724
2780
  /**
2725
- * Aim, every frame, whenever `lookAt` names something.
2726
- *
2727
- * In `update` and not `_syncObject3D`: the second is the renderer's copy pass
2728
- * and a static node is pruned from it, which silently freezes anything driven
2729
- * there. `rotation` is written as an ordinary prop, so the editor, the
2730
- * serializer and `describeFraming` all see what the camera is actually doing.
2781
+ * Aim, every frame, whenever `lookAt` names something — see `aimAt`, which
2782
+ * the spot light shares.
2731
2783
  */
2732
2784
  /** The `lookAt` path already reported as unlookable — cleared by a new one. */
2733
2785
  warnedLookAt = "";
2734
2786
  update(_dt) {
2735
- if (this.lookAt === "") return;
2736
- const target = this.getNodeOrNull(this.lookAt);
2737
- if (!target) return;
2738
- const to = worldPosition(target);
2739
- if (to.length < 3) {
2740
- if (this.warnedLookAt !== this.lookAt) {
2741
- this.warnedLookAt = this.lookAt;
2742
- diagnose(this.tree?.engine ?? null, "warn", `[incanto] ${this.getPath()}: lookAt '${this.lookAt}' resolves to a ${target.constructor.typeName ?? "Node"}, which has no world position — the camera keeps its authored rotation. Point it at a Node3D.`);
2743
- }
2744
- return;
2745
- }
2746
- const from = worldPosition(this);
2747
- eyeScratch.set(from[0] ?? 0, from[1] ?? 0, from[2] ?? 0);
2748
- targetScratch.set(to[0] ?? 0, to[1] ?? 0, to[2] ?? 0);
2749
- if (eyeScratch.distanceToSquared(targetScratch) < 1e-12) return;
2750
- lookScratch.lookAt(eyeScratch, targetScratch, UP);
2751
- eulerScratch$1.setFromRotationMatrix(lookScratch, "XYZ");
2752
- this.rotation = [
2753
- eulerScratch$1.x * RAD2DEG$1,
2754
- eulerScratch$1.y * RAD2DEG$1,
2755
- eulerScratch$1.z * RAD2DEG$1
2756
- ];
2787
+ this.warnedLookAt = aimAt(this, this.warnedLookAt, "camera");
2757
2788
  }
2758
2789
  _createObject3D() {
2759
2790
  return new PerspectiveCamera(this.fov, 1, this.near, this.far);
@@ -2782,6 +2813,8 @@ const MOVEMENT_STATES = [
2782
2813
  "fastRun",
2783
2814
  "airborne"
2784
2815
  ];
2816
+ /** Air control stands down this long after a wall kick, so the kick lands. */
2817
+ const WALL_LOCKOUT_SECONDS = .18;
2785
2818
  const ACC_DELTA_TIME = 8;
2786
2819
  const DRAG_DAMPING = .15;
2787
2820
  const SPRING_K = 1.2;
@@ -2820,7 +2853,7 @@ const UP_VEC = new Vector3(0, 1, 0);
2820
2853
  */
2821
2854
  var CharacterController3D = class extends Node3D {
2822
2855
  static typeName = "CharacterController3D";
2823
- static signals = ["movementStateChanged"];
2856
+ static signals = ["movementStateChanged", "wallJumped"];
2824
2857
  static props = {
2825
2858
  view: {
2826
2859
  default: "free",
@@ -2847,6 +2880,11 @@ var CharacterController3D = class extends Node3D {
2847
2880
  maxJumps: { default: 1 },
2848
2881
  /** How much of your ground control you keep in the air (0 = none, 1 = full). */
2849
2882
  airControl: { default: .2 },
2883
+ wallJumpImpulse: { default: [0, 0] },
2884
+ wallSlideSpeed: {
2885
+ default: 0,
2886
+ range: { min: 0 }
2887
+ },
2850
2888
  /** Gravity multiplier while falling — >1 is the "snappy" arc. */
2851
2889
  fallGravity: { default: 2.5 },
2852
2890
  floatHeight: { default: .01 },
@@ -2888,6 +2926,17 @@ var CharacterController3D = class extends Node3D {
2888
2926
  jumpCutMultiplier = 1;
2889
2927
  maxJumps = 1;
2890
2928
  airControl = .2;
2929
+ /**
2930
+ * `[away, up]` m/s: airborne and pressing into a wall, `jump` kicks off it —
2931
+ * away along the wall's normal, up. `[0, 0]` (default) = no wall-jumps.
2932
+ * The controller owns this because a kick written from outside lasted three
2933
+ * frames: the jump cut halved its rise on release and air control steered
2934
+ * the horizontal straight back into the wall. After a kick, air control
2935
+ * stands down for `WALL_LOCKOUT` so the kick lands.
2936
+ */
2937
+ wallJumpImpulse = [0, 0];
2938
+ /** Max fall speed (m/s) while pressing into a wall; 0 = no wall slide. */
2939
+ wallSlideSpeed = 0;
2891
2940
  fallGravity = 2.5;
2892
2941
  floatHeight = .01;
2893
2942
  /**
@@ -3008,6 +3057,10 @@ var CharacterController3D = class extends Node3D {
3008
3057
  rising = false;
3009
3058
  /** Between the jump impulse and the landing — the animation's clock. */
3010
3059
  jumping = false;
3060
+ /** Seconds of air control still suppressed after a wall kick. */
3061
+ wallLockout = 0;
3062
+ /** The outward normal of the wall being pressed into this step, if any. */
3063
+ wallNormal = null;
3011
3064
  get body() {
3012
3065
  const parent = this.parent;
3013
3066
  if (!(parent instanceof RigidBody3D)) throw new IncantoError("TREE_VIOLATION", `CharacterController3D '${this.name}' must be a child of a RigidBody3D (parent is '${parent?.name ?? "none"}').`);
@@ -3090,7 +3143,29 @@ var CharacterController3D = class extends Node3D {
3090
3143
  const intensity = keyboardIntensity(moving, sprinting);
3091
3144
  const speedMultiplier = 1 + (this.sprintMultiplier - 1) * intensity;
3092
3145
  const targetSpeed = moving ? this.maxSpeed * speedMultiplier : 0;
3093
- const control = this.grounded ? 1 : this.airControl;
3146
+ this.wallNormal = null;
3147
+ if ((this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0)) && !this.grounded && moving) {
3148
+ const r = body.collider.radius ?? .32;
3149
+ const n = (this.tree?.engine?.physics?.castRay([
3150
+ pos[0] ?? 0,
3151
+ pos[1] ?? 0,
3152
+ pos[2] ?? 0
3153
+ ], [
3154
+ mx,
3155
+ 0,
3156
+ mz
3157
+ ], r + .25, body, { staticOnly: true }))?.normal;
3158
+ if (n && Math.hypot(n[0] ?? 0, n[2] ?? 0) > .5) {
3159
+ const len = Math.hypot(n[0] ?? 0, n[2] ?? 0);
3160
+ this.wallNormal = [
3161
+ (n[0] ?? 0) / len,
3162
+ 0,
3163
+ (n[2] ?? 0) / len
3164
+ ];
3165
+ }
3166
+ }
3167
+ this.wallLockout = Math.max(0, this.wallLockout - dt);
3168
+ const control = this.wallLockout > 0 ? 0 : this.grounded ? 1 : this.airControl;
3094
3169
  const ax = (mx * targetSpeed + carry[0] - vel[0]) / ACC_DELTA_TIME * control;
3095
3170
  const az = (mz * targetSpeed + carry[1] - vel[2]) / ACC_DELTA_TIME * control;
3096
3171
  body.applyImpulse([
@@ -3121,7 +3196,7 @@ var CharacterController3D = class extends Node3D {
3121
3196
  if (rise > .02 && rise <= this.stepHeight + .02) springToi = toi - rise;
3122
3197
  }
3123
3198
  }
3124
- if (springToi !== null && springToi < floatingDis * 2 && !this.jumping) {
3199
+ if (springToi !== null && springToi < floatingDis * 2 && !this.jumping && (vel[1] ?? 0) < 2) {
3125
3200
  const spring = SPRING_K * (floatingDis - springToi) - vel[1] * SPRING_DAMPING;
3126
3201
  body.applyImpulse([
3127
3202
  0,
@@ -3141,7 +3216,27 @@ var CharacterController3D = class extends Node3D {
3141
3216
  const pressedJump = this.readAction("jumpAction", () => input.justPressed(this.jumpAction), false, "The character will never jump.");
3142
3217
  if (pressedJump) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
3143
3218
  else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
3144
- if ((this.bufferLeft > 0 || pressedJump) && (footed || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
3219
+ const wantsJump = this.bufferLeft > 0 || pressedJump;
3220
+ const wall = this.wallNormal;
3221
+ if (wantsJump && !this.grounded && wall && this.wallJumpImpulse.some((v) => v !== 0)) {
3222
+ const away = this.wallJumpImpulse[0] ?? 0;
3223
+ const up = this.wallJumpImpulse[1] ?? 0;
3224
+ body.linearVelocity = [
3225
+ wall[0] * away,
3226
+ up,
3227
+ wall[2] * away
3228
+ ];
3229
+ vel[0] = wall[0] * away;
3230
+ vel[1] = up;
3231
+ vel[2] = wall[2] * away;
3232
+ this.grounded = false;
3233
+ this.bufferLeft = 0;
3234
+ this.coyoteLeft = 0;
3235
+ this.rising = false;
3236
+ this.jumping = true;
3237
+ this.wallLockout = WALL_LOCKOUT_SECONDS;
3238
+ this.emit("wallJumped", wall);
3239
+ } else if (wantsJump && (footed || this.coyoteLeft > 0 || this.jumpsUsed < this.maxJumps)) {
3145
3240
  const jumpVel = this.jumpVelocity * (1 + (this.sprintJumpMultiplier - 1) * intensity);
3146
3241
  body.linearVelocity = [
3147
3242
  vel[0],
@@ -3169,8 +3264,17 @@ var CharacterController3D = class extends Node3D {
3169
3264
  this.rising = false;
3170
3265
  }
3171
3266
  }
3267
+ const sliding = this.wallSlideSpeed > 0 && !footed && this.wallNormal !== null && (vel[1] ?? 0) < 0;
3268
+ if (sliding && (vel[1] ?? 0) < -this.wallSlideSpeed) {
3269
+ body.linearVelocity = [
3270
+ vel[0],
3271
+ -this.wallSlideSpeed,
3272
+ vel[2]
3273
+ ];
3274
+ vel[1] = -this.wallSlideSpeed;
3275
+ }
3172
3276
  if ((vel[1] ?? 0) < FALLING_MAX_VEL) body.gravityScale = 0;
3173
- else if ((vel[1] ?? 0) < 0 && !footed) body.gravityScale = this.fallGravity;
3277
+ else if ((vel[1] ?? 0) < 0 && !footed) body.gravityScale = sliding ? 1 : this.fallGravity;
3174
3278
  else body.gravityScale = 1;
3175
3279
  if (this.view === "side" && Math.abs(pos[2]) > .01) body.position = [
3176
3280
  body.position[0] ?? 0,
@@ -3973,7 +4077,10 @@ var Flowers3D = class extends Node3D {
3973
4077
  height: { default: .45 },
3974
4078
  clustering: { default: .6 },
3975
4079
  sway: { default: .5 },
3976
- drape: { default: null },
4080
+ drape: {
4081
+ default: null,
4082
+ kinds: ["null", "boolean"]
4083
+ },
3977
4084
  avoidWater: { default: true },
3978
4085
  terrain: {
3979
4086
  default: "",
@@ -5387,7 +5494,10 @@ var Foliage3D = class Foliage3D extends Node3D {
5387
5494
  },
5388
5495
  flowers: { default: 0 },
5389
5496
  seed: { default: 1 },
5390
- drape: { default: null },
5497
+ drape: {
5498
+ default: null,
5499
+ kinds: ["null", "boolean"]
5500
+ },
5391
5501
  avoidWater: { default: true },
5392
5502
  terrain: {
5393
5503
  default: "",
@@ -6308,6 +6418,98 @@ var OmniLight3D = class extends Node3D {
6308
6418
  light.distance = this.range;
6309
6419
  }
6310
6420
  };
6421
+ /**
6422
+ * A cone of light — a flashlight, a searchlight, a car's headlights, a stage
6423
+ * spot, a guard's torch.
6424
+ *
6425
+ * It AIMS down the node's local −Z, exactly like a camera: at rest it points
6426
+ * along −Z, `rotation: [-90, 0, 0]` points it at the floor, and a headlamp
6427
+ * mounted on a +Z-forward skin wants `rotation: [0, 180, 0]`. `coneDeg` is the
6428
+ * FULL angle of the cone (a "30° beam" is 30), converted to three's half-angle
6429
+ * here so nobody has to halve it by hand. `intensity` is in the same physical
6430
+ * units as `OmniLight3D` — tens, not ones. Shadows are a perspective map from
6431
+ * the cone's apex, so `castShadow` costs one extra render per frame per light.
6432
+ */
6433
+ var SpotLight3D = class extends Node3D {
6434
+ static typeName = "SpotLight3D";
6435
+ static props = {
6436
+ color: { default: "#ffffff" },
6437
+ intensity: {
6438
+ default: 1,
6439
+ range: { min: 0 }
6440
+ },
6441
+ range: { default: 0 },
6442
+ coneDeg: {
6443
+ default: 60,
6444
+ range: {
6445
+ min: 0,
6446
+ max: 180,
6447
+ exclusiveMin: true,
6448
+ exclusiveMax: true
6449
+ }
6450
+ },
6451
+ penumbra: {
6452
+ default: .3,
6453
+ range: {
6454
+ min: 0,
6455
+ max: 1
6456
+ }
6457
+ },
6458
+ castShadow: { default: false },
6459
+ shadowMapSize: { default: 1024 },
6460
+ lookAt: {
6461
+ default: "",
6462
+ nodePath: true
6463
+ }
6464
+ };
6465
+ color = "#ffffff";
6466
+ intensity = 1;
6467
+ /** 0 = unlimited (three `distance` semantics). */
6468
+ range = 0;
6469
+ /** Full cone angle in degrees, exclusive of 0 and 180. */
6470
+ coneDeg = 60;
6471
+ /** 0 = hard edge, 1 = the whole cone is falloff. */
6472
+ penumbra = .3;
6473
+ castShadow = false;
6474
+ shadowMapSize = 1024;
6475
+ /**
6476
+ * A node path to AIM at, every frame — the camera's prop, for the same
6477
+ * reason: a searchlight that tracks the player, a stage spot on the boss.
6478
+ * Aiming by hand is Euler arithmetic in XYZ order, where a yaw on the same
6479
+ * node as a pitch swings the cone sideways (at yaw 180 a −55° pitch points
6480
+ * UP). Empty = the authored `rotation` is the aim.
6481
+ */
6482
+ lookAt = "";
6483
+ warnedLookAt = "";
6484
+ update(_dt) {
6485
+ this.warnedLookAt = aimAt(this, this.warnedLookAt, "light");
6486
+ }
6487
+ _createObject3D() {
6488
+ const light = new SpotLight();
6489
+ light.add(light.target);
6490
+ light.target.position.set(0, 0, -1);
6491
+ return light;
6492
+ }
6493
+ _syncObject3D() {
6494
+ super._syncObject3D();
6495
+ const light = this._ensureObject3D();
6496
+ light.color.set(this.color);
6497
+ light.intensity = this.intensity;
6498
+ light.distance = this.range;
6499
+ light.angle = MathUtils.degToRad(this.coneDeg / 2);
6500
+ light.penumbra = this.penumbra;
6501
+ light.castShadow = this.castShadow;
6502
+ if (this.castShadow) {
6503
+ if (light.shadow.mapSize.x !== this.shadowMapSize) {
6504
+ light.shadow.mapSize.set(this.shadowMapSize, this.shadowMapSize);
6505
+ light.shadow.map?.dispose();
6506
+ light.shadow.map = null;
6507
+ }
6508
+ light.shadow.bias = -2e-4;
6509
+ light.shadow.normalBias = .002;
6510
+ }
6511
+ }
6512
+ };
6311
6513
  //#endregion
6312
6514
  //#region src/3d/nodes/loft-mesh-3d.ts
6313
6515
  /**
@@ -11564,7 +11766,10 @@ var Tree3D = class Tree3D extends Node3D {
11564
11766
  leafFadeStart: { default: 0 },
11565
11767
  leafFadeEnd: { default: 0 },
11566
11768
  leafShadows: { default: true },
11567
- drape: { default: null },
11769
+ drape: {
11770
+ default: null,
11771
+ kinds: ["null", "boolean"]
11772
+ },
11568
11773
  terrain: {
11569
11774
  default: "",
11570
11775
  nodePath: true
@@ -12908,9 +13113,29 @@ var VoxelGrid3D = class extends Node3D {
12908
13113
  for (const [i, row] of self.voxels.entries()) if (!Array.isArray(row) || row.length < 3 || row.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `VoxelGrid3D '${node.name}': voxels[${i}] must be [x, y, z, tile?] (numbers), got ${JSON.stringify(row)}.`);
12909
13114
  if (self.voxels.length > MAX_INSTANCES) throw new IncantoError("BAD_FORMAT", `VoxelGrid3D '${node.name}': ${self.voxels.length} voxels exceeds the ${MAX_INSTANCES} this node can draw. Everything past the cap would still be SOLID and still be reported by tileAt(), and simply not appear. Split the world into several VoxelGrid3D nodes (one per chunk) and show the ones near the player.`);
12910
13115
  }
12911
- /** JSON-authored blocks as [x, y, z, tile] tuples (generated terrains bake these). */
12912
- voxels = [];
12913
- appliedVoxels = null;
13116
+ _voxels = [];
13117
+ /**
13118
+ * JSON-authored blocks as [x, y, z, tile] tuples (generated terrains bake
13119
+ * these). The seed lands WHEN THE PROP DOES — at load, before the tree
13120
+ * readies — not on the first `update()` as it used to: a sibling that
13121
+ * builds chunk colliders from `blocks()` at ready (the minecraft template's
13122
+ * Builder) saw an empty map and a baked hut was walked through, and the
13123
+ * late `setBlocks` then WIPED whatever a generator had built at ready.
13124
+ * Now a generator calling `setBlocks` after load is the author replacing
13125
+ * the seed, never the seed replacing the author.
13126
+ */
13127
+ get voxels() {
13128
+ return this._voxels;
13129
+ }
13130
+ set voxels(rows) {
13131
+ this._voxels = rows;
13132
+ if (rows.length > 0) this.setBlocks(rows.map((t) => ({
13133
+ x: t[0] ?? 0,
13134
+ y: t[1] ?? 0,
13135
+ z: t[2] ?? 0,
13136
+ tile: t[3] ?? 0
13137
+ })));
13138
+ }
12914
13139
  /** Said once per node: a runtime overflow must not spam a frame loop. */
12915
13140
  reportedOverflow = false;
12916
13141
  map = /* @__PURE__ */ new Map();
@@ -12969,15 +13194,6 @@ var VoxelGrid3D = class extends Node3D {
12969
13194
  return this.mesh;
12970
13195
  }
12971
13196
  update() {
12972
- if (this.voxels.length > 0 && this.voxels !== this.appliedVoxels) {
12973
- this.appliedVoxels = this.voxels;
12974
- this.setBlocks(this.voxels.map((t) => ({
12975
- x: t[0] ?? 0,
12976
- y: t[1] ?? 0,
12977
- z: t[2] ?? 0,
12978
- tile: t[3] ?? 0
12979
- })));
12980
- }
12981
13197
  if (this.map.size > MAX_INSTANCES && !this.reportedOverflow) {
12982
13198
  this.reportedOverflow = true;
12983
13199
  diagnose(this.tree?.engine ?? null, "error", `[incanto] VoxelGrid3D '${this.getPath()}' holds ${this.map.size} blocks and can draw ${MAX_INSTANCES}. The rest are still solid and still reported by tileAt(), and do not appear. Split the world across several VoxelGrid3D nodes.`);
@@ -13099,6 +13315,7 @@ function registerNodes3D() {
13099
13315
  registerNode(Camera3D);
13100
13316
  registerNode(DirectionalLight3D);
13101
13317
  registerNode(OmniLight3D);
13318
+ registerNode(SpotLight3D);
13102
13319
  registerNode(StaticBody3D);
13103
13320
  registerNode(RigidBody3D);
13104
13321
  registerNode(Area3D);
@@ -13136,4 +13353,4 @@ function resolveEnvironmentHdri(env) {
13136
13353
  return null;
13137
13354
  }
13138
13355
  //#endregion
13139
- export { Terrain3D as A, keyboardIntensity as B, resolveFlowerDensity as C, BoneLookAt3D as D, Camera3D as E, InstancedMesh3D as F, acquireTexture as G, rigPose as H, MeshInstance3D as I, buildMeshGeometry as L, TERRAIN_THEMES as M, terrainThemeLayers as N, BoneAttachment3D as O, Joint3D as P, QUARTER_PITCH as R, Flowers3D as S, CharacterController3D as T, TextureCache3D as U, movementState as V, acquireOwnTexture as W, LoftMesh3D as _, Tree3D as a, Foliage3D as b, buildRiverRings as c, riverCarveChannels as d, riverStepFor as f, ModelInstance3D as g, Particles3D as h, VoxelGrid3D as i, DEFAULT_TERRAIN_TEXTURE_BASE as j, Billboard3D as k, findRiverCoverageGaps as l, traceDownhillPath as m, registerNodes3D as n, Trail3D as o, smoothCourse as p, VOXEL_PALETTE as r, River3D as s, resolveEnvironmentHdri as t, projectToRiver as u, DirectionalLight3D as v, FLOWER_VARIETIES as w, DENSITY_PRESETS as x, OmniLight3D as y, cameraRelative as z };
13356
+ export { Billboard3D as A, cameraRelative as B, Flowers3D as C, Camera3D as D, CharacterController3D as E, Joint3D as F, acquireOwnTexture as G, movementState as H, InstancedMesh3D as I, acquireTexture as K, MeshInstance3D as L, DEFAULT_TERRAIN_TEXTURE_BASE as M, TERRAIN_THEMES as N, BoneLookAt3D as O, terrainThemeLayers as P, buildMeshGeometry as R, DENSITY_PRESETS as S, FLOWER_VARIETIES as T, rigPose as U, keyboardIntensity as V, TextureCache3D as W, LoftMesh3D as _, Tree3D as a, SpotLight3D as b, buildRiverRings as c, riverCarveChannels as d, riverStepFor as f, ModelInstance3D as g, Particles3D as h, VoxelGrid3D as i, Terrain3D as j, BoneAttachment3D as k, findRiverCoverageGaps as l, traceDownhillPath as m, registerNodes3D as n, Trail3D as o, smoothCourse as p, VOXEL_PALETTE as r, River3D as s, resolveEnvironmentHdri as t, projectToRiver as u, DirectionalLight3D as v, resolveFlowerDensity as w, Foliage3D as x, OmniLight3D as y, QUARTER_PITCH as z };