@adatechnology/game-characters-three 0.5.0 → 0.6.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.
package/dist/index.d.ts CHANGED
@@ -1103,6 +1103,11 @@ type PlayerAvatar = {
1103
1103
  readonly playing: string | null;
1104
1104
  /** A socket at the hand: 'R' is the weapon hand (the puppet's right, at -x), 'L' the other one. */
1105
1105
  hand(side: 'R' | 'L'): THREE_NS.Object3D;
1106
+ /**
1107
+ * Brings the hands to where the tool in them asks (`HandTargets`, world space) after `update`: shoulder, elbow and wrist each bend inside the human range, the chest turns and the shoulder
1108
+ * blade slides toward a hand out of reach. Null, a strike playing or the body going down leave the arms as `update` posed them. Call it after the tool is placed and the root moved.
1109
+ */
1110
+ reach(targets: HandTargets | null): void;
1106
1111
  on(type: 'event', listener: (event: AvatarEvent) => void): () => void;
1107
1112
  dispose(): void;
1108
1113
  };
@@ -1275,6 +1280,8 @@ type RoomAvatarLike = {
1275
1280
  contact?(origin: [number, number, number], direction: [number, number, number], maxDistance: number): number | null;
1276
1281
  }): void;
1277
1282
  };
1283
+ /** Brings the body's hands to the tool in them, in third person (`RoomAvatar.reach`). */
1284
+ reach(targets: HandTargets | null): void;
1278
1285
  /** Plays a strike on the body and the arms and answers the seconds to its impact (`RoomAvatar.strike`). */
