incanto 0.74.0 → 0.76.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 (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +861 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +29 -8
  54. package/skills/incanto-environment.md +257 -14
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +398 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -1,10 +1,10 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { Y as diagnose } from "./save-slots-CEUGeLuB.js";
2
+ import { et as diagnose } from "./pose-ByFB_J3O.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.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";
4
+ import { $ as Water3D, at as PhysicsBody3D, ct as Node3D, it as CharacterBody3D, lt as validateCollider3D, ot as RigidBody3D, rt as Area3D, st as StaticBody3D } from "./gameplay-Bfff_beb.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 { F as Joint3D, I as InstancedMesh3D, R as buildMeshGeometry, j as Terrain3D } from "./environment-presets-BkqlWewf.js";
7
+ import { B as InstancedMesh3D, F as Terrain3D, H as buildMeshGeometry, z as Joint3D } from "./environment-presets-e_9YCnGu.js";
8
8
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
9
9
  //#region src/3d/physics/collider-lines.ts
10
10
  /**
@@ -210,6 +210,89 @@ var physics_3d_exports = /* @__PURE__ */ __exportAll({
210
210
  });
211
211
  const DEG = Math.PI / 180;
212
212
  /**
213
+ * A hinge's angle: body A's rotation relative to body B, about A's local
214
+ * `axis`, in degrees on (-180, 180]. The twist of the relative quaternion
215
+ * about the axis — `2·atan2(q·n, q.w)` — which is exactly the turn when the
216
+ * joint holds every other axis still.
217
+ */
218
+ /**
219
+ * How far two bodies disagree about a joint axis, in degrees: the part of
220
+ * their relative rotation that is NOT about `axis`. Zero when they share a
221
+ * rotation, or differ only by a turn about the axis itself.
222
+ */
223
+ function axisDisagreementDeg(qa, qb, axis) {
224
+ const bx = -qb.x;
225
+ const by = -qb.y;
226
+ const bz = -qb.z;
227
+ const bw = qb.w;
228
+ const rx = bw * qa.x + bx * qa.w + by * qa.z - bz * qa.y;
229
+ const ry = bw * qa.y - bx * qa.z + by * qa.w + bz * qa.x;
230
+ const rz = bw * qa.z + bx * qa.y - by * qa.x + bz * qa.w;
231
+ const rw = bw * qa.w - bx * qa.x - by * qa.y - bz * qa.z;
232
+ const n = Math.hypot(axis[0] ?? 0, axis[1] ?? 0, axis[2] ?? 0) || 1;
233
+ const ux = (axis[0] ?? 0) / n;
234
+ const uy = (axis[1] ?? 0) / n;
235
+ const uz = (axis[2] ?? 0) / n;
236
+ const proj = rx * ux + ry * uy + rz * uz;
237
+ const tw = Math.hypot(proj, rw) || 1;
238
+ const twist = {
239
+ x: proj * ux / tw,
240
+ y: proj * uy / tw,
241
+ z: proj * uz / tw,
242
+ w: rw / tw
243
+ };
244
+ const sw = rw * twist.w + rx * twist.x + ry * twist.y + rz * twist.z;
245
+ return Math.abs(2 * Math.acos(Math.min(1, Math.abs(sw))) / DEG);
246
+ }
247
+ /** `v` turned by the quaternion `q`. */
248
+ function rotateVec3(q, x, y, z) {
249
+ const cx = q.y * z - q.z * y;
250
+ const cy = q.z * x - q.x * z;
251
+ const cz = q.x * y - q.y * x;
252
+ return [
253
+ x + 2 * (q.w * cx + (q.y * cz - q.z * cy)),
254
+ y + 2 * (q.w * cy + (q.z * cx - q.x * cz)),
255
+ z + 2 * (q.w * cz + (q.x * cy - q.y * cx))
256
+ ];
257
+ }
258
+ /** `offset`, turned by the rotation that takes `qFrom` to `qNow` (world frame). */
259
+ function rotateByDelta(qNow, qFrom, ox, oy, oz) {
260
+ const [fx, fy, fz, fw] = [
261
+ -qFrom[0],
262
+ -qFrom[1],
263
+ -qFrom[2],
264
+ qFrom[3]
265
+ ];
266
+ const dx = qNow.w * fx + qNow.x * fw + qNow.y * fz - qNow.z * fy;
267
+ const dy = qNow.w * fy - qNow.x * fz + qNow.y * fw + qNow.z * fx;
268
+ const dz = qNow.w * fz + qNow.x * fy - qNow.y * fx + qNow.z * fw;
269
+ const dw = qNow.w * fw - qNow.x * fx - qNow.y * fy - qNow.z * fz;
270
+ const cx = dy * oz - dz * oy;
271
+ const cy = dz * ox - dx * oz;
272
+ const cz = dx * oy - dy * ox;
273
+ return [
274
+ ox + 2 * (dw * cx + (dy * cz - dz * cy)),
275
+ oy + 2 * (dw * cy + (dz * cx - dx * cz)),
276
+ oz + 2 * (dw * cz + (dx * cy - dy * cx))
277
+ ];
278
+ }
279
+ function hingeAngle3D(qa, qb, axis) {
280
+ const bx = -qb.x;
281
+ const by = -qb.y;
282
+ const bz = -qb.z;
283
+ const bw = qb.w;
284
+ const rx = bw * qa.x + bx * qa.w + by * qa.z - bz * qa.y;
285
+ const ry = bw * qa.y - bx * qa.z + by * qa.w + bz * qa.x;
286
+ const rz = bw * qa.z + bx * qa.y - by * qa.x + bz * qa.w;
287
+ const rw = bw * qa.w - bx * qa.x - by * qa.y - bz * qa.z;
288
+ const n = Math.hypot(axis[0] ?? 0, axis[1] ?? 0, axis[2] ?? 0) || 1;
289
+ const proj = (rx * (axis[0] ?? 0) + ry * (axis[1] ?? 0) + rz * (axis[2] ?? 0)) / n;
290
+ let deg = 2 * Math.atan2(proj, rw) / DEG;
291
+ if (deg > 180) deg -= 360;
292
+ if (deg <= -180) deg += 360;
293
+ return deg;
294
+ }
295
+ /**
213
296
  * Enable 3D physics for an engine. Dynamically imports Rapier (compat build)
214
297
  * so games without physics never pay its bundle cost. 1 unit = 1 meter, y-up —
215
298
  * three's native space, no conversion.
@@ -232,6 +315,11 @@ var Physics3D = class {
232
315
  warnedNoCollider = /* @__PURE__ */ new WeakSet();
233
316
  entries = /* @__PURE__ */ new Map();
234
317
  byColliderHandle = /* @__PURE__ */ new Map();
318
+ /** Every Water3D in the tree, rescanned when the structure changes. */
319
+ waters = [];
320
+ watersStale = true;
321
+ /** Raycast vehicles, one per Vehicle3D in the tree, on their chassis entries. */
322
+ vehicles = /* @__PURE__ */ new Map();
235
323
  world;
236
324
  events;
237
325
  kcc;
@@ -294,16 +382,18 @@ var Physics3D = class {
294
382
  }
295
383
  }
