@adatechnology/game-characters-three 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -29,7 +29,7 @@ time.value += dt
29
29
  - `createFluidsBridge({ sink, film })` and `bloodLookOfAge(age)`: a `body` event throws drops of blood into the film, in the fresh and dry colours of the infection age (`ages.json`), `old_blood` for the thin ones.
30
30
 
31
31
  **Player avatar.** `createPlayerAvatar(THREE, options?)` is the team game's player body: the rich human of the Bestiário/Elenco built from `presets/player-avatar.json`
32
- (`{ type: 'Lento', human: true, seed: 4242, skin, hs: 'short', hair }`, shirt `0x3d5a73`), overridable with `{ spec, shirtColor, clips, blend, fallSeconds }`.
32
+ (`{ type: 'Lento', human: true, seed: 4242, skin, hs: 'short', hair }`, shirt `0x3d5a73`), overridable with `{ spec, shirtColor, clips, blend, fallSeconds, cascade, clipCascade }` (`clipCascade`, default 5: how many times faster the joints follow while a clip that does not loop plays, so a strike reaches its pose by its impact).
33
33
 
34
34
  ```ts
35
35
  const player = createPlayerAvatar(THREE)
@@ -41,16 +41,16 @@ player.play('grabbed_bitten_human', { onEvent: (event) => event.type === 'impact
41
41
 
42
42
  Crouch, down and the aimed arms are derived literally from the game's GPU pose code (`BODY_CHUNK`, lines quoted in `src/player/avatar.ts`): crouch is thigh `-0.95`, knee `1.9`, trunk `+0.25`, hip `-0.35`;
43
43
  down turns the body about the hip (`±1.5` rad, face up or down by the phase) with the limbs of pose 2; `armed` follows `(pitch + 1) / 3`. `play` overlays a `PoseClip` of `@adatechnology/game-characters`
44
- (`channels: 'upper'` leaves the legs to the gait) with a 0.12 s blend, and fires its events (`impact`, `phase`) once through `onEvent` and `on('event')`. `hand('R')` is the weapon hand (the puppet's right, at -x).
44
+ (`channels: 'upper'` leaves the legs to the gait) with a 0.12 s blend, and fires its events (`impact`, `phase`) once through `onEvent` and `on('event')`. `hand('R')` is the weapon hand (the puppet's right, at -x). A pose clip's `armL`/`legL` channels move that side (`arms[1]`/`legs[0]`), so `punch_right` and `front_kick` of `ATTACK_CLIPS` drive `armL` and `legL`, the same arm and leg the first-person view shows.
45
45
 
46
46
  **In the test room.** `room.setAvatar(toRoomAvatar(player))` shows the body at the player's feet (`toRoomAvatar` only flips the pitch: the room counts a look up as positive, the team game a look down) and V switches between the first and the third person of
47
47
  `@adatechnology/game-test-room`. `createAvatarTestModule({ avatar, camera, state })` is the module of the area `avatar_area` (`AVATAR_AREA_ID`): bench buttons for the view (1st/3rd person), crouch, stand, fall, get up, armed on/off,
48
48
  one button per registered clip (`Tocar: grabbed_bitten_human`, `grab_bite_zombie`, and with `clips: [...POSE_CLIPS, ...ATTACK_CLIPS]` the punch, shove, kick and the recoils; `clipLocales` gives the phase texts) and stop; metrics for the camera, the clip playing, the last clip event and the joints. Every action is a JSON line in the room log (`{"type":"avatar","action":"play_clip","clip":...}`), clip events included, and an `impact` event also gets its own `{"action":"impact","clip":...,"t":...}` line.
49
49
  The module takes the body, the view (`camera: { mode, set }`) and the state (`state: { set }`) by option and imports only `@adatechnology/game-contracts`.
50
50
 
51
- `toRoomAvatar` also gives the room the first-person arms (`firstPerson: false` for none) and `strike(clipId)`, which plays the attack clip on the body and the first-person clip of the same id on the arms and answers the seconds to the impact (the same in both views).
51
+ `toRoomAvatar` also gives the room the first-person arms (`firstPerson: false` for none) and `strike(clipId)`, which plays the attack clip on the body and the first-person clip of the same id on the arms and answers the seconds to the impact (the same in both views). `strike(clip, { leg: 'left' })` kicks with the left leg and `strike(clip, { hand: 'left' })` punches with the left hand: the mirror of the clip on the body (`mirrorPoseClip`, id `<clip>_left`) and on the arms.
52
52
 
53
- **First-person arms.** `createFirstPersonArms(THREE, { colors?, rig?, stances?, clips?, blend?, bob? })` builds sleeves (upper arm, forearm, elbow, cuff) and hands of skin (palm, four fingers of three segments, a thumb that close on a grip) drawn in the camera's space, plus the right leg that only shows during a kick. `arms.update(dt, { camera, targets, stance, speed, running, crouch, contact? })`: the `relaxed` and `fists` stances are rest poses (`FIRST_PERSON_STANCES`, from `presets/first-person-arms.json`), `hold` follows the `HandTargets` of `game-contracts` (world space) with a two-bone IK from fixed shoulders, the elbow bending down and out; a target out of reach is clamped (the shoulder moves `reachShift` toward it first). `arms.play('punch_right' | 'shove_two_hands' | 'front_kick')` overlays a `FirstPersonClip` (`FIRST_PERSON_CLIPS` of `game-characters-people`, same durations and impact seconds as the attack clips); `arms.on('event', fn)` gives its `impact`; `arms.handPose(side)` reads where a hand is drawn. `contact(origin, direction, maxDistance)` (optional; distance to the first surface or null) makes the hands and the kick foot stop a few millimetres before a surface along the camera forward (the elbow bends); `arms.play('front_kick', { leg: 'left' })` kicks with the left leg. Hand frame: x wrist to knuckles, y back of the hand, z little-finger side; a left hand is the right one mirrored.
53
+ **First-person arms.** `createFirstPersonArms(THREE, { colors?, rig?, stances?, clips?, blend?, bob? })` builds sleeves (upper arm, forearm, elbow, cuff) and hands of skin (palm, four fingers of three segments, a thumb that close on a grip) drawn in the camera's space, plus the right leg that only shows during a kick. `arms.update(dt, { camera, targets, stance, speed, running, crouch, contact? })`: the `relaxed` and `fists` stances are rest poses (`FIRST_PERSON_STANCES`, from `presets/first-person-arms.json`), `hold` follows the `HandTargets` of `game-contracts` (world space) with a two-bone IK from fixed shoulders, the elbow bending down and out; a target out of reach is clamped (the shoulder moves `reachShift` toward it first). `arms.play('punch_right' | 'shove_two_hands' | 'front_kick')` overlays a `FirstPersonClip` (`FIRST_PERSON_CLIPS` of `game-characters-people`, same durations and impact seconds as the attack clips); `arms.on('event', fn)` gives its `impact`; `arms.handPose(side)` reads where a hand is drawn. `contact(origin, direction, maxDistance)` (optional; distance to the first surface or null) makes the hands and the kick foot stop a few millimetres before a surface along the camera forward (the elbow bends); `arms.play('front_kick', { leg: 'left' })` kicks with the left leg and `arms.play('punch_right', { hand: 'left' })` punches with the left hand (the hands of the clip in a mirror). Hand frame: x wrist to knuckles, y back of the hand, z little-finger side; a left hand is the right one mirrored.
54
54
 
55
55
  ## Geometry
56
56
 
package/dist/index.d.ts CHANGED
@@ -1040,6 +1040,11 @@ type PlayerAvatarOptions = {
1040
1040
  readonly fallSeconds?: number;
1041
1041
  /** Smooth the joints with the Bestiário cascade (default true). */
1042
1042
  readonly cascade?: boolean;
1043
+ /**
1044
+ * How many times faster the cascade follows while a clip that does not loop plays: a strike is over in a third of a second, and at the walk's pace the joints would
1045
+ * still be on their way when it ends (a punch that never reaches the shoulder). 1 keeps the walk's pace. Default 5.
1046
+ */
1047
+ readonly clipCascade?: number;
1043
1048
  };
1044
1049
  type AvatarEvent = PoseClipEvent & {
1045
1050
  clip: string;
@@ -1206,9 +1211,10 @@ type FirstPersonArms = {
1206
1211
  /** Add it to the scene: it follows the camera by itself. */
1207
1212
  root: THREE_NS.Group;
1208
1213
  /** Starts a clip; false when no registered clip has that id. The previous clip is dropped. */
1209
- /** `leg: 'left'` plays the foot of the clip (the right foot) as the left one, in a mirror. */
1214
+ /** `leg: 'left'` plays the foot of the clip (the right foot) as the left one, in a mirror; `hand: 'left'` plays the hands in a mirror (the right hand's punch thrown by the left). */
1210
1215
  play(clip: string, options?: {
1211
1216
  leg?: 'left' | 'right';
1217
+ hand?: 'left' | 'right';
1212
1218
  }): boolean;
1213
1219
  stop(): void;
1214
1220
  /** The clip being played, or null. */
@@ -1263,6 +1269,7 @@ type RoomAvatarLike = {
1263
1269
  /** Plays a strike on the body and the arms and answers the seconds to its impact (`RoomAvatar.strike`). */
1264
1270
  strike(clipId: string, options?: {
1265
1271
  leg?: 'left' | 'right';
1272
+ hand?: 'left' | 'right';
1266
1273
  }): number | null;
1267
1274
  /** The avatar it wraps. */
1268
1275
  readonly avatar: PlayerAvatar;
@@ -1281,7 +1288,7 @@ type ToRoomAvatarOptions = {
1281
1288
  * Fits the player's body to the test room's `RoomAvatar` (`room.setAvatar(toRoomAvatar(createPlayerAvatar(THREE)))`). It changes the sign of the pitch: the room
1282
1289
  * counts a look up as positive, the team game's `view.pitch` (what `AvatarUpdate.pitch` reads) counts a look down as positive. It also gives the room the first-person arms, dressed in the
1283
1290
  * colours of the body (`firstPerson: false` for none), and `strike(clipId)`: the strike plays on the body (the attack clips) and on the arms (the first-person clips), and answers the seconds to its
1284
- * impact, which are the same in both views.
1291
+ * impact, which are the same in both views. A kick with the left leg (`leg: 'left'`) or a punch with the left hand (`hand: 'left'`) plays the mirror of the clip written for the right.
1285
1292
  */
1286
1293
  declare const toRoomAvatar: (avatar: PlayerAvatar, options?: ToRoomAvatarOptions) => RoomAvatarLike;
1287
1294
 
@@ -8640,6 +8640,7 @@ var GameCharactersThree = (() => {
8640
8640
  var TWO_PI2 = Math.PI * 2;
8641
8641
  var MAX_GAIT = 1.4;
8642
8642
  var CLIP_BLEND = 0.12;
8643
+ var CLIP_CASCADE = 5;
8643
8644
  var FALL_SECONDS2 = 0.3;
8644
8645
  var HIP_HEIGHT = 0.95;
8645
8646
  var WALK_ARM = 0.28;
@@ -8679,6 +8680,7 @@ var GameCharactersThree = (() => {
8679
8680
  const blendDefault = options.blend ?? CLIP_BLEND;
8680
8681
  const fallSeconds = options.fallSeconds ?? FALL_SECONDS2;
8681
8682
  const smooth = options.cascade ?? true;
8683
+ const clipCascade = options.clipCascade ?? CLIP_CASCADE;
8682
8684
  const root = new THREE.Group();
8683
8685
  const tilt = new THREE.Group();
8684
8686
  tilt.add(body);
@@ -8969,14 +8971,14 @@ var GameCharactersThree = (() => {
8969
8971
  }
8970
8972
  placeBody(fall);
8971
8973
  stepClip(dt, down);
8972
- if (smooth) animator.cascade(rig, dt);
8974
+ if (smooth) animator.cascade(rig, run && !run.clip.loop ? dt * clipCascade : dt);
8973
8975
  const clipId = run ? run.clip.id : null;
8974
8976
  const ph = cycles * TWO_PI2;
8975
8977
  hands.forEach((hand) => {
8976
8978
  const isRight = hand.side < 0;
8977
8979
  const holdRight = isRight && (!!state.holding || !!state.itemUse);
8978
8980
  const holdLeft = !isRight && (state.itemUse?.id === "open" || state.itemUse?.id === "close");
8979
- const grip = clipId === "punch_right" ? 1.4 : clipId === "shove_two_hands" ? 0.15 : state.armed || holdRight || holdLeft ? 1.22 : running ? 0.85 : 0.12;
8981
+ const grip = clipId?.startsWith("punch_") ? 1.4 : clipId === "shove_two_hands" ? 0.15 : state.armed || holdRight || holdLeft ? 1.22 : running ? 0.85 : 0.12;
8980
8982
  const wristSwing = holdRight || state.armed || running ? 0 : -Math.min(1, gait) * Math.sin(ph - 0.6) * 0.12 * hand.side;
8981
8983
  setHandGrip(hand, grip, wristSwing);
8982
8984
  });
@@ -9022,9 +9024,9 @@ var GameCharactersThree = (() => {
9022
9024
  channels: "upper",
9023
9025
  keys: [
9024
9026
  { t: 0, pose: { lean: 0, twist: 0, side: 0, headX: 0, headY: 0, headZ: 0, armR: 0.05, armL: -0.1, elbR: -0.2, elbL: -0.2, armRz: 0, armLz: 0 } },
9025
- { t: 0.1, pose: { lean: 0.04, twist: -0.35, side: 0, headX: 0, headY: 0.28, headZ: 0, armR: 0.45, armL: -0.7, elbR: -1.9, elbL: -1.6, armRz: 0, armLz: 0.1 } },
9026
- { t: 0.16, pose: { lean: 0.2, twist: 0.5, side: 0, headX: 0, headY: -0.38, headZ: 0, armR: -1.5, armL: -0.6, elbR: -0.1, elbL: -1.7, armRz: -0.1, armLz: 0.1 } },
9027
- { t: 0.22, pose: { lean: 0.16, twist: 0.4, side: 0, headX: 0, headY: -0.3, headZ: 0, armR: -1.35, armL: -0.6, elbR: -0.25, elbL: -1.6, armRz: -0.08, armLz: 0.08 } },
9027
+ { t: 0.1, pose: { lean: 0.04, twist: 0.3, side: 0, headX: 0, headY: -0.24, headZ: 0, armR: -0.9, armL: -0.35, elbR: -1.9, elbL: -2.1, armRz: -0.2, armLz: 0.15 } },
9028
+ { t: 0.16, pose: { lean: 0.16, twist: -0.35, side: 0, headX: 0, headY: 0.3, headZ: 0, armR: -0.95, armL: -1.55, elbR: -1.95, elbL: -0.05, armRz: -0.25, armLz: 0.3 } },
9029
+ { t: 0.22, pose: { lean: 0.13, twist: -0.3, side: 0, headX: 0, headY: 0.25, headZ: 0, armR: -0.9, armL: -1.45, elbR: -1.9, elbL: -0.2, armRz: -0.22, armLz: 0.26 } },
9028
9030
  { t: 0.35, pose: { lean: 0, twist: 0, side: 0, headX: 0, headY: 0, headZ: 0, armR: 0.05, armL: -0.1, elbR: -0.2, elbL: -0.2, armRz: 0, armLz: 0 } }
9029
9031
  ],
9030
9032
  events: [
@@ -9062,9 +9064,9 @@ var GameCharactersThree = (() => {
9062
9064
  channels: "full",
9063
9065
  keys: [
9064
9066
  { t: 0, pose: { lean: 0, twist: 0, side: 0, drop: 0, headX: 0, headY: 0, headZ: 0, armR: 0.05, armL: -0.1, elbR: -0.2, elbL: -0.2, armRz: 0, armLz: 0, legL: -0.05, legR: 0.05, kneeL: 0.05, kneeR: 0.05 } },
9065
- { t: 0.18, pose: { lean: -0.1, twist: 0, side: 0.04, drop: -0.05, headX: 0.05, headY: 0, headZ: 0, armR: -0.5, armL: 0.4, elbR: -0.5, elbL: -0.4, armRz: -0.1, armLz: 0.1, legL: 0.1, legR: -1.1, kneeL: 0.35, kneeR: 1.8 } },
9066
- { t: 0.32, pose: { lean: -0.22, twist: 0, side: 0.04, drop: -0.06, headX: 0.1, headY: 0, headZ: 0, armR: -0.7, armL: 0.7, elbR: -0.6, elbL: -0.3, armRz: -0.15, armLz: 0.15, legL: 0.12, legR: -1.45, kneeL: 0.2, kneeR: 0.1 } },
9067
- { t: 0.42, pose: { lean: -0.18, twist: 0, side: 0.03, drop: -0.05, headX: 0.08, headY: 0, headZ: 0, armR: -0.5, armL: 0.5, elbR: -0.5, elbL: -0.3, armRz: -0.1, armLz: 0.1, legL: 0.1, legR: -1.2, kneeL: 0.3, kneeR: 1.2 } },
9067
+ { t: 0.18, pose: { lean: -0.1, twist: 0, side: -0.04, drop: -0.05, headX: 0.05, headY: 0, headZ: 0, armR: 0.4, armL: -0.5, elbR: -0.4, elbL: -0.5, armRz: -0.1, armLz: 0.1, legL: -1.1, legR: 0.1, kneeL: 1.8, kneeR: 0.35 } },
9068
+ { t: 0.32, pose: { lean: -0.22, twist: 0, side: -0.04, drop: -0.06, headX: 0.1, headY: 0, headZ: 0, armR: 0.7, armL: -0.7, elbR: -0.3, elbL: -0.6, armRz: -0.15, armLz: 0.15, legL: -1.45, legR: 0.12, kneeL: 0.1, kneeR: 0.2 } },
9069
+ { t: 0.42, pose: { lean: -0.18, twist: 0, side: -0.03, drop: -0.05, headX: 0.08, headY: 0, headZ: 0, armR: 0.5, armL: -0.5, elbR: -0.3, elbL: -0.5, armRz: -0.1, armLz: 0.1, legL: -1.2, legR: 0.1, kneeL: 1.2, kneeR: 0.3 } },
9068
9070
  { t: 0.6, pose: { lean: 0, twist: 0, side: 0, drop: 0, headX: 0, headY: 0, headZ: 0, armR: 0.05, armL: -0.1, elbR: -0.2, elbL: -0.2, armRz: 0, armLz: 0, legL: -0.05, legR: 0.05, kneeL: 0.05, kneeR: 0.05 } }
9069
9071
  ],
9070
9072
  events: [
@@ -9670,10 +9672,11 @@ var GameCharactersThree = (() => {
9670
9672
  const palmAt = new THREE.Vector3();
9671
9673
  const fingers = new THREE.Vector3();
9672
9674
  const towards = new THREE.Vector3();
9673
- const fromChannel = (channel, side, base, out) => {
9675
+ const fromChannel = (channel, side, base, out, from = side) => {
9674
9676
  out.p.set(...channel.p);
9675
9677
  out.q.copy(quaternionOf(channel.r));
9676
- if (channel.mirror && side === "L") {
9678
+ const rightNumbers = from === "R" || channel.mirror;
9679
+ if (rightNumbers !== (side === "R")) {
9677
9680
  out.p.x = -out.p.x;
9678
9681
  out.q.copy(mirrorQuaternion(THREE, out.q));
9679
9682
  }
@@ -9738,7 +9741,7 @@ var GameCharactersThree = (() => {
9738
9741
  play(id, options2) {
9739
9742
  const clip = clips.get(id);
9740
9743
  if (!clip) return false;
9741
- run = { clip, time: 0, left: options2?.leg === "left" };
9744
+ run = { clip, time: 0, left: options2?.leg === "left", swap: options2?.hand === "left" };
9742
9745
  fire(clip, -1e-9, 0);
9743
9746
  return true;
9744
9747
  },
@@ -9786,10 +9789,11 @@ var GameCharactersThree = (() => {
9786
9789
  arm.blend = blendSeconds <= 0 ? 1 : Math.min(1, arm.blend + dt / blendSeconds);
9787
9790
  if (arm.blend >= 1) copyPose(arm.shown, desired);
9788
9791
  else mixPose(THREE, arm.shown, arm.from, desired, ease2(arm.blend));
9789
- const channel = sample?.[side];
9792
+ const from = run?.swap ? side === "R" ? "L" : "R" : side;
9793
+ const channel = sample?.[from];
9790
9794
  let pose;
9791
9795
  if (channel && channel.w > 0) {
9792
- fromChannel(channel, side, arm.shown, channelPose);
9796
+ fromChannel(channel, side, arm.shown, channelPose, from);
9793
9797
  mixPose(THREE, mixed, arm.shown, channelPose, channel.w);
9794
9798
  pose = mode === "target" ? mixed : keepOutside(mixed, handReach(mixed));
9795
9799
  } else pose = mode === "target" ? arm.shown : keepOutside(arm.shown, handReach(arm.shown));
@@ -9878,9 +9882,9 @@ var GameCharactersThree = (() => {
9878
9882
  strike: (clipId, strikeOptions) => {
9879
9883
  const clip = strikes.get(clipId);
9880
9884
  if (!clip) return null;
9881
- const left = strikeOptions?.leg === "left";
9885
+ const left = strikeOptions?.leg === "left" || strikeOptions?.hand === "left";
9882
9886
  avatar.play(left ? leftOf(clip) : clip);
9883
- arms?.play(clipId, left ? { leg: "left" } : void 0);
9887
+ arms?.play(clipId, strikeOptions?.leg === "left" ? { leg: "left" } : strikeOptions?.hand === "left" ? { hand: "left" } : void 0);
9884
9888
  return impactOf(clip);
9885
9889
  }
9886
9890
  };