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,10 +1,10 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { Y as diagnose } from "./save-slots-CEuJPUle.js";
2
+ import { Y as diagnose } from "./save-slots-CEUGeLuB.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { H as CharacterBody3D, K as Node3D, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, q as validateCollider3D } from "./gameplay-BlQe-M07.js";
4
+ import { G as PhysicsBody3D, J as Node3D, K as RigidBody3D, U as Area3D, W as CharacterBody3D, Y as validateCollider3D, q as StaticBody3D } from "./gameplay-Bxe1sMVT.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { n as jointContacts, t as withoutRapierInitNoise } from "./quiet-rapier-C6fW4zcW.js";
7
- import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-BeHoaGhy.js";
7
+ import { F as Joint3D, I as InstancedMesh3D, R as buildMeshGeometry, j as Terrain3D } from "./environment-presets-BkqlWewf.js";
8
8
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
9
9
  //#region src/3d/physics/collider-lines.ts
10
10
  /**
@@ -348,6 +348,7 @@ var Physics3D = class {
348
348
  const b = this.byColliderHandle.get(h2);
349
349
  if (!a || !b) return;
350
350
  const sig = started ? "triggerEnter" : "triggerExit";
351
+ if (started === (this.overlaps.get(a)?.has(b) ?? false)) return;
351
352
  this.trackOverlap(a, b, started);
352
353
  this.trackOverlap(b, a, started);
353
354
  try {
@@ -424,7 +425,8 @@ var Physics3D = class {
424
425
  y: vy * this.lastDt + carryY,
425
426
  z: (node.velocity[2] ?? 0) * this.lastDt + carryZ
426
427
  };
427
- this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS);
428
+ const others = node.collideWithCharacters ? void 0 : (c) => !(this.byColliderHandle.get(c.handle) instanceof CharacterBody3D);
429
+ this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS, void 0, others);
428
430
  const c = this.kcc.computedMovement();
429
431
  const t = e.body.translation();
430
432
  const nx = t.x + c.x;
@@ -528,7 +530,12 @@ var Physics3D = class {
528
530
  * off ONE fixed body — they never move, so there is nothing to step — and the
529
531
  * whole set is rebuilt only when the tree's shape changes.
530
532
  */
531
- /** Where each live scatter field was when its colliders were built. */
533
+ /**
534
+ * Where each live scatter field was when its colliders were built — and what
535
+ * they were built FROM. Rows and place were remembered from the start; the
536
+ * stone itself (`mesh`, `size`, the `collider` spec) was not, so a field
537
+ * grown at runtime, or resized in the inspector, collided as the old stones.
538
+ */
532
539
  scatterState = /* @__PURE__ */ new Map();