296
384
  this.syncBodies();
385
+ this.applyBuoyancy(dt);
297
386
  this.world.timestep = dt;
298
387
  this.world.step(this.events);
299
388
  for (const [node, e] of this.entries) {
300
389
  const t = e.body.translation();
301
390
  if (!e.body.isFixed() && !(node instanceof Area3D)) {
302
- const [ox, oy, oz] = parentWorldOffset3D(node);
391
+ parentPose3D(node);
392
+ localV.set(t.x, t.y, t.z).sub(parentPosV).applyQuaternion(invQ.copy(parentQuatQ).invert());
303
393
  const next = [
304
- t.x - ox,
305
- t.y - oy,
306
- t.z - oz
394
+ localV.x,
395
+ localV.y,
396
+ localV.z
307
397
  ];
308
398
  node._interpPrev = node._interpCurr ?? next;
309
399
  node._interpCurr = next;
@@ -315,7 +405,14 @@ var Physics3D = class {
315
405
  if (node instanceof RigidBody3D) {
316
406
  if (!node.fixedRotation) {
317
407
  const q = e.body.rotation();
318
- eulerScratch.setFromQuaternion(quatScratch.set(q.x, q.y, q.z, q.w), "XYZ");
408
+ quatScratch.set(q.x, q.y, q.z, q.w);
409
+ e.lastQuat[0] = q.x;
410
+ e.lastQuat[1] = q.y;
411
+ e.lastQuat[2] = q.z;
412
+ e.lastQuat[3] = q.w;
413
+ parentPose3D(node);
414
+ localQ.copy(parentQuatQ).invert().multiply(quatScratch);
415
+ eulerScratch.setFromQuaternion(localQ, "XYZ");
319
416
  const rot = node.rotation;
320
417
  rot[0] = eulerScratch.x / DEG;
321
418
  rot[1] = eulerScratch.y / DEG;
@@ -337,6 +434,9 @@ var Physics3D = class {
337
434
  e.lastV[0] = lv.x;
338
435
  e.lastV[1] = lv.y;
339
436
  e.lastV[2] = lv.z;
437
+ e.lastAV[0] = av.x;
438
+ e.lastAV[1] = av.y;
439
+ e.lastAV[2] = av.z;
340
440
  }
341
441
  const t2 = e.body.translation();
342
442
  e.last[0] = t2.x;
@@ -347,6 +447,7 @@ var Physics3D = class {
347
447
  const a = this.byColliderHandle.get(h1);
348
448
  const b = this.byColliderHandle.get(h2);
349
449
  if (!a || !b) return;
450
+ if (!a.enabled || !b.enabled) return;
350
451
  const sig = started ? "triggerEnter" : "triggerExit";
351
452
  if (started === (this.overlaps.get(a)?.has(b) ?? false)) return;
352
453
  this.trackOverlap(a, b, started);
@@ -370,13 +471,12 @@ var Physics3D = class {
370
471
  */
371
472
  poseFromTree() {
372
473
  for (const [node, e] of this.entries) {
373
- const [wx, wy, wz] = worldPosition3D(node);
474
+ worldPose3D(node, wpV, quatScratch);
374
475
  e.body.setTranslation({
375
- x: wx,
376
- y: wy,
377
- z: wz
476
+ x: wpV.x,
477
+ y: wpV.y,
478
+ z: wpV.z
378
479
  }, false);
379
- quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
380
480
  e.body.setRotation({
381
481
  x: quatScratch.x,
382
482
  y: quatScratch.y,
@@ -386,6 +486,77 @@ var Physics3D = class {
386
486
  }
387
487
  this.world.propagateModifiedBodyPositionsToColliders();
388
488
  }
489
+ /**
490
+ * @internal Called by Vehicle3D.fixedUpdate: push the throttle, steer and
491
+ * brake into Rapier's raycast vehicle on the chassis and let it apply the
492
+ * step's forces; read the wheels back. The controller is made on first
493
+ * use and remade if the chassis body was rebuilt.
494
+ */
495
+ _driveVehicle(node, dt) {
496
+ const e = this.ensureEntry(node.body);
497
+ if (!e) return;
498
+ const R = this.R;
499
+ let v = this.vehicles.get(node);
500
+ if (!v || v.chassis !== e.body) {
501
+ if (v) v.ctrl.free();
502
+ const ctrl = new R.DynamicRayCastVehicleController(e.body, this.world.broadPhase, this.world.narrowPhase, this.world.bodies, this.world.colliders);
503
+ ctrl.indexUpAxis = 1;
504
+ ctrl.setIndexForwardAxis = 2;
505
+ node.specs().forEach((w, i) => {
506
+ ctrl.addWheel({
507
+ x: w.position[0],
508
+ y: w.position[1],
509
+ z: w.position[2]
510
+ }, {
511
+ x: 0,
512
+ y: -1,
513
+ z: 0
514
+ }, {
515
+ x: -1,
516
+ y: 0,
517
+ z: 0
518
+ }, w.rest, w.radius);
519
+ ctrl.setWheelSuspensionStiffness(i, node.suspensionStiffness);
520
+ ctrl.setWheelSuspensionCompression(i, node.suspensionCompression);
521
+ ctrl.setWheelSuspensionRelaxation(i, node.suspensionRelaxation);
522
+ ctrl.setWheelMaxSuspensionTravel(i, node.maxTravel);
523
+ ctrl.setWheelFrictionSlip(i, node.friction);
524
+ ctrl.setWheelSideFrictionStiffness(i, node.sideFriction);
525
+ });
526
+ v = {
527
+ ctrl,
528
+ chassis: e.body
529
+ };
530
+ this.vehicles.set(node, v);
531
+ }
532
+ const ctrl = v.ctrl;
533
+ const specs = node.specs();
534
+ const steer = Math.max(-1, Math.min(1, node.steer)) * node.maxSteer;
535
+ const throttle = Math.max(-1, Math.min(1, node.throttle)) * node.engineForce;
536
+ const brake = Math.max(0, Math.min(1, node.brake)) * node.brakeForce;
537
+ specs.forEach((w, i) => {
538
+ ctrl.setWheelSteering(i, w.steer ? -steer : 0);
539
+ ctrl.setWheelEngineForce(i, w.drive ? throttle : 0);
540
+ ctrl.setWheelBrake(i, brake);
541
+ });
542
+ e.body.wakeUp();
543
+ const own = e.collider.handle;
544
+ ctrl.updateVehicle(dt, void 0, void 0, (c) => c.handle !== own && !(this.byColliderHandle.get(c.handle) instanceof Area3D));
545
+ node.speed = ctrl.currentVehicleSpeed();
546
+ node._states = specs.map((w, i) => ({
547
+ contact: ctrl.wheelIsInContact(i),
548
+ suspension: ctrl.wheelSuspensionLength(i) ?? w.rest,
549
+ rotation: ctrl.wheelRotation(i) ?? 0,
550
+ steering: ctrl.wheelSteering(i) ?? 0
551
+ }));
552
+ }
553
+ /** @internal The Vehicle3D left the tree: drop its controller. */
554
+ _freeVehicle(node) {
555
+ const v = this.vehicles.get(node);
556
+ if (!v) return;
557
+ v.ctrl.free();
558
+ this.vehicles.delete(node);
559
+ }
389
560
  /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
390
561
  /**
391
562
  * May `candidate` carry `rider`? — see the 2D twin.
@@ -395,6 +566,7 @@ var Physics3D = class {
395
566
  * the guard was really for is a carry CYCLE, so that is what it tests.
396
567
  */
397
568
  canCarry(rider, candidate) {
569
+ if (this.jointed.get(rider)?.has(candidate)) return false;
398
570
  let at = candidate;
399
571
  for (let depth = 0; depth < 8 && at; depth++) {
400
572
  if (at === rider) return false;
@@ -419,13 +591,34 @@ var Physics3D = class {
419
591
  carryX = gx - e.groundAt[0];
420
592
  carryY = gy - e.groundAt[1];
421
593
  carryZ = gz - e.groundAt[2];
594
+ const gb = this.entries.get(e.ground);
595
+ if (gb && e.groundRot && e.riderAt) {
596
+ const q = gb.body.rotation();
597
+ const [ox, oy, oz] = [
598
+ e.riderAt[0] - e.groundAt[0],
599
+ e.riderAt[1] - e.groundAt[1],
600
+ e.riderAt[2] - e.groundAt[2]
601
+ ];
602
+ const [rx, ry, rz] = rotateByDelta(q, e.groundRot, ox, oy, oz);
603
+ carryX += rx - ox;
604
+ carryY += ry - oy;
605
+ carryZ += rz - oz;
606
+ }
422
607
  } else e.ground = null;
423
608
  const desired = {
424
609
  x: (node.velocity[0] ?? 0) * this.lastDt + carryX,
425
610
  y: vy * this.lastDt + carryY,
426
611
  z: (node.velocity[2] ?? 0) * this.lastDt + carryZ
427
612
  };
428
- const others = node.collideWithCharacters ? void 0 : (c) => !(this.byColliderHandle.get(c.handle) instanceof CharacterBody3D);
613
+ const jointedTo = this.jointed.get(node);
614
+ const noContact = jointedTo && [...jointedTo.values()].some((on) => !on) ? jointedTo : null;
615
+ const others = node.collideWithCharacters && !noContact ? void 0 : (c) => {
616
+ const other = this.byColliderHandle.get(c.handle);
617
+ if (!node.collideWithCharacters && other instanceof CharacterBody3D) return false;
618
+ return !(other && noContact?.get(other) === false);
619
+ };
620
+ this.kcc.setApplyImpulsesToDynamicBodies(node.pushes > 0);
621
+ if (node.pushes > 0) this.kcc.setCharacterMass(node.pushes);
429
622
  this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS, void 0, others);
430
623
  const c = this.kcc.computedMovement();
431
624
  const t = e.body.translation();
@@ -465,6 +658,18 @@ var Physics3D = class {
465
658
  ], .4, node);
466
659
  if (hit?.node && this.canCarry(node, hit.node)) {
467
660
  e.ground = hit.node;
661
+ const gq = this.entries.get(hit.node)?.body.rotation();
662
+ e.groundRot = gq ? [
663
+ gq.x,
664
+ gq.y,
665
+ gq.z,
666
+ gq.w
667
+ ] : void 0;
668
+ e.riderAt = [
669
+ nx,
670
+ ny - half,
671
+ nz
672
+ ];
468
673
  const at = worldPosition3D(hit.node);
469
674
  e.groundAt = [
470
675
  at[0],
@@ -519,6 +724,42 @@ var Physics3D = class {
519
724
  lastRoot = null;
520
725
  jointSet = /* @__PURE__ */ new Set();
521
726
  joints = /* @__PURE__ */ new Map();
727
+ /** The two bodies each live joint ties, so a departed joint can be unlinked. */
728
+ jointBodies = /* @__PURE__ */ new Map();
729
+ /** What a hinge's limits/motor were last pushed as, so a write re-applies. */
730
+ hingeState = /* @__PURE__ */ new Map();
731
+ /** Joints refused at creation (bodies that disagree about the axis) — said once. */
732
+ refusedJoints = /* @__PURE__ */ new Set();
733
+ /** A rope's or spring's numbers as last made, so a write REMAKES the joint. */
734
+ tetherState = /* @__PURE__ */ new Map();
735
+ tetherKey(joint) {
736
+ return `${joint.length}/${joint.stiffness}/${joint.damping}`;
737
+ }
738
+ /**
739
+ * Who is jointed to whom, and whether that pair's contacts are on.
740
+ *
741
+ * A joint's own bodies are not the world to the character controller: a
742
+ * body jointed to a character with its contacts off is not an obstacle to
743
+ * its shape-cast (the joint said so, for the contact solver; the cast asks
744
+ * its own question), and a body jointed to it is never its GROUND — a
745
+ * chain hanging from a crane hook was the hook's "moving platform", and the
746
+ * carry dragged the hook along by however far the hook had just moved.
747
+ */
748
+ jointed = /* @__PURE__ */ new Map();
749
+ linkJointed(a, b, contacts) {
750
+ for (const [x, y] of [[a, b], [b, a]]) {
751
+ let m = this.jointed.get(x);
752
+ if (!m) {
753
+ m = /* @__PURE__ */ new Map();
754
+ this.jointed.set(x, m);
755
+ }
756
+ m.set(y, contacts);
757
+ }
758
+ }
759
+ unlinkJointed(a, b) {
760
+ this.jointed.get(a)?.delete(b);
761
+ this.jointed.get(b)?.delete(a);
762
+ }
522
763
  /** One fixed body per solid scatter field, carrying a collider per row. */
523
764
  scatterBodies = /* @__PURE__ */ new Map();
524
765
  bodySet = /* @__PURE__ */ new Set();
@@ -676,6 +917,7 @@ var Physics3D = class {
676
917
  }
677
918
  this.syncJoints(structureChanged);
678
919
  if (structureChanged) {
920
+ this.watersStale = true;
679
921
  for (const [node, e] of this.entries) if (!seen.has(node)) {
680
922
  this.byColliderHandle.delete(e.collider.handle);
681
923
  this.world.removeRigidBody(e.body);
@@ -770,6 +1012,94 @@ var Physics3D = class {
770
1012
  * meantime. syncBodies runs every step, so the body appears the moment the
771
1013
  * download lands.
772
1014
  */
1015
+ /**
1016
+ * Lift every buoyant dynamic body by the water it is in — before the solver
1017
+ * step, so the impulses are part of this step's motion.
1018
+ *
1019
+ * `Water3D.heightAt` had answered "so gameplay can float a raft, bob a
1020
+ * buoy" since the water shipped, and nothing floated anything but the
1021
+ * swimmer: a RigidBody3D dropped into a lake fell through it to the bed,
1022
+ * whatever it weighed. This is Archimedes on eight sample points: each
1023
+ * octant of the collider is an eighth of its volume, lifted by the water's
1024
+ * density times however much of it is under the surface AT THAT POINT, at
1025
+ * that point — so a tilted hull's low corners push harder and it rights
1026
+ * itself, and a wave under one end rocks it. The water's `drag` then bleeds
1027
+ * velocity and spin in proportion to how much is under, which is what stops
1028
+ * a pushed crate and lets a bob die out.
1029
+ */
1030
+ applyBuoyancy(dt) {
1031
+ if (this.watersStale) {
1032
+ this.waters.length = 0;
1033
+ const root = this.engine.scene?.root;
1034
+ if (root) collectWaters(root, this.waters);
1035
+ this.watersStale = false;
1036
+ }
1037
+ if (this.waters.length === 0) return;
1038
+ const g = Math.abs(this.world.gravity.y) || 9.81;
1039
+ for (const [node, e] of this.entries) {
1040
+ if (!(node instanceof RigidBody3D) || !(node.buoyancy > 0)) continue;
1041
+ if (node.children.some((c) => typeNameOf(c) === "CharacterController3D")) continue;
1042
+ if (e.hull === void 0) e.hull = hullOf(e.collider, this.R.ShapeType.Capsule);
1043
+ const hull = e.hull;
1044
+ if (!hull) continue;
1045
+ const t = e.body.translation();
1046
+ const q = e.body.rotation();
1047
+ quatScratch.set(q.x, q.y, q.z, q.w);
1048
+ let submerged = 0;
1049
+ let water = null;
1050
+ for (const [ox, oy, oz] of OCTANTS) {
1051
+ buoyScratch.set(ox * hull.hx / 2, oy * hull.hy / 2, oz * hull.hz / 2).applyQuaternion(quatScratch);
1052
+ const px = t.x + buoyScratch.x;
1053
+ const py = t.y + buoyScratch.y;
1054
+ const pz = t.z + buoyScratch.z;
1055
+ const here = this.waterAt(px, pz);
1056
+ if (!here) continue;
1057
+ const f = Math.min(1, Math.max(0, (here.heightAt(px, pz) - (py - hull.hy / 4)) / (hull.hy / 2)));
1058
+ if (f <= 0) continue;
1059
+ water = here;
1060
+ submerged += f / 8;
1061
+ const lift = WATER_DENSITY * g * (hull.volume / 8) * f * node.buoyancy * dt;
1062
+ e.body.applyImpulseAtPoint({
1063
+ x: 0,
1064
+ y: lift,
1065
+ z: 0
1066
+ }, {
1067
+ x: px,
1068
+ y: py,
1069
+ z: pz
1070
+ }, true);
1071
+ }
1072
+ if (submerged <= 0 || !water) continue;
1073
+ const m = e.body.mass();
1074
+ const lv = e.body.linvel();
1075
+ const waterplane = 4 * hull.hx * hull.hz;
1076
+ const heaveK = WATER_DENSITY * g * waterplane * node.buoyancy;
1077
+ const heave = Math.min(1, 2 * HEAVE_ZETA * Math.sqrt(heaveK * m) / m * dt);
1078
+ const k = Math.min(1, water.drag * Math.min(1, 4 * submerged) * dt);
1079
+ e.body.applyImpulse({
1080
+ x: -lv.x * m * k,
1081
+ y: -lv.y * m * heave,
1082
+ z: -lv.z * m * k
1083
+ }, true);
1084
+ if (k <= 0) continue;
1085
+ const inertia = m * (hull.hx * hull.hx + hull.hy * hull.hy + hull.hz * hull.hz) / 3;
1086
+ const spin = Math.min(1, k * SPIN_DRAG);
1087
+ const av = e.body.angvel();
1088
+ e.body.applyTorqueImpulse({
1089
+ x: -av.x * inertia * spin,
1090
+ y: -av.y * inertia * spin,
1091
+ z: -av.z * inertia * spin
1092
+ }, true);
1093
+ }
1094
+ }
1095
+ /** The water whose footprint covers world (x, z), or null. */
1096
+ waterAt(x, z) {
1097
+ for (const w of this.waters) {
1098
+ if (w.tree === null) continue;
1099
+ if (w._waterYAt(x, z) !== null) return w;
1100
+ }
1101
+ return null;
1102
+ }
773
1103
  ensureEntry(node) {
774
1104
  let e = this.entries.get(node);
775
1105
  const R = this.R;
@@ -780,9 +1110,11 @@ var Physics3D = class {
780
1110
  if (node instanceof RigidBody3D) desc = R.RigidBodyDesc.dynamic();
781
1111
  else if (node instanceof CharacterBody3D || node instanceof Area3D) desc = R.RigidBodyDesc.kinematicPositionBased();
782
1112
  else desc = R.RigidBodyDesc.fixed();
783
- const [wx, wy, wz] = worldPosition3D(node);
1113
+ worldPose3D(node, wpV, quatScratch);
1114
+ const wx = wpV.x;
1115
+ const wy = wpV.y;
1116
+ const wz = wpV.z;
784
1117
  desc.setTranslation(wx, wy, wz);
785
- quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
786
1118
  desc.setRotation({
787
1119
  x: quatScratch.x,
788
1120
  y: quatScratch.y,
@@ -821,11 +1153,22 @@ var Physics3D = class {
821
1153
  node.rotation[1] ?? 0,
822
1154
  node.rotation[2] ?? 0
823
1155
  ],
1156
+ lastQuat: [
1157
+ quatScratch.x,
1158
+ quatScratch.y,
1159
+ quatScratch.z,
1160
+ quatScratch.w
1161
+ ],
824
1162
  lastV: [
825
1163
  0,
826
1164
  0,
827
1165
  0
828
1166
  ],
1167
+ lastAV: [
1168
+ node instanceof RigidBody3D && !node.fixedRotation ? node.angularVelocity[0] ?? 0 : 0,
1169
+ node instanceof RigidBody3D && !node.fixedRotation ? node.angularVelocity[1] ?? 0 : 0,
1170
+ node instanceof RigidBody3D && !node.fixedRotation ? node.angularVelocity[2] ?? 0 : 0
1171
+ ],
829
1172
  lastMass: node instanceof RigidBody3D ? node.mass : 1,
830
1173
  lastFriction: node.friction ?? .5,
831
1174
  lastRestitution: node.restitution ?? 0,
@@ -853,7 +1196,11 @@ var Physics3D = class {
853
1196
  node._enabledChanged = false;
854
1197
  if (node.enabled) this.armOverlaps(node, e.collider);
855
1198
  }
856
- const p = worldPosition3D(node);
1199
+ worldPose3D(node, wpV, quatScratch);
1200
+ const p = posScratch;
1201
+ p[0] = wpV.x;
1202
+ p[1] = wpV.y;
1203
+ p[2] = wpV.z;
857
1204
  if (p[0] !== e.last[0] || p[1] !== e.last[1] || p[2] !== e.last[2]) {
858
1205
  e.body.setTranslation({
859
1206
  x: p[0],
@@ -868,14 +1215,17 @@ var Physics3D = class {
868
1215
  const rx = node.rotation[0] ?? 0;
869
1216
  const ry = node.rotation[1] ?? 0;
870
1217
  const rz = node.rotation[2] ?? 0;
871
- if (rx !== e.lastRot[0] || ry !== e.lastRot[1] || rz !== e.lastRot[2]) {
872
- quatScratch.setFromEuler(eulerScratch.set(rx * DEG, ry * DEG, rz * DEG, "XYZ"));
1218
+ if (quatScratch.x !== e.lastQuat[0] || quatScratch.y !== e.lastQuat[1] || quatScratch.z !== e.lastQuat[2] || quatScratch.w !== e.lastQuat[3]) {
873
1219
  e.body.setRotation({
874
1220
  x: quatScratch.x,
875
1221
  y: quatScratch.y,
876
1222
  z: quatScratch.z,
877
1223
  w: quatScratch.w
878
1224
  }, true);
1225
+ e.lastQuat[0] = quatScratch.x;
1226
+ e.lastQuat[1] = quatScratch.y;
1227
+ e.lastQuat[2] = quatScratch.z;
1228
+ e.lastQuat[3] = quatScratch.w;
879
1229
  e.lastRot[0] = rx;
880
1230
  e.lastRot[1] = ry;
881
1231
  e.lastRot[2] = rz;
@@ -894,6 +1244,19 @@ var Physics3D = class {
894
1244
  }, true);
895
1245
  e.lastV = v;
896
1246
  }
1247
+ const av = [
1248
+ node.angularVelocity[0] ?? 0,
1249
+ node.angularVelocity[1] ?? 0,
1250
+ node.angularVelocity[2] ?? 0
1251
+ ];
1252
+ if (av[0] !== e.lastAV[0] || av[1] !== e.lastAV[1] || av[2] !== e.lastAV[2]) {
1253
+ if (!node.fixedRotation) e.body.setAngvel({
1254
+ x: av[0],
1255
+ y: av[1],
1256
+ z: av[2]
1257
+ }, true);
1258
+ e.lastAV = av;
1259
+ }
897
1260
  if (node.gravityScale !== e.body.gravityScale()) e.body.setGravityScale(node.gravityScale, true);
898
1261
  if (node.mass !== e.lastMass) {
899
1262
  e.collider.setMass(node.mass);
@@ -923,16 +1286,55 @@ var Physics3D = class {
923
1286
  }
924
1287
  /** Apply a world-space impulse to a dynamic body (character controllers). */
925
1288
  applyImpulse(node, impulse) {
926
- this.entries.get(node)?.body.applyImpulse({
1289
+ const e = this.entries.get(node);
1290
+ if (!e) return;
1291
+ if (node instanceof RigidBody3D) {
1292
+ const v = node.linearVelocity;
1293
+ const vx = v[0] ?? 0;
1294
+ const vy = v[1] ?? 0;
1295
+ const vz = v[2] ?? 0;
1296
+ if (vx !== e.lastV[0] || vy !== e.lastV[1] || vz !== e.lastV[2]) e.body.setLinvel({
1297
+ x: vx,
1298
+ y: vy,
1299
+ z: vz
1300
+ }, true);
1301
+ }
1302
+ e.body.applyImpulse({
927
1303
  x: impulse[0],
928
1304
  y: impulse[1],
929
1305
  z: impulse[2]
930
1306
  }, true);
1307
+ if (node instanceof RigidBody3D) {
1308
+ const lv = e.body.linvel();
1309
+ node.linearVelocity[0] = lv.x;
1310
+ node.linearVelocity[1] = lv.y;
1311
+ node.linearVelocity[2] = lv.z;
1312
+ e.lastV = [
1313
+ lv.x,
1314
+ lv.y,
1315
+ lv.z
1316
+ ];
1317
+ }
931
1318
  }
932
1319
  /** Mass the solver actually uses (collider-derived unless overridden). */
933
1320
  massOf(node) {
934
1321
  return this.entries.get(node)?.body.mass() ?? 1;
935
1322
  }
1323
+ /** The solver's rotation of a body, as a quaternion `[x, y, z, w]`. */
1324
+ rotationOf(node) {
1325
+ const q = this.entries.get(node)?.body.rotation();
1326
+ return q ? [
1327
+ q.x,
1328
+ q.y,
1329
+ q.z,
1330
+ q.w
1331
+ ] : [
1332
+ 0,
1333
+ 0,
1334
+ 0,
1335
+ 1
1336
+ ];
1337
+ }
936
1338
  /** Current solver velocity (fresher than the node prop mid-step). */
937
1339
  velocityOf(node) {
938
1340
  const v = this.entries.get(node)?.body.linvel();
@@ -953,14 +1355,27 @@ var Physics3D = class {
953
1355
  for (const [joint, impulse] of this.joints) if (!this.jointSet.has(joint)) {
954
1356
  this.world.removeImpulseJoint(impulse, true);
955
1357
  this.joints.delete(joint);
1358
+ const pair = this.jointBodies.get(joint);
1359
+ if (pair) this.unlinkJointed(pair[0], pair[1]);
1360
+ this.jointBodies.delete(joint);
1361
+ this.hingeState.delete(joint);
1362
+ this.tetherState.delete(joint);
956
1363
  }
957
1364
  }
958
1365
  for (const joint of this.jointSet) {
959
- if (this.joints.has(joint)) continue;
1366
+ if (this.joints.has(joint) || this.refusedJoints.has(joint)) continue;
960
1367
  const { a, b } = joint._resolveBodies();
961
1368
  const ea = this.entries.get(a);
962
1369
  const eb = this.entries.get(b);
963
1370
  if (!ea || !eb) continue;
1371
+ if (joint.type === "revolute" || joint.type === "prismatic") {
1372
+ const off = axisDisagreementDeg(ea.body.rotation(), eb.body.rotation(), joint.axis);
1373
+ if (off > .5) {
1374
+ this.refusedJoints.add(joint);
1375
+ diagnose(this.engine, "error", `[incanto] Joint3D '${joint.getPath()}': its two bodies disagree about the ${joint.type} axis by ${off.toFixed(1)}° — '${a.name}' and '${b.name}' are authored with rotations that differ by more than a turn about ${JSON.stringify(joint.axis)}, and Rapier reads the axis in BOTH bodies' local frames. Give both bodies the same rotation (turn the MESH inside the body instead — an 'auto' collider follows it), or rotate one only about the axis. The joint was not made.`);
1376
+ continue;
1377
+ }
1378
+ }
964
1379
  const a1 = {
965
1380
  x: joint.anchor[0] ?? 0,
966
1381
  y: joint.anchor[1] ?? 0,
@@ -987,10 +1402,93 @@ var Physics3D = class {
987
1402
  if (joint.type === "fixed") data = R.JointData.fixed(a1, identity, a2, identity);
988
1403
  else if (joint.type === "rope") data = R.JointData.rope(length, a1, a2);
989
1404
  else if (joint.type === "spring") data = R.JointData.spring(length, joint.stiffness, joint.damping, a1, a2);
990
- else data = R.JointData.spherical(a1, a2);
1405
+ else if (joint.type === "revolute" || joint.type === "prismatic") {
1406
+ const ax = joint.axis[0] ?? 0;
1407
+ const ay = joint.axis[1] ?? 0;
1408
+ const az = joint.axis[2] ?? 0;
1409
+ const n = Math.hypot(ax, ay, az) || 1;
1410
+ const axis = {
1411
+ x: ax / n,
1412
+ y: ay / n,
1413
+ z: az / n
1414
+ };
1415
+ data = joint.type === "revolute" ? R.JointData.revolute(a1, a2, axis) : R.JointData.prismatic(a1, a2, axis);
1416
+ } else data = R.JointData.spherical(a1, a2);
991
1417
  const made = this.world.createImpulseJoint(data, ea.body, eb.body, true);
992
- made.setContactsEnabled(jointContacts(joint.type, joint.collide));
1418
+ if (joint.type === "revolute" || joint.type === "prismatic") this.applyHinge(joint, made, true);
1419
+ const contacts = jointContacts(joint.type, joint.collide);
1420
+ made.setContactsEnabled(contacts);
993
1421
  this.joints.set(joint, made);
1422
+ this.jointBodies.set(joint, [a, b]);
1423
+ this.linkJointed(a, b, contacts);
1424
+ if (joint.type === "rope" || joint.type === "spring") this.tetherState.set(joint, this.tetherKey(joint));
1425
+ }
1426
+ for (const [joint, made] of this.joints) {
1427
+ if (joint.type !== "rope" && joint.type !== "spring") continue;
1428
+ const key = this.tetherKey(joint);
1429
+ if (this.tetherState.get(joint) === key) continue;
1430
+ this.world.removeImpulseJoint(made, true);
1431
+ this.joints.delete(joint);
1432
+ const pair = this.jointBodies.get(joint);
1433
+ if (pair) this.unlinkJointed(pair[0], pair[1]);
1434
+ this.jointBodies.delete(joint);
1435
+ this.tetherState.delete(joint);
1436
+ }
1437
+ for (const [joint, made] of this.joints) {
1438
+ if (joint.type !== "revolute" && joint.type !== "prismatic") continue;
1439
+ this.applyHinge(joint, made, false);
1440
+ const pair = this.jointBodies.get(joint);
1441
+ const ea = pair && this.entries.get(pair[0]);
1442
+ const eb = pair && this.entries.get(pair[1]);
1443
+ if (!ea || !eb) continue;
1444
+ if (joint.type === "revolute") joint.angle = hingeAngle3D(ea.body.rotation(), eb.body.rotation(), joint.axis);
1445
+ else {
1446
+ const qa = ea.body.rotation();
1447
+ const ta = ea.body.translation();
1448
+ const tb = eb.body.translation();
1449
+ const wa = rotateVec3(qa, joint.anchor[0] ?? 0, joint.anchor[1] ?? 0, joint.anchor[2] ?? 0);
1450
+ const wb = rotateVec3(eb.body.rotation(), joint.targetAnchor[0] ?? 0, joint.targetAnchor[1] ?? 0, joint.targetAnchor[2] ?? 0);
1451
+ const ax = rotateVec3(qa, joint.axis[0] ?? 0, joint.axis[1] ?? 0, joint.axis[2] ?? 0);
1452
+ const n = Math.hypot(ax[0], ax[1], ax[2]) || 1;
1453
+ const dx = ta.x + wa[0] - (tb.x + wb[0]);
1454
+ const dy = ta.y + wa[1] - (tb.y + wb[1]);
1455
+ const dz = ta.z + wa[2] - (tb.z + wb[2]);
1456
+ joint.travel = (dx * ax[0] + dy * ax[1] + dz * ax[2]) / n;
1457
+ }
1458
+ }
1459
+ }
1460
+ /**
1461
+ * Push a hinge's `limits` and motor to the solver — on creation, and again
1462
+ * whenever either changes. Limits and `angle` are this body relative to the
1463
+ * target; Rapier measures the other way round, hence the flip.
1464
+ */
1465
+ applyHinge(joint, made, first) {
1466
+ const limits = JSON.stringify(joint.limits);
1467
+ const motor = `${joint.motorSpeed}/${joint.motorStrength}`;
1468
+ const last = this.hingeState.get(joint);
1469
+ const unit = made;
1470
+ const unitScale = joint.type === "revolute" ? DEG : 1;
1471
+ if (first || last?.limits !== limits) {
1472
+ if (joint.limits.length === 2) {
1473
+ const lo = (joint.limits[0] ?? 0) * unitScale;
1474
+ const hi = (joint.limits[1] ?? 0) * unitScale;
1475
+ unit.setLimits(-Math.max(lo, hi), -Math.min(lo, hi));
1476
+ } else if (!first) unit.setLimits(-1e9, 1e9);
1477
+ }
1478
+ if (first || last?.motor !== motor) {
1479
+ if (joint.motorSpeed !== 0) unit.configureMotorVelocity(-joint.motorSpeed * unitScale, joint.motorStrength);
1480
+ else if (!first) unit.configureMotorVelocity(0, 0);
1481
+ }
1482
+ this.hingeState.set(joint, {
1483
+ limits,
1484
+ motor
1485
+ });
1486
+ if (joint.motorSpeed !== 0) {
1487
+ const pair = this.jointBodies.get(joint);
1488
+ for (const b of pair ?? []) {
1489
+ const e = this.entries.get(b);
1490
+ if (e && !e.body.isFixed() && e.body.isSleeping()) e.body.wakeUp();
1491
+ }
994
1492
  }
995
1493
  }
996
1494
  /** Who is currently inside each sensor/body, maintained from the event drain. */
@@ -1168,6 +1666,111 @@ var Physics3D = class {
1168
1666
  };
1169
1667
  const quatScratch = new Quaternion();
1170
1668
  const eulerScratch = new Euler();
1669
+ const buoyScratch = new Vector3();
1670
+ /** Fresh water, kg/m³ — the reference `RigidBody3D.buoyancy: 1` scales. */
1671
+ const WATER_DENSITY = 1e3;
1672
+ /** Damping ratio of a floating body's bob: under 1 so it still rocks once. */
1673
+ const HEAVE_ZETA = .7;
1674
+ /** Spin loses `drag` × this per second — rotation in water is the slow thing. */
1675
+ const SPIN_DRAG = 3;
1676
+ /** The eight octant centres of a collider, in units of its half extents. */
1677
+ const OCTANTS = [
1678
+ [
1679
+ -1,
1680
+ -1,
1681
+ -1
1682
+ ],
1683
+ [
1684
+ 1,
1685
+ -1,
1686
+ -1
1687
+ ],
1688
+ [
1689
+ -1,
1690
+ -1,
1691
+ 1
1692
+ ],
1693
+ [
1694
+ 1,
1695
+ -1,
1696
+ 1
1697
+ ],
1698
+ [
1699
+ -1,
1700
+ 1,
1701
+ -1
1702
+ ],
1703
+ [
1704
+ 1,
1705
+ 1,
1706
+ -1
1707
+ ],
1708
+ [
1709
+ -1,
1710
+ 1,
1711
+ 1
1712
+ ],
1713
+ [
1714
+ 1,
1715
+ 1,
1716
+ 1
1717
+ ]
1718
+ ];
1719
+ const typeNameOf = (node) => node.constructor.typeName;
1720
+ function collectWaters(node, out) {
1721
+ if (node instanceof Water3D) out.push(node);
1722
+ for (const child of node.children) collectWaters(child, out);
1723
+ }
1724
+ /**
1725
+ * Half extents and volume of a collider, from the shape Rapier built — so an
1726
+ * `auto` box, a fitted hull and an authored sphere all answer. A hull or mesh
1727
+ * is taken as the box around its vertices: a rock floats like its crate,
1728
+ * which is the right answer for a rock and a wrong one nobody will ship.
1729
+ */
1730
+ function hullOf(collider, capsuleType) {
1731
+ const he = collider.halfExtents();
1732
+ if (he) return {
1733
+ hx: he.x,
1734
+ hy: he.y,
1735
+ hz: he.z,
1736
+ volume: 8 * he.x * he.y * he.z
1737
+ };
1738
+ const r = collider.radius();
1739
+ const hh = collider.halfHeight();
1740
+ if (r !== null && hh !== null && hh > 0) {
1741
+ const capsule = collider.shapeType() === capsuleType;
1742
+ const caps = capsule ? 4 / 3 * Math.PI * r ** 3 : 0;
1743
+ return {
1744
+ hx: r,
1745
+ hy: hh + (capsule ? r : 0),
1746
+ hz: r,
1747
+ volume: Math.PI * r * r * 2 * hh + caps
1748
+ };
1749
+ }
1750
+ if (r !== null) return {
1751
+ hx: r,
1752
+ hy: r,
1753
+ hz: r,
1754
+ volume: 4 / 3 * Math.PI * r ** 3
1755
+ };
1756
+ const v = collider.vertices();
1757
+ if (!v || v.length < 3) return null;
1758
+ let maxX = 0;
1759
+ let maxY = 0;
1760
+ let maxZ = 0;
1761
+ for (let i = 0; i + 2 < v.length; i += 3) {
1762
+ maxX = Math.max(maxX, Math.abs(v[i] ?? 0));
1763
+ maxY = Math.max(maxY, Math.abs(v[i + 1] ?? 0));
1764
+ maxZ = Math.max(maxZ, Math.abs(v[i + 2] ?? 0));
1765
+ }
1766
+ if (maxX === 0 || maxY === 0 || maxZ === 0) return null;
1767
+ return {
1768
+ hx: maxX,
1769
+ hy: maxY,
1770
+ hz: maxZ,
1771
+ volume: 8 * maxX * maxY * maxZ
1772
+ };
1773
+ }
1171
1774
  function collectJoints(node, out) {
1172
1775
  if (node instanceof Joint3D) out.add(node);
1173
1776
  for (const c of node.children) collectJoints(c, out);
@@ -1379,39 +1982,58 @@ function characterHalfHeight(node) {
1379
1982
  if (collider.shape === "sphere" && typeof collider.radius === "number") return collider.radius;
1380
1983
  return 1;
1381
1984
  }
1382
- function worldPosition3D(node) {
1383
- let x = 0;
1384
- let y = 0;
1385
- let z = 0;
1386
- for (let n = node; n; n = n.parent) if (n instanceof Node3D) {
1387
- x += n.position[0] ?? 0;
1388
- y += n.position[1] ?? 0;
1389
- z += n.position[2] ?? 0;
1985
+ const poseChain = [];
1986
+ const poseV = new Vector3();
1987
+ const poseQ = new Quaternion();
1988
+ const poseStepQ = new Quaternion();
1989
+ const poseEuler = new Euler();
1990
+ /**
1991
+ * The WORLD pose of `node` — position and orientation composed through every
1992
+ * Node3D ancestor, the way the renderer composes them. This used to sum
1993
+ * positions only: a child two metres down +z of a hull turned 90° was put at
1994
+ * world +z while the renderer drew it at world +x, so a guard's grab box "in
1995
+ * front" of a turned guard, a turret's muzzle and a tank's barrel-tip sensor
1996
+ * all sat where nothing was drawn. `outQuat` may be omitted when only the
1997
+ * position is wanted. Scale is not composed (it never was).
1998
+ */
1999
+ function worldPose3D(node, outPos, outQuat) {
2000
+ poseChain.length = 0;
2001
+ for (let n = node; n; n = n.parent) if (n instanceof Node3D) poseChain.push(n);
2002
+ outPos.set(0, 0, 0);
2003
+ const q = outQuat ?? poseQ;
2004
+ q.identity();
2005
+ for (let i = poseChain.length - 1; i >= 0; i--) {
2006
+ const n = poseChain[i];
2007
+ poseV.set(n.position[0] ?? 0, n.position[1] ?? 0, n.position[2] ?? 0).applyQuaternion(q);
2008
+ outPos.add(poseV);
2009
+ const r = n.rotation;
2010
+ if ((r[0] ?? 0) !== 0 || (r[1] ?? 0) !== 0 || (r[2] ?? 0) !== 0) {
2011
+ poseStepQ.setFromEuler(poseEuler.set((r[0] ?? 0) * DEG, (r[1] ?? 0) * DEG, (r[2] ?? 0) * DEG, "XYZ"));
2012
+ q.multiply(poseStepQ);
2013
+ }
1390
2014
  }
1391
- posScratch[0] = x;
1392
- posScratch[1] = y;
1393
- posScratch[2] = z;
2015
+ }
2016
+ const wpV = new Vector3();
2017
+ function worldPosition3D(node) {
2018
+ worldPose3D(node, wpV, null);
2019
+ posScratch[0] = wpV.x;
2020
+ posScratch[1] = wpV.y;
2021
+ posScratch[2] = wpV.z;
1394
2022
  return posScratch;
1395
2023
  }
1396
- const offsetScratch = [
1397
- 0,
1398
- 0,
1399
- 0
1400
- ];
1401
- /** Writes into the shared scratchconsume before the next call. */
1402
- function parentWorldOffset3D(node) {
1403
- let x = 0;
1404
- let y = 0;
1405
- let z = 0;
2024
+ const parentPosV = new Vector3();
2025
+ const parentQuatQ = new Quaternion();
2026
+ const localV = new Vector3();
2027
+ const localQ = new Quaternion();
2028
+ const invQ = new Quaternion();
2029
+ /** The pose of `node`'s PARENT frame in world space what a body's world pose is measured against. */
2030
+ function parentPose3D(node) {
2031
+ let parent = null;
1406
2032
  for (let n = node.parent; n; n = n.parent) if (n instanceof Node3D) {
1407
- x += n.position[0] ?? 0;
1408
- y += n.position[1] ?? 0;
1409
- z += n.position[2] ?? 0;
2033
+ parent = n;
2034
+ break;
1410
2035
  }
1411
- offsetScratch[0] = x;
1412
- offsetScratch[1] = y;
1413
- offsetScratch[2] = z;
1414
- return offsetScratch;
2036
+ worldPose3D(parent, parentPosV, parentQuatQ);
1415
2037
  }
1416
2038
  function parseCollider3D(R, node) {
1417
2039
  const collider = node.collider;