1279
1286
  strike(clipId: string, options?: {
1280
1287
  leg?: 'left' | 'right';
@@ -8637,6 +8637,176 @@ var GameCharactersThree = (() => {
8637
8637
  shirtColor: "3d5a73"
8638
8638
  };
8639
8639
 
8640
+ // src/first-person/ik.ts
8641
+ var solveTwoBone = (root, target, upper, lower, pole, out) => {
8642
+ const line = out.line.copy(target).sub(root);
8643
+ let distance3 = line.length();
8644
+ if (distance3 < 1e-6) line.set(0, 0, -1);
8645
+ else line.divideScalar(distance3);
8646
+ const min = Math.abs(upper - lower) + 1e-4;
8647
+ const max = upper + lower - 1e-4;
8648
+ out.clamped = distance3 > max || distance3 < min;
8649
+ distance3 = Math.min(max, Math.max(min, distance3));
8650
+ const along = (distance3 * distance3 + upper * upper - lower * lower) / (2 * distance3);
8651
+ const across = Math.sqrt(Math.max(0, upper * upper - along * along));
8652
+ const side = out.side.copy(pole);
8653
+ side.addScaledVector(line, -side.dot(line));
8654
+ if (side.lengthSq() < 1e-8) side.set(0, 1, 0).addScaledVector(line, -line.y);
8655
+ if (side.lengthSq() < 1e-8) side.set(1, 0, 0);
8656
+ side.normalize();
8657
+ out.joint.copy(root).addScaledVector(line, along).addScaledVector(side, across);
8658
+ out.end.copy(root).addScaledVector(line, distance3);
8659
+ return out;
8660
+ };
8661
+ var createTwoBone = (THREE) => ({ joint: new THREE.Vector3(), end: new THREE.Vector3(), clamped: false, line: new THREE.Vector3(), side: new THREE.Vector3() });
8662
+
8663
+ // src/player/arm-reach.ts
8664
+ var ARM_REACH = {
8665
+ /** The elbow never straightens past this bend (a locked elbow reads as a stick). */
8666
+ elbowMin: 0.12,
8667
+ /** Full flexion of the elbow. */
8668
+ elbowMax: 2.5,
8669
+ /** The wrist bends (flexion, extension, deviation) up to this angle from the line of the forearm (flexion reaches about 80 degrees). */
8670
+ wristBend: 1.3,
8671
+ /** The forearm turns (pronation and supination) up to this angle about its own axis. */
8672
+ wristTwist: 1.6,
8673
+ /** The most the chest turns to bring the support shoulder toward a far hand. */
8674
+ chestTurn: 0.6,
8675
+ /** The most a shoulder blade slides toward a far hand (protraction and elevation together). */
8676
+ protraction: 0.11,
8677
+ /** A hand is within reach below this fraction of the arm's length. */
8678
+ slack: 0.96,
8679
+ /** Where the elbow points: down and out from the body, a little back (body space, x outward). */
8680
+ elbowPole: [0.55, -1, -0.25],
8681
+ /** From the wrist joint to the middle of the palm, metres of the body. */
8682
+ palm: 0.042
8683
+ };
8684
+ var createArmReach = (THREE, rig, hands) => {
8685
+ const arms = {
8686
+ R: { g: rig.arms[1].g, el: rig.arms[1].el, hand: hands.R, outward: -1 },
8687
+ L: { g: rig.arms[0].g, el: rig.arms[0].el, hand: hands.L, outward: 1 }
8688
+ };
8689
+ const upper = rig.upper;
8690
+ const handBasis = {
8691
+ R: new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(new THREE.Vector3(0, -1, 0), new THREE.Vector3(-1, 0, 0), new THREE.Vector3(0, 0, -1))).invert(),
8692
+ L: new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(new THREE.Vector3(0, -1, 0), new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 1))).invert()
8693
+ };
8694
+ const saved = {
8695
+ twist: 0,
8696
+ arms: { R: { position: new THREE.Vector3(), g: new THREE.Quaternion(), el: new THREE.Quaternion(), hand: new THREE.Quaternion() }, L: { position: new THREE.Vector3(), g: new THREE.Quaternion(), el: new THREE.Quaternion(), hand: new THREE.Quaternion() } }
8697
+ };
8698
+ let applied = false;
8699
+ const bone = createTwoBone(THREE);
8700
+ const v = { shoulder: new THREE.Vector3(), elbow: new THREE.Vector3(), wrist: new THREE.Vector3(), palm: new THREE.Vector3(), target: new THREE.Vector3(), pole: new THREE.Vector3(), a: new THREE.Vector3(), b: new THREE.Vector3(), c: new THREE.Vector3() };
8701
+ const q = { hand: new THREE.Quaternion(), parent: new THREE.Quaternion(), body: new THREE.Quaternion(), local: new THREE.Quaternion(), swing: new THREE.Quaternion(), twist: new THREE.Quaternion(), rest: new THREE.Quaternion() };
8702
+ const basis = new THREE.Matrix4();
8703
+ const yAxis = new THREE.Vector3(0, 1, 0);
8704
+ const lengthsOf = (arm) => {
8705
+ const scale = arm.g.getWorldScale(v.a).y;
8706
+ return { upper: arm.el.position.length() * scale, fore: arm.hand.position.length() * scale, palm: ARM_REACH.palm * scale };
8707
+ };
8708
+ const wristOf = (side, pose, palm) => {
8709
+ q.hand.set(...pose.quaternion).multiply(handBasis[side]);
8710
+ v.palm.set(...pose.position);
8711
+ v.wrist.copy(v.palm).addScaledVector(v.a.set(0, -1, 0).applyQuaternion(q.hand), -palm);
8712
+ };
8713
+ const turnChest = (pose) => {
8714
+ const { upper: up, fore, palm } = lengthsOf(arms.L);
8715
+ wristOf("L", pose, palm);
8716
+ const excess = arms.L.g.getWorldPosition(v.shoulder).distanceTo(v.wrist) - (up + fore) * ARM_REACH.slack;
8717
+ if (excess <= 0) return;
8718
+ const chestRadius = Math.hypot(arms.L.g.position.x, arms.L.g.position.z) * arms.L.g.getWorldScale(v.a).y;
8719
+ upper.rotation.y -= Math.min(ARM_REACH.chestTurn, excess / Math.max(0.05, chestRadius));
8720
+ upper.updateMatrixWorld(true);
8721
+ };
8722
+ const protract = (arm, length) => {
8723
+ const excess = arm.g.getWorldPosition(v.shoulder).distanceTo(v.wrist) - length * ARM_REACH.slack;
8724
+ if (excess <= 0) return;
8725
+ const parent = arm.g.parent;
8726
+ v.b.copy(v.wrist).sub(v.shoulder).normalize().multiplyScalar(Math.min(excess, ARM_REACH.protraction)).add(v.shoulder);
8727
+ arm.g.position.copy(parent.worldToLocal(v.b));
8728
+ arm.g.updateMatrixWorld(true);
8729
+ };
8730
+ const limitWrist = (local) => {
8731
+ const projection = v.c.set(local.x, local.y, local.z).projectOnVector(yAxis);
8732
+ q.twist.set(projection.x, projection.y, projection.z, local.w).normalize();
8733
+ const twistAngle = 2 * Math.atan2(q.twist.y, q.twist.w);
8734
+ const swing = q.swing.copy(local).multiply(q.twist.invert());
8735
+ const swingAngle = 2 * Math.acos(Math.min(1, Math.abs(swing.w)));
8736
+ if (swingAngle > ARM_REACH.wristBend) swing.slerp(q.rest.identity(), 1 - ARM_REACH.wristBend / swingAngle);
8737
+ const clampedTwist = Math.max(-ARM_REACH.wristTwist, Math.min(ARM_REACH.wristTwist, Math.atan2(Math.sin(twistAngle), Math.cos(twistAngle))));
8738
+ return local.copy(swing).multiply(q.twist.setFromAxisAngle(yAxis, clampedTwist));
8739
+ };
8740
+ const reach = (side, pose) => {
8741
+ const arm = arms[side];
8742
+ const { upper: up, fore, palm } = lengthsOf(arm);
8743
+ wristOf(side, pose, palm);
8744
+ protract(arm, up + fore);
8745
+ arm.g.getWorldPosition(v.shoulder);
8746
+ upper.getWorldQuaternion(q.body);
8747
+ v.pole.set(ARM_REACH.elbowPole[0] * arm.outward, ARM_REACH.elbowPole[1], ARM_REACH.elbowPole[2]).applyQuaternion(q.body);
8748
+ const most = Math.sqrt(up * up + fore * fore + 2 * up * fore * Math.cos(ARM_REACH.elbowMin));
8749
+ const least = Math.sqrt(up * up + fore * fore + 2 * up * fore * Math.cos(ARM_REACH.elbowMax));
8750
+ const line = v.c.copy(v.wrist).sub(v.shoulder);
8751
+ const distance3 = Math.min(most, Math.max(least, line.length()));
8752
+ v.target.copy(v.shoulder).addScaledVector(line.normalize(), distance3);
8753
+ solveTwoBone(v.shoulder, v.target, up, fore, v.pole, bone);
8754
+ const along = v.a.copy(bone.joint).sub(v.shoulder).normalize();
8755
+ const forearm = v.b.copy(bone.end).sub(bone.joint).normalize();
8756
+ const fold = v.c.copy(forearm).addScaledVector(along, -forearm.dot(along));
8757
+ if (fold.lengthSq() < 1e-8) fold.copy(v.pole).addScaledVector(along, -v.pole.dot(along)).negate();
8758
+ fold.normalize();
8759
+ const yWorld = v.elbow.copy(along).negate();
8760
+ basis.makeBasis(v.pole.crossVectors(yWorld, fold), yWorld, fold);
8761
+ arm.g.parent.getWorldQuaternion(q.parent);
8762
+ arm.g.quaternion.setFromRotationMatrix(basis).premultiply(q.parent.invert());
8763
+ arm.el.rotation.set(-Math.acos(Math.max(-1, Math.min(1, along.dot(forearm)))), 0, 0);
8764
+ arm.g.updateMatrixWorld(true);
8765
+ const reached = arm.hand.getWorldPosition(v.a).sub(v.shoulder).normalize();
8766
+ q.swing.setFromUnitVectors(reached, v.b.copy(v.target).sub(v.shoulder).normalize());
8767
+ arm.g.parent.getWorldQuaternion(q.parent);
8768
+ arm.g.quaternion.premultiply(q.parent).premultiply(q.swing).premultiply(q.parent.invert());
8769
+ arm.g.updateMatrixWorld(true);
8770
+ arm.el.getWorldQuaternion(q.parent);
8771
+ arm.hand.quaternion.copy(limitWrist(q.local.copy(q.parent.invert()).multiply(q.hand)));
8772
+ };
8773
+ const save = () => {
8774
+ saved.twist = upper.rotation.y;
8775
+ for (const side of ["R", "L"]) {
8776
+ const arm = arms[side];
8777
+ const into = saved.arms[side];
8778
+ into.position.copy(arm.g.position);
8779
+ into.g.copy(arm.g.quaternion);
8780
+ into.el.copy(arm.el.quaternion);
8781
+ into.hand.copy(arm.hand.quaternion);
8782
+ }
8783
+ };
8784
+ return {
8785
+ apply(targets) {
8786
+ if (!targets || !targets.R && !targets.L) return;
8787
+ save();
8788
+ applied = true;
8789
+ upper.updateWorldMatrix(true, true);
8790
+ if (targets.L && targets.R) turnChest(targets.L);
8791
+ if (targets.R) reach("R", targets.R);
8792
+ if (targets.L) reach("L", targets.L);
8793
+ },
8794
+ restore() {
8795
+ if (!applied) return;
8796
+ applied = false;
8797
+ upper.rotation.y = saved.twist;
8798
+ for (const side of ["R", "L"]) {
8799
+ const arm = arms[side];
8800
+ const from = saved.arms[side];
8801
+ arm.g.position.copy(from.position);
8802
+ arm.g.quaternion.copy(from.g);
8803
+ arm.el.quaternion.copy(from.el);
8804
+ arm.hand.quaternion.copy(from.hand);
8805
+ }
8806
+ }
8807
+ };
8808
+ };
8809
+
8640
8810
  // src/player/avatar.ts