533
540
  /**
534
541
  * Has any scatter field moved, or had its rows replaced, since the build?
@@ -543,6 +550,9 @@ var Physics3D = class {
543
550
  const [x, y, z] = worldPosition3D(field);
544
551
  if (!was) return true;
545
552
  if (was.rows !== field.transforms || was.x !== x || was.y !== y || was.z !== z) return true;
553
+ const [sx, sy, sz] = field.size;
554
+ if (was.mesh !== field.mesh || was.sx !== sx || was.sy !== sy || was.sz !== sz) return true;
555
+ if (was.spec !== field.collider) return true;
546
556
  }
547
557
  return false;
548
558
  }
@@ -595,7 +605,12 @@ var Physics3D = class {
595
605
  rows: field.transforms,
596
606
  x: ox,
597
607
  y: oy,
598
- z: oz
608
+ z: oz,
609
+ mesh: field.mesh,
610
+ sx,
611
+ sy,
612
+ sz,
613
+ spec: field.collider
599
614
  });
600
615
  }
601
616
  }
@@ -650,7 +665,7 @@ var Physics3D = class {
650
665
  continue;
651
666
  }
652
667
  const existing = this.entries.get(node);
653
- if (existing && node._colliderChanged) {
668
+ if (existing && (node._colliderChanged || this.heightfieldReshaped(node, existing) || this.autoReshaped(node, existing))) {
654
669
  this.byColliderHandle.delete(existing.collider.handle);
655
670
  this.world.removeRigidBody(existing.body);
656
671
  this.entries.delete(node);
@@ -672,6 +687,84 @@ var Physics3D = class {
672
687
  }
673
688
  }
674
689
  /**
690
+ * The stamp an auto body is compared against, reused across every body and
691
+ * step: the walk writes here instead of returning an object, because it runs
692
+ * once per auto body per step and a village of crates must not allocate.
693
+ */
694
+ stamp = {
695
+ kind: "",
696
+ model: null,
697
+ nums: new Float64Array(12)
698
+ };
699
+ /** An auto body whose carried mesh (or mounted model) is not what it was built from. */
700
+ autoReshaped(node, e) {
701
+ const was = e.autoShape;
702
+ if (!was || node.collider.shape !== "auto") return false;
703
+ this.stampAutoShape(node);
704
+ const now = this.stamp;
705
+ if (now.kind !== was.kind) return true;
706
+ if ((now.model?._mountedObject3D() ?? null) !== was.mounted) return true;
707
+ for (let i = 0; i < 12; i++) if (now.nums[i] !== was.nums[i]) return true;
708
+ return false;
709
+ }
710
+ rememberAutoShape(node) {
711
+ this.stampAutoShape(node);
712
+ return {
713
+ kind: this.stamp.kind,
714
+ nums: Float64Array.from(this.stamp.nums),
715
+ mounted: this.stamp.model?._mountedObject3D() ?? null
716
+ };
717
+ }
718
+ /**
719
+ * The same choice `meshesUnder` makes — the FIRST carried mesh in tree
720
+ * order, else the first mounted model — reduced to the numbers the collider
721
+ * was built from, so a change in any of them is visible without a rebuild.
722
+ */
723
+ stampAutoShape(node) {
724
+ this.stamp.kind = "";
725
+ this.stamp.model = null;
726
+ this.stamp.nums.fill(0);
727
+ this.walkAutoShape(node, 0, 0, 0, 0, 0, 0, 1, 1, 1);
728
+ }
729
+ walkAutoShape(node, ax, ay, az, rx, ry, rz, sx, sy, sz) {
730
+ const stamp = this.stamp;
731
+ if (isMountedModel(node)) {
732
+ if (!stamp.model) stamp.model = node;
733
+ } else if (stamp.kind === "") {
734
+ const self = node;
735
+ if (typeof self.mesh === "string" && Array.isArray(self.size)) {
736
+ const size = self.size;
737
+ stamp.kind = self.mesh;
738
+ const n = stamp.nums;
739
+ n[0] = size[0] ?? 0;
740
+ n[1] = size[1] ?? 0;
741
+ n[2] = size[2] ?? 0;
742
+ n[3] = ax;
743
+ n[4] = ay;
744
+ n[5] = az;
745
+ n[6] = rx;
746
+ n[7] = ry;
747
+ n[8] = rz;
748
+ n[9] = sx;
749
+ n[10] = sy;
750
+ n[11] = sz;
751
+ }
752
+ }
753
+ for (const child of node.children) {
754
+ const t = child;
755
+ const p = Array.isArray(t.position) ? t.position : void 0;
756
+ const r = Array.isArray(t.rotation) ? t.rotation : void 0;
757
+ const c = Array.isArray(t.scale) ? t.scale : void 0;
758
+ this.walkAutoShape(child, ax + (p?.[0] ?? 0) * sx, ay + (p?.[1] ?? 0) * sy, az + (p?.[2] ?? 0) * sz, rx + (r?.[0] ?? 0), ry + (r?.[1] ?? 0), rz + (r?.[2] ?? 0), sx * (c?.[0] ?? 1), sy * (c?.[1] ?? 1), sz * (c?.[2] ?? 1));
759
+ }
760
+ }
761
+ /** A heightfield body whose Terrain3D regenerated its grid since the build. */
762
+ heightfieldReshaped(node, e) {
763
+ if (node.collider.shape !== "heightfield") return false;
764
+ const terrain = heightfieldSource(node);
765
+ return terrain !== void 0 && terrain._heightmap() !== e.heightmap;
766
+ }
767
+ /**
675
768
  * Null while a `{"shape":"auto"}` body is waiting on its model — the shape
676
769
  * lives in the model's triangles, and there is nothing honest to guess in the
677
770
  * meantime. syncBodies runs every step, so the body appears the moment the
@@ -698,6 +791,7 @@ var Physics3D = class {
698
791
  });
699
792
  if (node instanceof RigidBody3D) {
700
793
  desc.setGravityScale(node.gravityScale);
794
+ desc.setLinearDamping(node.linearDamping).setAngularDamping(node.angularDamping);
701
795
  desc.setLinvel(node.linearVelocity[0] ?? 0, node.linearVelocity[1] ?? 0, node.linearVelocity[2] ?? 0);
702
796
  const locked = node.fixedRotation;
703
797
  desc.setAngvel({
@@ -711,7 +805,8 @@ var Physics3D = class {
711
805
  colDesc.setActiveEvents(R.ActiveEvents.COLLISION_EVENTS);
712
806
  colDesc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
713
807
  if (node instanceof Area3D) colDesc.setSensor(true);
714
- if (node instanceof RigidBody3D) colDesc.setMass(node.mass).setFriction(node.friction).setRestitution(node.restitution);
808
+ if (node instanceof RigidBody3D) colDesc.setMass(node.mass);
809
+ if (node instanceof RigidBody3D || node instanceof StaticBody3D) applySurface(R, colDesc, node);
715
810
  const collider = this.world.createCollider(colDesc, body);
716
811
  e = {
717
812
  body,
@@ -732,9 +827,13 @@ var Physics3D = class {
732
827
  0
733
828
  ],
734
829
  lastMass: node instanceof RigidBody3D ? node.mass : 1,
735
- lastFriction: node instanceof RigidBody3D ? node.friction : .5,
736
- lastRestitution: node instanceof RigidBody3D ? node.restitution : 0
830
+ lastFriction: node.friction ?? .5,
831
+ lastRestitution: node.restitution ?? 0,
832
+ lastLinDamp: node instanceof RigidBody3D ? node.linearDamping : 0,
833
+ lastAngDamp: node instanceof RigidBody3D ? node.angularDamping : 0
737
834
  };
835
+ if (node.collider.shape === "heightfield") e.heightmap = heightfieldSource(node)?._heightmap();
836
+ if (node.collider.shape === "auto") e.autoShape = this.rememberAutoShape(node);
738
837
  this.entries.set(node, e);
739
838
  this.byColliderHandle.set(collider.handle, node);
740
839
  if (!node.enabled) {
@@ -748,9 +847,11 @@ var Physics3D = class {
748
847
  return e;
749
848
  }
750
849
  if (node._enabledChanged) {
850
+ if (!node.enabled) this.releaseOverlaps(node);
751
851
  e.collider.setEnabled(node.enabled);
752
852
  e.body.setEnabled(node.enabled);
753
853
  node._enabledChanged = false;
854
+ if (node.enabled) this.armOverlaps(node, e.collider);
754
855
  }
755
856
  const p = worldPosition3D(node);
756
857
  if (p[0] !== e.last[0] || p[1] !== e.last[1] || p[2] !== e.last[2]) {
@@ -798,12 +899,23 @@ var Physics3D = class {
798
899
  e.collider.setMass(node.mass);
799
900
  e.lastMass = node.mass;
800
901
  }
801
- if (node.friction !== e.lastFriction) {
802
- e.collider.setFriction(node.friction);
902
+ if (node.friction !== e.lastFriction || node.restitution !== e.lastRestitution) {
903
+ applySurface(this.R, e.collider, node);
803
904
  e.lastFriction = node.friction;
905
+ e.lastRestitution = node.restitution;
906
+ }
907
+ if (node.linearDamping !== e.lastLinDamp) {
908
+ e.body.setLinearDamping(node.linearDamping);
909
+ e.lastLinDamp = node.linearDamping;
804
910
  }
805
- if (node.restitution !== e.lastRestitution) {
806
- e.collider.setRestitution(node.restitution);
911
+ if (node.angularDamping !== e.lastAngDamp) {
912
+ e.body.setAngularDamping(node.angularDamping);
913
+ e.lastAngDamp = node.angularDamping;
914
+ }
915
+ } else if (node instanceof StaticBody3D) {
916
+ if (node.friction !== e.lastFriction || node.restitution !== e.lastRestitution) {
917
+ applySurface(this.R, e.collider, node);
918
+ e.lastFriction = node.friction;
807
919
  e.lastRestitution = node.restitution;
808
920
  }
809
921
  }
@@ -911,6 +1023,52 @@ var Physics3D = class {
911
1023
  if (others.size === 0) this.overlaps.delete(self);
912
1024
  }
913
1025
  }
1026
+ /** Every overlap `node` is in ends now — the exits Rapier does not send for a disabled collider. */
1027
+ releaseOverlaps(node) {
1028
+ const set = this.overlaps.get(node);
1029
+ if (!set || set.size === 0) return;
1030
+ for (const other of [...set]) {
1031
+ this.trackOverlap(node, other, false);
1032
+ this.trackOverlap(other, node, false);
1033
+ try {
1034
+ node.emit("triggerExit", other);
1035
+ other.emit("triggerExit", node);
1036
+ } catch (error) {
1037
+ diagnose(this.engine, "error", `incanto: error in a 'triggerExit' handler (${node.name} ↔ ${other.name}):`, error);
1038
+ }
1039
+ }
1040
+ }
1041
+ /** Every body the armed sensor is inside of RIGHT NOW enters it — the starts Rapier does not send for a re-enabled collider. */
1042
+ armOverlaps(node, collider) {
1043
+ const found = [];
1044
+ const t = collider.translation();
1045
+ const r = collider.rotation();
1046
+ this.world.intersectionsWithShape({
1047
+ x: t.x,
1048
+ y: t.y,
1049
+ z: t.z
1050
+ }, {
1051
+ x: r.x,
1052
+ y: r.y,
1053
+ z: r.z,
1054
+ w: r.w
1055
+ }, collider.shape, (hit) => {
1056
+ const other = this.byColliderHandle.get(hit.handle);
1057
+ if (other && other !== node && hit.isEnabled()) found.push(other);
1058
+ return true;
1059
+ }, void 0, void 0, collider);
1060
+ for (const other of found) {
1061
+ if (this.overlaps.get(node)?.has(other)) continue;
1062
+ this.trackOverlap(node, other, true);
1063
+ this.trackOverlap(other, node, true);
1064
+ try {
1065
+ node.emit("triggerEnter", other);
1066
+ other.emit("triggerEnter", node);
1067
+ } catch (error) {
1068
+ diagnose(this.engine, "error", `incanto: error in a 'triggerEnter' handler (${node.name} ↔ ${other.name}):`, error);
1069
+ }
1070
+ }
1071
+ }
914
1072
  pruneOverlaps() {
915
1073
  for (const [self, others] of this.overlaps) {
916
1074
  if (self.tree === null) {
@@ -1273,8 +1431,12 @@ function parseCollider3D(R, node) {
1273
1431
  * body — no duplicated data in the collider prop, terrain and collider can
1274
1432
  * never drift apart.
1275
1433
  */
1434
+ /** The Terrain3D child a heightfield body reads its grid from. */
1435
+ function heightfieldSource(node) {
1436
+ return node.children.find((c) => c instanceof Terrain3D);
1437
+ }
1276
1438
  function buildHeightfieldDesc3D(R, node) {
1277
- const terrain = node.children.find((c) => c instanceof Terrain3D);
1439
+ const terrain = heightfieldSource(node);
1278
1440
  if (!terrain) throw new IncantoError("BAD_FORMAT", `Collider on '${node.name}': heightfield needs a Terrain3D CHILD of this body — add { "type": "Terrain3D" } under the StaticBody3D; the collider reads its height grid.`, { prop: "collider" });
1279
1441
  const hm = terrain._heightmap();
1280
1442
  const nrows = hm.segsZ;
@@ -1348,5 +1510,30 @@ function buildColliderDesc3D(R, collider, nodeName, node) {
1348
1510
  }
1349
1511
  throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'auto', 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
1350
1512
  }
1513
+ /**
1514
+ * A static body's authored surface, and the rule that makes it WIN the pair.
1515
+ *
1516
+ * Rapier combines two colliders' coefficients by the higher-priority rule of
1517
+ * the two (Max > Multiply > Min > Average) and defaults to Average, which is
1518
+ * why a 0.75 ball on a fixed-0 floor kept 37% of its speed. The surface an
1519
+ * author writes on a floor is an intent about whatever touches it: a
1520
+ * trampoline launches a dead ball (Max), an ice patch slides a grippy crate
1521
+ * (Min), glue stops a slick one (Max). At the defaults nothing is set, so a
1522
+ * scene that says nothing pairs exactly as it always did.
1523
+ *
1524
+ * The same rule on the MOVING body, because the character skill had said for
1525
+ * a year "set `friction: 0` on the body" and nothing honoured it: a
1526
+ * RigidBody's 0 met the floor's unwritten 0.5 under Average and became 0.25,
1527
+ * and every 3D character walked at 3.7 where the skill promised 4.0. An
1528
+ * authored value wins the pair whichever side wrote it; when both sides
1529
+ * speak, Rapier's priority order (Max > Multiply > Min > Average) decides.
1530
+ */
1531
+ function applySurface(R, target, node) {
1532
+ const Rule = R.CoefficientCombineRule;
1533
+ target.setFriction(node.friction);
1534
+ target.setFrictionCombineRule(node.friction === .5 ? Rule.Average : node.friction < .5 ? Rule.Min : Rule.Max);
1535
+ target.setRestitution(node.restitution);
1536
+ target.setRestitutionCombineRule(node.restitution > 0 ? Rule.Max : Rule.Average);
1537
+ }
1351
1538
  //#endregion
1352
1539
  export { enablePhysics3D as n, physics_3d_exports as r, Physics3D as t };
@@ -1,7 +1,7 @@
1
- import { _ as AudioPlayer } from "./register-5IYfooAN.js";
1
+ import { _ as AudioPlayer } from "./register-CjociOtt.js";
2
2
  import { i as logReport, o as parseDrive } from "./webgl-unavailable-C8aDbGmR.js";
3
3
  import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-D-_7YF2G.js";
4
- import { Q as worldPosition } from "./gameplay-BlQe-M07.js";
4
+ import { et as worldPosition } from "./gameplay-Bxe1sMVT.js";
5
5
  //#region src/core/audio-errors.ts
6
6
  /** Every AudioPlayer under `root` whose source could not be played. */
7
7
  function audioErrors(root) {
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-Bf9P9oB2.js";
1
+ import { P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-J1cmVah0.js";
2
2
  import { c as JsonValue } from "./rng-Bb-IutXB.js";
3
3
  import { CSSProperties, ReactNode } from "react";
4
4
 
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-CmFAtLZp.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-C9rWqqAC.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-BAiA-FjP.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-ssvGvP7W.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,8 +1,8 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { B as HudLayer, F as resolveOrderGroups, G as UiText, H as TAPPABLE, I as InputMap, J as Node, N as ORDER_GROUP_BASE, U as UiBanner, V as HudWidgetBase, W as UiBar, Y as diagnose, a as restoreBehaviors, ct as Signal, i as captureBehaviors, j as createSaveStore, q as requireText, t as ENGINE_STATE_KEY, z as FITS } from "./save-slots-CEuJPUle.js";
2
+ import { B as HudLayer, F as resolveOrderGroups, G as UiText, H as TAPPABLE, I as InputMap, J as Node, N as ORDER_GROUP_BASE, U as UiBanner, V as HudWidgetBase, W as UiBar, Y as diagnose, a as restoreBehaviors, ct as Signal, i as captureBehaviors, j as createSaveStore, q as requireText, t as ENGINE_STATE_KEY, z as FITS } from "./save-slots-CEUGeLuB.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-CDOMybym.js";
5
- import { p as registerNode } from "./touch-BnCyPA0G.js";
5
+ import { p as registerNode } from "./touch-CioTZB-y.js";
6
6
  //#region src/core/audio/buses.ts
7
7
  const AUDIO_LOG_LIMIT = 200;
8
8
  var AudioBuses = class {
@@ -1366,6 +1366,14 @@ var EffectLog = class {
1366
1366
  countOf(name) {
1367
1367
  return this.events.reduce((n, e) => e.name === name ? n + 1 : n, 0);
1368
1368
  }
1369
+ /**
1370
+ * How many effects of this KIND fired (`shake`, `hitstop`, `burst`, `sfx`).
1371
+ * A shake has no name, so `countOf('shake')` answered 0 for a boss whose
1372
+ * every slam shook the camera — the harness that asked reached for the kind.
1373
+ */
1374
+ countKind(kind) {
1375
+ return this.events.reduce((n, e) => e.kind === kind ? n + 1 : n, 0);
1376
+ }
1369
1377
  /** How many effects this node fired — the question a wiring check has. */
1370
1378
  countFrom(path) {
1371
1379
  return this.events.reduce((n, e) => e.from === path ? n + 1 : n, 0);
@@ -3259,7 +3267,10 @@ var Respawn = class extends Node {
3259
3267
  default: "..",
3260
3268
  nodePath: true
3261
3269
  },
3262
- below: { default: null },
3270
+ below: {
3271
+ default: null,
3272
+ kinds: ["null", "number"]
3273
+ },
3263
3274
  to: { default: [] },
3264
3275
  resetVelocity: { default: true }
3265
3276
  };
@@ -3333,7 +3344,7 @@ var Respawn = class extends Node {
3333
3344
  const y = worldY(target);
3334
3345
  if ((y - line) * this.down <= 0) return;
3335
3346
  this.falls += 1;
3336
- setPosition(target, this.home);
3347
+ setPosition(target, this.to.length > 0 ? this.to : this.home);
3337
3348
  if (this.resetVelocity) zeroVelocity(target);
3338
3349
  this.emit("respawned", target, y);
3339
3350
  }
@@ -3389,6 +3400,10 @@ var Timer = class extends Node {
3389
3400
  autostart = false;
3390
3401
  running = false;
3391
3402
  remaining = 0;
3403
+ /** Seconds until the next `timeout` — a countdown a HUD can draw. `0` when stopped. */
3404
+ get timeLeft() {
3405
+ return this.running ? Math.max(0, this.remaining) : 0;
3406
+ }
3392
3407
  start(time = this.waitTime) {
3393
3408
  this.waitTime = time;
3394
3409
  this.remaining = this.waitTime;
@@ -1,9 +1,9 @@
1
- import { J as Node, P as effectiveOrder, Y as diagnose } from "./save-slots-CEuJPUle.js";
2
- import { M as translateOn, t as registerCoreNodes } from "./register-5IYfooAN.js";
1
+ import { J as Node, P as effectiveOrder, 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
- import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-BnCyPA0G.js";
5
- 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";
6
- import { D as effectRng, Q as worldPosition, T as tolerateUnknownAction, Z as localFromWorld } from "./gameplay-BlQe-M07.js";
4
+ import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-CioTZB-y.js";
5
+ 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";
6
+ import { $ as localFromWorld, E as tolerateUnknownAction, O as effectRng, et as worldPosition } from "./gameplay-Bxe1sMVT.js";
7
7
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
8
8
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
9
9
  //#region src/2d/physics/collider-validate.ts
@@ -235,9 +235,27 @@ var PhysicsBody2D = class extends Node2D {
235
235
  /** @internal Set by Physics2D when the body is created. */
236
236
  _physics = null;
237
237
  };
238
- /** Immovable collider (ground, walls, platforms). */
238
+ /**
239
+ * Immovable collider (ground, walls, platforms).
240
+ *
241
+ * It also carries the SURFACE it is: `friction` and `restitution` used to
242
+ * live on `RigidBody2D` alone, so a trampoline, an ice patch or a sticky
243
+ * landing could not be written in scene JSON, and Rapier's AVERAGE rule
244
+ * halved whatever a ball brought to a plain floor. A static body's authored
245
+ * value WINS the pair — restitution above 0 combines by Max, friction below
246
+ * the default by Min and above it by Max — so the pad decides, whatever lands
247
+ * on it. Left at the defaults, nothing changes for any existing scene.
248
+ */
239
249
  var StaticBody2D = class extends PhysicsBody2D {
240
250
  static typeName = "StaticBody2D";
251
+ static props = {
252
+ friction: { default: .5 },
253
+ restitution: { default: 0 }
254
+ };
255
+ /** 0.5 plain · 0 ice (slides on and on) · 1+ glue. Away from 0.5 it wins the pair. */
256
+ friction = .5;
257
+ /** 0 dead · 1 trampoline. Above 0 it wins the pair: a dead ball still bounces. */
258
+ restitution = 0;
241
259
  };
242
260
  /**
243
261
  * Sensor volume. Emits `triggerEnter(other)` / `triggerExit(other)` — the
@@ -264,13 +282,25 @@ var RigidBody2D = class extends PhysicsBody2D {
264
282
  friction: { default: .5 },
265
283
  restitution: { default: 0 },
266
284
  linearVelocity: { default: [0, 0] },
267
- angularVelocity: { default: 0 }
285
+ angularVelocity: { default: 0 },
286
+ linearDamping: {
287
+ default: 0,
288
+ range: { min: 0 }
289
+ },
290
+ angularDamping: {
291
+ default: 0,
292
+ range: { min: 0 }
293
+ }
268
294
  };
269
295
  mass = 1;
270
296
  gravityScale = 1;
271
297
  fixedRotation = false;
272
298
  friction = .5;
273
299
  restitution = 0;
300
+ /** Drag, per second: velocity decays by `e^(-linearDamping·t)`. `0` = none. */
301
+ linearDamping = 0;
302
+ /** Rolling resistance, per second, on the spin — see `RigidBody3D.angularDamping`. */
303
+ angularDamping = 0;
274
304
  /** px/s, y-down. Read back every step; write to launch. */
275
305
  linearVelocity = [0, 0];
276
306
  /**
@@ -326,7 +356,8 @@ var CharacterBody2D = class extends PhysicsBody2D {
326
356
  stepHeight: {
327
357
  default: 35,
328
358
  range: { min: 0 }
329
- }
359
+ },
360
+ collideWithCharacters: { default: true }
330
361
  };
331
362
  /**
332
363
  * Renamed to match the 3D body, where `snapToGround` had to give the name
@@ -351,6 +382,8 @@ var CharacterBody2D = class extends PhysicsBody2D {
351
382
  * `0` restores the old behaviour.
352
383
  */
353
384
  stepHeight = 35;
385
+ /** See CharacterBody3D.collideWithCharacters — off, a crowd ignores itself and costs nothing. */
386
+ collideWithCharacters = true;
354
387
  /** @internal Updated by Physics2D.moveAndSlide. */
355
388
  _grounded = false;
356
389
  moveAndSlide() {
@@ -419,7 +452,10 @@ var Joint2D = class extends Node2D {
419
452
  length: { default: 0 },
420
453
  stiffness: { default: 50 },
421
454
  damping: { default: 5 },
422
- collide: { default: null }
455
+ collide: {
456
+ default: null,
457
+ kinds: ["null", "boolean"]
458
+ }
423
459
  };
424
460
  type = "revolute";
425
461
  target = "";
@@ -1,7 +1,25 @@
1
- import { L as handlerShadowWarnings, R as sceneDimension, V as HudWidgetBase, Z as nodeRefWarnings } from "./save-slots-CEuJPUle.js";
2
- import { N as translationKey } from "./register-5IYfooAN.js";
1
+ import { L as handlerShadowWarnings, R as sceneDimension, V as HudWidgetBase, Z as nodeRefWarnings } from "./save-slots-CEUGeLuB.js";
2
+ import { N as translationKey } from "./register-CjociOtt.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { u as getNodeType } from "./touch-BnCyPA0G.js";
4
+ import { u as getNodeType } from "./touch-CioTZB-y.js";
5
+ //#region src/core/light-types.ts
6
+ /**
7
+ * The node types that are LIGHTS — one set, read by everything that has to
8
+ * decide.
9
+ *
10
+ * `auditScene` counts them (a 3D scene with none is a warning), `framing`
11
+ * leaves them out of the bounds (a sun at y 100 is not "the game"), and the
12
+ * picker never lets a click land on one. Three files each kept their own copy
13
+ * of this set, and the day a third light arrived every copy would have been a
14
+ * place to forget. The test beside `SpotLight3D` asks the registry for every
15
+ * `*Light3D` and checks it is here.
16
+ */
17
+ const LIGHT_NODE_TYPES = new Set([
18
+ "DirectionalLight3D",
19
+ "OmniLight3D",
20
+ "SpotLight3D"
21
+ ]);
22
+ //#endregion
5
23
  //#region src/core/audit.ts
6
24
  /**
7
25
  * Behaviours whose ENTIRE observable effect is the signals they emit.
@@ -71,7 +89,7 @@ const DRAPERS = new Set([
71
89
  "Tree3D",
72
90
  "River3D"
73
91
  ]);
74
- const LIGHT_TYPES = new Set(["DirectionalLight3D", "OmniLight3D"]);
92
+ const LIGHT_TYPES = LIGHT_NODE_TYPES;
75
93
  /**
76
94
  * Nodes whose gameplay answer comes from a CPU twin built out of `size`.
77
95
  *
@@ -124,6 +142,13 @@ function isLit(environment) {
124
142
  * Offsets compose; rotation and scale do not — the body is placed at its WORLD
125
143
  * position and given its OWN rotation, so an ancestor's is simply lost.
126
144
  */
145
+ /**
146
+ * Node types the engine ROTATES at runtime — a skin, yawed by
147
+ * `CharacterController3D.skinPath` and `Chase`/`Patrol.facePath`. Billboards
148
+ * and sprites turn too, toward the CAMERA, and a collider that does not follow
149
+ * that is the right collider, so they are not listed.
150
+ */
151
+ const TURNED_AT_RUNTIME = new Set(["ModelInstance3D"]);
127
152
  function transformOf(props) {
128
153
  const rot = props.rotation;
129
154
  const scale = props.scale;
@@ -194,6 +219,17 @@ function auditScene(scene, opts = {}) {
194
219
  const colours = props.colors ?? {};
195
220
  if (cells.length > 0 && (props.texture ?? "") === "" && Object.keys(colours).length === 0) warnings.push(`${path}: TileMap2D has ${cells.length} row(s) and neither "texture" nor "colors", so nothing of it is drawn. Give it a "colors" entry per legend char to see the level before the art arrives. (If it is meant to be an invisible collision map over scenery you drew yourself, this warning is the price of saying so.)`);
196
221
  }
222
+ if (!onShelf && props.visible !== false) {
223
+ const art = {
224
+ ModelInstance3D: ["model", "a `$model` asset ref"],
225
+ Sprite3D: ["texture", "an image URL"],
226
+ AnimatedSprite3D: ["sheet", "a spritesheet URL (with frameWidth/frameHeight)"]
227
+ }[type];
228
+ if (art && ((props[art[0]] ?? "") === "" || props[art[0]] === void 0)) warnings.push(`${path}: ${type} has no "${art[0]}", so nothing of it is drawn — it loads, validates and takes up a row in the tree, and puts no pixel on screen. Give it ${art[1]}.`);
229
+ if (type === "InstancedMesh3D") {
230
+ if ((Array.isArray(props.transforms) ? props.transforms : []).length === 0) warnings.push(`${path}: InstancedMesh3D has no "transforms", so it draws nothing — a scatter with no rows. Add [x, y, z, yawDeg?, scale?] rows, or fill them from a behaviour and say so.`);
231
+ }
232
+ }
197
233
  if (type === "CharacterController3D") cameraDrivers.push(path);
198
234
  if (type === "Terrain3D") terrains.push(path);
199
235
  if (DRAPERS.has(type) && (props.terrain ?? "") === "") autoDrapers.push(path);
@@ -228,7 +264,7 @@ function auditScene(scene, opts = {}) {
228
264
  if (staticRoot && ANIMATED_TYPES.has(type)) warnings.push(`${path}: ${type} is frozen inside static subtree '${staticRoot}' — it will not animate/update on screen.`);
229
265
  const nextStatic = staticRoot ?? (props.static === true ? node.name ?? path : null);
230
266
  const nextHud = insideHud || type === "HudLayer";
231
- const own = transformOf(props);
267
+ const own = transformOf(props) ?? (TURNED_AT_RUNTIME.has(type) ? `runtime yaw (a ${type} is what the controller and Chase turn)` : null);
232
268
  const nextTransformed = own ? {
233
269
  path,
234
270
  what: own
@@ -599,4 +635,4 @@ function replay(engine, recording, opts) {
599
635
  }
600
636
  }
601
637
  //#endregion
602
- export { instancedScenes as i, startRecording as n, auditScene as r, replay as t };
638
+ export { LIGHT_NODE_TYPES as a, instancedScenes as i, startRecording as n, auditScene as r, replay as t };
@@ -1,4 +1,4 @@
1
- import { b as Engine } from "./behavior-Bf9P9oB2.js";
1
+ import { b as Engine } from "./behavior-J1cmVah0.js";
2
2
  import { s as JsonObject } from "./rng-Bb-IutXB.js";
3
3
 
4
4
  //#region src/core/audit.d.ts
@@ -1,6 +1,6 @@
1
1
  import { n as jsonEquals, o as __exportAll, t as jsonClone } from "./json-CfTjpvW8.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { a as applySchemaProps, c as getNodeSchema, d as mergeStaticProps, f as mergeStaticSignals, g as nearestName, h as didYouMean, r as capturePointer, s as createNode, u as getNodeType } from "./touch-BnCyPA0G.js";
3
+ import { a as applySchemaProps, c as getNodeSchema, d as mergeStaticProps, f as mergeStaticSignals, g as nearestName, h as didYouMean, r as capturePointer, s as createNode, u as getNodeType } from "./touch-CioTZB-y.js";
4
4
  //#region src/core/signal.ts
5
5
  var Signal = class {
6
6
  connections = [];
@@ -150,6 +150,10 @@ var Behavior = class {
150
150
  getNode(path) {
151
151
  return this.node.getNode(path);
152
152
  }
153
+ /** The node's `getNodeOrNull`, for the same reason `getNode` is here. */
154
+ getNodeOrNull(path) {
155
+ return this.node.getNodeOrNull(path);
156
+ }
153
157
  emit(signal, ...args) {
154
158
  this.node.emit(signal, ...args);
155
159
  }
@@ -4505,7 +4509,12 @@ function loadScene(json, opts) {
4505
4509
  } : sceneJson, collected), broughtAssets), broughtInput), broughtBands);
4506
4510
  if (opts?.engine && full.strings) opts.engine.locale.load(full.strings);
4507
4511
  wireConnections(root, sceneJson.connections ?? [], opts?.declareConnectionSignals);
4508
- tree.setRoot(root);
4512
+ try {
4513
+ tree.setRoot(root);
4514
+ } catch (error) {
4515
+ root.free();
4516
+ throw error;
4517
+ }
4509
4518
  loadingAssets = null;
4510
4519
  return new Scene(full, root, tree);
4511
4520
  }
@@ -1,8 +1,8 @@
1
1
  import { n as jsonEquals, t as jsonClone } from "./json-CfTjpvW8.js";
2
- import { J as Node, Y as diagnose, ct as Signal, d as rebuildOptions, h as loadScene, l as markCloned, m as buildSceneSubtree, p as stripUids, u as markTemplateName } from "./save-slots-CEuJPUle.js";
3
- import { t as registerCoreNodes, v as Engine } from "./register-5IYfooAN.js";
2
+ import { J as Node, Y as diagnose, ct as Signal, d as rebuildOptions, h as loadScene, l as markCloned, m as buildSceneSubtree, p as stripUids, u as markTemplateName } from "./save-slots-CEUGeLuB.js";
3
+ import { t as registerCoreNodes, v as Engine } from "./register-CjociOtt.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { p as registerNode } from "./touch-BnCyPA0G.js";
5
+ import { p as registerNode } from "./touch-CioTZB-y.js";
6
6
  //#region src/net/loopback.ts
7
7
  /**
8
8
  * In-memory multiplayer hub implementing the SAME kernel contract as
@@ -1150,7 +1150,7 @@ function applySyncPatch(root, patch, setProp, onMissingNode) {
1150
1150
  }
1151
1151
  }
1152
1152
  async function createDefaultTransport(config, diagnostics) {
1153
- const { createAgent8Server } = await import("./agent8-BqZ0_O1u.js").then((n) => n.t);
1153
+ const { createAgent8Server } = await import("./agent8-CHTT4unP.js").then((n) => n.t);
1154
1154
  return createAgent8Server(config, diagnostics);
1155
1155
  }
1156
1156
  //#endregion