8641
8811
  var TWO_PI2 = Math.PI * 2;
8642
8812
  var MAX_GAIT = 1.4;
@@ -8733,6 +8903,7 @@ var GameCharactersThree = (() => {
8733
8903
  const built = buildHand({ THREE, add: addMesh, hg, handM: handMaterial, s: side, detailed: true });
8734
8904
  hands.push({ side, hg, fingers: built.fingers, th: built.th });
8735
8905
  });
8906
+ const armReach = createArmReach(THREE, rig, { R: hands.find((hand) => hand.side < 0).hg, L: hands.find((hand) => hand.side > 0).hg });
8736
8907
  const setHandGrip = (hand, grip, wristX = 0) => {
8737
8908
  hand.fingers.forEach((finger, j) => {
8738
8909
  finger.rotation.z = -hand.side * grip * (j % 2 ? 1.1 : 0.9);
@@ -8963,6 +9134,7 @@ var GameCharactersThree = (() => {
8963
9134
  return run ? run.clip.id : null;
8964
9135
  },
8965
9136
  update(dt, state = {}) {
9137
+ armReach.restore();
8966
9138
  const speed = state.speed ?? 0;
8967
9139
  const moving = speed >= 0.05;
8968
9140
  const down = !!state.down;
@@ -9016,6 +9188,11 @@ var GameCharactersThree = (() => {
9016
9188
  if (run) run.target = 0;
9017
9189
  },
9018
9190
  hand: (side) => sockets[side],
9191
+ reach(targets) {
9192
+ if (run || fall > 0) return;
9193
+ root.updateMatrixWorld(true);
9194
+ armReach.apply(targets);
9195
+ },
9019
9196
  on(_type, listener) {
9020
9197
  listeners.add(listener);
9021
9198
  return () => void listeners.delete(listener);
@@ -9571,29 +9748,6 @@ var GameCharactersThree = (() => {
9571
9748
  };
9572
9749
  };
9573
9750
 
9574
- // src/first-person/ik.ts
9575
- var solveTwoBone = (root, target, upper, lower, pole, out) => {
9576
- const line = out.line.copy(target).sub(root);
9577
- let distance3 = line.length();
9578
- if (distance3 < 1e-6) line.set(0, 0, -1);
9579
- else line.divideScalar(distance3);
9580
- const min = Math.abs(upper - lower) + 1e-4;
9581
- const max = upper + lower - 1e-4;
9582
- out.clamped = distance3 > max || distance3 < min;
9583
- distance3 = Math.min(max, Math.max(min, distance3));
9584
- const along = (distance3 * distance3 + upper * upper - lower * lower) / (2 * distance3);
9585
- const across = Math.sqrt(Math.max(0, upper * upper - along * along));
9586
- const side = out.side.copy(pole);
9587
- side.addScaledVector(line, -side.dot(line));
9588
- if (side.lengthSq() < 1e-8) side.set(0, 1, 0).addScaledVector(line, -line.y);
9589
- if (side.lengthSq() < 1e-8) side.set(1, 0, 0);
9590
- side.normalize();
9591
- out.joint.copy(root).addScaledVector(line, along).addScaledVector(side, across);
9592
- out.end.copy(root).addScaledVector(line, distance3);
9593
- return out;
9594
- };
9595
- var createTwoBone = (THREE) => ({ joint: new THREE.Vector3(), end: new THREE.Vector3(), clamped: false, line: new THREE.Vector3(), side: new THREE.Vector3() });
9596
-
9597
9751
  // src/first-person/arms.ts
9598
9752
  var DEFAULT_BLEND = 0.18;
9599
9753
  var ELBOW_SEGMENTS = 8;
@@ -10059,6 +10213,7 @@ var GameCharactersThree = (() => {
10059
10213
  });
10060
10214
  },
10061
10215
  hand: (side) => avatar.hand(side),
10216
+ reach: (targets) => avatar.reach(targets),
10062
10217
  strike: (clipId, strikeOptions) => {
10063
10218
  const clip = strikes.get(clipId);
10064
10219
  if (!clip) return null;