@adatechnology/game-characters-three 0.4.1 → 0.5.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/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,17 @@ 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; while crouched only a kick takes the legs, so a punch or the guard stays low) 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. After a punch (an `impact` of kind `punch`) the body holds the guard (`guardClip`, default `'fight_guard'`, `false` for none) for
52
+ `guardHold` seconds (default 1.2) while it stands still, so a combo goes guard, punch, guard; the punch blends in over `punchBlend` (default 0.05 s). Walking, running, a weapon or a held item drop the guard.
52
53
 
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.
54
+ **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
55
 
55
56
  ## Geometry
56
57
 
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;
@@ -1066,6 +1071,11 @@ type AvatarUpdate = {
1066
1071
  holding?: boolean;
1067
1072
  /** Active scripted item use (`drink`, `sip`, `chug`, `pour`, `open`, `close`, `tilt`). */
1068
1073
  itemUse?: AvatarItemUsePose | null;
1074
+ /** Hands on a piece of furniture: `grip` closes on it with `progress`, `push` walks with it, `strain` strains against what does not give. */
1075
+ push?: {
1076
+ readonly phase: 'grip' | 'push' | 'strain';
1077
+ readonly progress: number;
1078
+ } | null;
1069
1079
  };
1070
1080
  type PlayPoseOptions = {
1071
1081
  onEvent?: (event: AvatarEvent) => void;
@@ -1206,9 +1216,10 @@ type FirstPersonArms = {
1206
1216
  /** Add it to the scene: it follows the camera by itself. */
1207
1217
  root: THREE_NS.Group;
1208
1218
  /** 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. */
1219
+ /** `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
1220
  play(clip: string, options?: {
1211
1221
  leg?: 'left' | 'right';
1222
+ hand?: 'left' | 'right';
1212
1223
  }): boolean;
1213
1224
  stop(): void;
1214
1225
  /** The clip being played, or null. */
@@ -1243,6 +1254,10 @@ type RoomAvatarLike = {
1243
1254
  down: boolean;
1244
1255
  armed: boolean;
1245
1256
  pitch: number;
1257
+ push?: {
1258
+ readonly phase: 'grip' | 'push' | 'strain';
1259
+ readonly progress: number;
1260
+ } | null;
1246
1261
  holding?: boolean;
1247
1262
  itemUse?: AvatarItemUsePose | null;
1248
1263
  }): void;
@@ -1263,6 +1278,7 @@ type RoomAvatarLike = {
1263
1278
  /** Plays a strike on the body and the arms and answers the seconds to its impact (`RoomAvatar.strike`). */
1264
1279
  strike(clipId: string, options?: {
1265
1280
  leg?: 'left' | 'right';
1281
+ hand?: 'left' | 'right';
1266
1282
  }): number | null;
1267
1283
  /** The avatar it wraps. */
1268
1284
  readonly avatar: PlayerAvatar;
@@ -1276,12 +1292,19 @@ type ToRoomAvatarOptions = {
1276
1292
  strikeClips?: readonly PoseClip[];
1277
1293
  /** The first-person clips the arms play. Default `FIRST_PERSON_CLIPS` of game-characters-people. */
1278
1294
  firstPersonClips?: readonly FirstPersonClip[];
1295
+ /** The looping clip the body holds between punches (a clip of `strikeClips`), so a combo goes guard, punch, guard instead of dropping the hands each time; `false` for none. Default `'fight_guard'`. */
1296
+ guardClip?: string | false;
1297
+ /** Seconds the guard stays up after a punch ends. Default 1.2. */
1298
+ guardHold?: number;
1299
+ /** Seconds a punch takes to blend in over the walk (or the guard it comes from). Default 0.05: the fist leaves at once. */
1300
+ punchBlend?: number;
1279
1301
  };
1280
1302
  /**
1281
1303
  * 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
1304
  * 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
1305
  * 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.
1306
+ * 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.
1307
+ * After a punch the body holds the guard (`guardClip`) for `guardHold` seconds while it stands, so the next punch leaves from the guard.
1285
1308
  */
1286
1309
  declare const toRoomAvatar: (avatar: PlayerAvatar, options?: ToRoomAvatarOptions) => RoomAvatarLike;
1287
1310
 
@@ -8479,6 +8479,7 @@ var GameCharactersThree = (() => {
8479
8479
  return {
8480
8480
  id: CHARACTERS_AREA_ID,
8481
8481
  name: text.name,
8482
+ ground: { surface: "concrete_slab" },
8482
8483
  group: text.group,
8483
8484
  get entries() {
8484
8485
  return entries;
@@ -8640,6 +8641,7 @@ var GameCharactersThree = (() => {
8640
8641
  var TWO_PI2 = Math.PI * 2;
8641
8642
  var MAX_GAIT = 1.4;
8642
8643
  var CLIP_BLEND = 0.12;
8644
+ var CLIP_CASCADE = 5;
8643
8645
  var FALL_SECONDS2 = 0.3;
8644
8646
  var HIP_HEIGHT = 0.95;
8645
8647
  var WALK_ARM = 0.28;
@@ -8679,6 +8681,7 @@ var GameCharactersThree = (() => {
8679
8681
  const blendDefault = options.blend ?? CLIP_BLEND;
8680
8682
  const fallSeconds = options.fallSeconds ?? FALL_SECONDS2;
8681
8683
  const smooth = options.cascade ?? true;
8684
+ const clipCascade = options.clipCascade ?? CLIP_CASCADE;
8682
8685
  const root = new THREE.Group();
8683
8686
  const tilt = new THREE.Group();
8684
8687
  tilt.add(body);
@@ -8778,6 +8781,24 @@ var GameCharactersThree = (() => {
8778
8781
  });
8779
8782
  return { armR: shoulder, armL: shoulder, armRz: AIMED.zRight, armLz: AIMED.zLeft, elbR: AIMED.elbowRight, elbL: AIMED.elbowLeft };
8780
8783
  };
8784
+ const pushingPose = (push) => {
8785
+ const on = push.phase === "grip" ? Math.min(1, push.progress * 2.2) : 1;
8786
+ const strain = push.phase === "strain" ? 1 : 0;
8787
+ const shake = strain * Math.sin(time * 40) * 0.025;
8788
+ const raise = -1.5 * on;
8789
+ const elbow = -0.9 + 0.72 * (push.phase === "grip" ? push.progress : 1) + strain * 0.12;
8790
+ return {
8791
+ lean: (0.2 + 0.2 * strain) * on,
8792
+ headX: (0.05 + 0.08 * strain) * on,
8793
+ armR: raise + shake,
8794
+ armL: raise - shake,
8795
+ armRz: -0.08 * on,
8796
+ armLz: 0.08 * on,
8797
+ elbR: elbow,
8798
+ elbL: elbow,
8799
+ ...strain ? { legL: -0.5, legR: 0.4, kneeL: 0.55, kneeR: 0.08, drop: -0.05 } : {}
8800
+ };
8801
+ };
8781
8802
  const holdingPose = (gait, itemUse) => {
8782
8803
  const bob = Math.sin(cycles * TWO_PI2) * 0.05 * gait;
8783
8804
  if (!itemUse) {
@@ -8905,7 +8926,7 @@ var GameCharactersThree = (() => {
8905
8926
  const fireClipEvents = (r, from, to) => {
8906
8927
  for (const event of clipEventsBetween(r.clip, from, to)) emit({ ...event, clip: r.clip.id }, r.onEvent);
8907
8928
  };
8908
- const stepClip = (dt, down) => {
8929
+ const stepClip = (dt, down, crouch) => {
8909
8930
  if (!run) return;
8910
8931
  const r = run;
8911
8932
  const from = r.time;
@@ -8925,7 +8946,9 @@ var GameCharactersThree = (() => {
8925
8946
  return;
8926
8947
  }
8927
8948
  if (down) return;
8928
- const sampled = filterPoseChannels(samplePoseClip(r.clip, Math.min(r.time, duration)), r.clip.channels ?? "full");
8949
+ const keepsCrouch = crouch && !r.clip.events?.some((event) => event.data?.kind === "kick");
8950
+ const channels = r.clip.channels ?? "full";
8951
+ const sampled = filterPoseChannels(filterPoseChannels(samplePoseClip(r.clip, Math.min(r.time, duration)), channels), keepsCrouch ? "upper" : "full");
8929
8952
  const current = readPose();
8930
8953
  const blended = {};
8931
8954
  for (const [name, value] of Object.entries(sampled)) if (name in current) blended[name] = current[name] + (value - current[name]) * r.weight;
@@ -8956,7 +8979,8 @@ var GameCharactersThree = (() => {
8956
8979
  else {
8957
8980
  if (running) animator.setPose(rig, runningPose(gait, Boolean(state.armed || state.holding || state.itemUse)));
8958
8981
  if (state.crouch) animator.setPose(rig, crouchPose(gait));
8959
- if (state.armed) animator.setPose(rig, armed(state.pitch ?? 0, gait, state.hip ?? 0));
8982
+ if (state.push) animator.setPose(rig, pushingPose(state.push));
8983
+ else if (state.armed) animator.setPose(rig, armed(state.pitch ?? 0, gait, state.hip ?? 0));
8960
8984
  else if (state.holding || state.itemUse) animator.setPose(rig, holdingPose(gait, state.itemUse));
8961
8985
  else if (!running) {
8962
8986
  animator.setPose(rig, {
@@ -8968,15 +8992,15 @@ var GameCharactersThree = (() => {
8968
8992
  }
8969
8993
  }
8970
8994
  placeBody(fall);
8971
- stepClip(dt, down);
8972
- if (smooth) animator.cascade(rig, dt);
8973
- const clipId = run ? run.clip.id : null;
8995
+ stepClip(dt, down, Boolean(state.crouch));
8996
+ if (smooth) animator.cascade(rig, run && !run.clip.loop ? dt * clipCascade : dt);
8997
+ const blow = run?.clip.events?.find((event) => (event.type === "impact" || event.type === "grip") && event.data?.kind)?.data?.kind;
8974
8998
  const ph = cycles * TWO_PI2;
8975
8999
  hands.forEach((hand) => {
8976
9000
  const isRight = hand.side < 0;
8977
9001
  const holdRight = isRight && (!!state.holding || !!state.itemUse);
8978
9002
  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;
9003
+ const grip = blow === "punch" ? 1.4 : blow === "shove" ? 0.15 : state.armed || holdRight || holdLeft ? 1.22 : running ? 0.85 : 0.12;
8980
9004
  const wristSwing = holdRight || state.armed || running ? 0 : -Math.min(1, gait) * Math.sin(ph - 0.6) * 0.12 * hand.side;
8981
9005
  setHandGrip(hand, grip, wristSwing);
8982
9006
  });
@@ -9016,22 +9040,97 @@ var GameCharactersThree = (() => {
9016
9040
  clips: [
9017
9041
  {
9018
9042
  id: "punch_right",
9019
- source: { authored: "game-kit", note: "authored in the kit; replace with the Elenco team's clip when delivered" },
9020
- duration: 0.35,
9043
+ source: { authored: "game-kit", note: "authored in the kit from boxing technique (guard, hip and shoulder turn, full extension, weight over the front knee, back to the guard); the right hand's straight punch (the cross; the left plays its mirror); replace with the Elenco team's clip when delivered" },
9044
+ duration: 0.38,
9021
9045
  loop: false,
9022
- channels: "upper",
9046
+ channels: "full",
9023
9047
  keys: [
9024
- { 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 } },
9028
- { 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 } }
9048
+ { t: 0, pose: { lean: 0.12, twist: -0.3, side: 0, drop: -0.06, headX: 0.18, headY: 0.28, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.75, elbL: -2, armLz: 0.55, legL: 0.25, kneeL: 0.3, legR: -0.3, kneeR: 0.3 } },
9049
+ { t: 0.06, pose: { lean: 0.12, twist: -0.42, side: 0, drop: -0.09, headX: 0.18, headY: 0.34, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.7, elbL: -2.1, armLz: 0.55, legL: 0.25, kneeL: 0.42, legR: -0.3, kneeR: 0.3 } },
9050
+ { t: 0.14, pose: { lean: 0.22, twist: 0.5, side: 0, drop: -0.08, headX: 0.18, headY: -0.3, headZ: 0, armR: -0.8, elbR: -2.05, armRz: -0.3, armL: -1.75, elbL: -0.05, armLz: 0.12, legL: 0.35, kneeL: 0.12, legR: -0.3, kneeR: 0.45 } },
9051
+ { t: 0.2, pose: { lean: 0.2, twist: 0.42, side: 0, drop: -0.08, headX: 0.18, headY: -0.26, headZ: 0, armR: -0.8, elbR: -2.05, armRz: -0.3, armL: -1.6, elbL: -0.3, armLz: 0.15, legL: 0.33, kneeL: 0.15, legR: -0.3, kneeR: 0.42 } },
9052
+ { t: 0.38, pose: { lean: 0.12, twist: -0.3, side: 0, drop: -0.06, headX: 0.18, headY: 0.28, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.75, elbL: -2, armLz: 0.55, legL: 0.25, kneeL: 0.3, legR: -0.3, kneeR: 0.3 } }
9029
9053
  ],
9030
9054
  events: [
9031
9055
  { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9032
- { t: 0.1, type: "phase", data: { label: "attack_clip.phase.strike" } },
9033
- { t: 0.16, type: "impact", data: { kind: "punch" } },
9034
- { t: 0.22, type: "phase", data: { label: "attack_clip.phase.recover" } }
9056
+ { t: 0.06, type: "phase", data: { label: "attack_clip.phase.strike" } },
9057
+ { t: 0.14, type: "impact", data: { kind: "punch" } },
9058
+ { t: 0.2, type: "phase", data: { label: "attack_clip.phase.recover" } }
9059
+ ]
9060
+ },
9061
+ {
9062
+ id: "jab",
9063
+ source: { authored: "game-kit", note: "authored in the kit from boxing technique (guard, hip and shoulder turn, full extension, weight over the front knee, back to the guard); the lead hand's jab, written for the right hand (the left plays its mirror, from the orthodox guard); one step of the punch combo; replace with the Elenco team's clip when delivered" },
9064
+ duration: 0.3,
9065
+ loop: false,
9066
+ channels: "full",
9067
+ keys: [
9068
+ { t: 0, pose: { lean: 0.12, twist: 0.3, side: 0, drop: -0.06, headX: 0.18, headY: -0.28, headZ: 0, armL: -0.85, elbL: -1.95, armLz: 0.5, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.3, legL: -0.3, kneeL: 0.3 } },
9069
+ { t: 0.04, pose: { lean: 0.12, twist: 0.34, side: 0, drop: -0.08, headX: 0.18, headY: -0.28, headZ: 0, armL: -0.85, elbL: -1.75, armLz: 0.5, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.3, legL: -0.3, kneeL: 0.36 } },
9070
+ { t: 0.11, pose: { lean: 0.16, twist: 0.55, side: 0, drop: -0.08, headX: 0.18, headY: -0.35, headZ: 0, armL: -1.75, elbL: -0.05, armLz: 0.08, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.22, legL: -0.38, kneeL: 0.38 } },
9071
+ { t: 0.16, pose: { lean: 0.15, twist: 0.5, side: 0, drop: -0.08, headX: 0.18, headY: -0.33, headZ: 0, armL: -1.62, elbL: -0.35, armLz: 0.12, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.3, legL: -0.36, kneeL: 0.36 } },
9072
+ { t: 0.3, pose: { lean: 0.12, twist: 0.3, side: 0, drop: -0.06, headX: 0.18, headY: -0.28, headZ: 0, armL: -0.85, elbL: -1.95, armLz: 0.5, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.3, legL: -0.3, kneeL: 0.3 } }
9073
+ ],
9074
+ events: [
9075
+ { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9076
+ { t: 0.04, type: "phase", data: { label: "attack_clip.phase.strike" } },
9077
+ { t: 0.11, type: "impact", data: { kind: "punch" } },
9078
+ { t: 0.16, type: "phase", data: { label: "attack_clip.phase.recover" } }
9079
+ ]
9080
+ },
9081
+ {
9082
+ id: "hook",
9083
+ source: { authored: "game-kit", note: "authored in the kit from boxing technique (guard, hip and shoulder turn, full extension, weight over the front knee, back to the guard); the lead hand's hook, written for the right hand (the left plays its mirror, from the orthodox guard); one step of the punch combo; replace with the Elenco team's clip when delivered" },
9084
+ duration: 0.42,
9085
+ loop: false,
9086
+ channels: "full",
9087
+ keys: [
9088
+ { t: 0, pose: { lean: 0.12, twist: 0.3, side: 0, drop: -0.06, headX: 0.18, headY: -0.28, headZ: 0, armL: -0.85, elbL: -1.95, armLz: 0.5, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.3, legL: -0.3, kneeL: 0.3 } },
9089
+ { t: 0.08, pose: { lean: 0.12, twist: 0.12, side: 0, drop: -0.1, headX: 0.18, headY: -0.15, headZ: 0, armL: -0.45, elbL: -1.9, armLz: -0.85, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.3, legL: -0.3, kneeL: 0.5 } },
9090
+ { t: 0.18, pose: { lean: 0.14, twist: 0.85, side: 0, drop: -0.08, headX: 0.18, headY: -0.48, headZ: 0, armL: 0, elbL: -1.6, armLz: -1.62, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.38, legL: -0.3, kneeL: 0.36 } },
9091
+ { t: 0.25, pose: { lean: 0.13, twist: 0.95, side: 0, drop: -0.08, headX: 0.18, headY: -0.5, headZ: 0, armL: 0, elbL: -1.7, armLz: -1.5, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.38, legL: -0.3, kneeL: 0.36 } },
9092
+ { t: 0.42, pose: { lean: 0.12, twist: 0.3, side: 0, drop: -0.06, headX: 0.18, headY: -0.28, headZ: 0, armL: -0.85, elbL: -1.95, armLz: 0.5, armR: -0.75, elbR: -2, armRz: -0.55, legR: 0.25, kneeR: 0.3, legL: -0.3, kneeL: 0.3 } }
9093
+ ],
9094
+ events: [
9095
+ { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9096
+ { t: 0.08, type: "phase", data: { label: "attack_clip.phase.strike" } },
9097
+ { t: 0.18, type: "impact", data: { kind: "punch" } },
9098
+ { t: 0.25, type: "phase", data: { label: "attack_clip.phase.recover" } }
9099
+ ]
9100
+ },
9101
+ {
9102
+ id: "uppercut",
9103
+ source: { authored: "game-kit", note: "authored in the kit from boxing technique (guard, hip and shoulder turn, full extension, weight over the front knee, back to the guard); the rear hand's uppercut; one step of the punch combo; replace with the Elenco team's clip when delivered" },
9104
+ duration: 0.42,
9105
+ loop: false,
9106
+ channels: "full",
9107
+ keys: [
9108
+ { t: 0, pose: { lean: 0.12, twist: -0.3, side: 0, drop: -0.06, headX: 0.18, headY: 0.28, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.75, elbL: -2, armLz: 0.55, legL: 0.25, kneeL: 0.3, legR: -0.3, kneeR: 0.3 } },
9109
+ { t: 0.09, pose: { lean: 0.25, twist: -0.12, side: 0, drop: -0.15, headX: 0.18, headY: 0.3, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.15, elbL: -1.75, armLz: 0.1, legL: 0.25, kneeL: 0.65, legR: -0.3, kneeR: 0.55 } },
9110
+ { t: 0.19, pose: { lean: -0.04, twist: 0.45, side: 0, drop: -0.02, headX: 0.08, headY: -0.25, headZ: 0, armR: -0.8, elbR: -2.05, armRz: -0.5, armL: -0.95, elbL: -2, armLz: 0.6, legL: 0.25, kneeL: 0.12, legR: -0.3, kneeR: 0.25 } },
9111
+ { t: 0.26, pose: { lean: -0.02, twist: 0.4, side: 0, drop: -0.03, headX: 0.1, headY: -0.22, headZ: 0, armR: -0.8, elbR: -2.05, armRz: -0.5, armL: -1, elbL: -2.05, armLz: 0.55, legL: 0.25, kneeL: 0.15, legR: -0.3, kneeR: 0.28 } },
9112
+ { t: 0.42, pose: { lean: 0.12, twist: -0.3, side: 0, drop: -0.06, headX: 0.18, headY: 0.28, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.75, elbL: -2, armLz: 0.55, legL: 0.25, kneeL: 0.3, legR: -0.3, kneeR: 0.3 } }
9113
+ ],
9114
+ events: [
9115
+ { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9116
+ { t: 0.09, type: "phase", data: { label: "attack_clip.phase.strike" } },
9117
+ { t: 0.19, type: "impact", data: { kind: "punch" } },
9118
+ { t: 0.26, type: "phase", data: { label: "attack_clip.phase.recover" } }
9119
+ ]
9120
+ },
9121
+ {
9122
+ id: "fight_guard",
9123
+ source: { authored: "game-kit", note: "authored in the kit from boxing technique (guard, hip and shoulder turn, full extension, weight over the front knee, back to the guard); the guard a punch comes from and goes back to (orthodox: left foot and left fist forward, chin down); replace with the Elenco team's clip when delivered" },
9124
+ duration: 1.2,
9125
+ loop: true,
9126
+ channels: "full",
9127
+ keys: [
9128
+ { t: 0, pose: { lean: 0.12, twist: -0.3, side: 0, drop: -0.06, headX: 0.18, headY: 0.28, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.75, elbL: -2, armLz: 0.55, legL: 0.25, kneeL: 0.3, legR: -0.3, kneeR: 0.3 } },
9129
+ { t: 0.6, pose: { lean: 0.13, twist: -0.3, side: 0, drop: -0.075, headX: 0.2, headY: 0.28, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.75, elbL: -2, armLz: 0.55, legL: 0.25, kneeL: 0.3, legR: -0.3, kneeR: 0.3 } },
9130
+ { t: 1.2, pose: { lean: 0.12, twist: -0.3, side: 0, drop: -0.06, headX: 0.18, headY: 0.28, headZ: 0, armR: -0.85, elbR: -1.95, armRz: -0.5, armL: -0.75, elbL: -2, armLz: 0.55, legL: 0.25, kneeL: 0.3, legR: -0.3, kneeR: 0.3 } }
9131
+ ],
9132
+ events: [
9133
+ { t: 0, type: "grip", data: { kind: "punch" } }
9035
9134
  ]
9036
9135
  },
9037
9136
  {
@@ -9062,9 +9161,9 @@ var GameCharactersThree = (() => {
9062
9161
  channels: "full",
9063
9162
  keys: [
9064
9163
  { 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 } },
9164
+ { 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 } },
9165
+ { 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 } },
9166
+ { 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
9167
  { 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
9168
  ],
9070
9169
  events: [
@@ -9127,19 +9226,73 @@ var GameCharactersThree = (() => {
9127
9226
  {
9128
9227
  id: "punch_right",
9129
9228
  source: { authored: "game-kit", note: "first-person counterpart of attack-clips.json (same duration, same events); authored in the kit, replace with the Elenco team's clips when delivered" },
9130
- duration: 0.35,
9229
+ duration: 0.38,
9230
+ keys: [
9231
+ { t: 0, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } },
9232
+ { t: 0.06, R: { p: [0.21, -0.15, -0.29], r: [0.1, 1.45, 0.2], grip: 1 } },
9233
+ { t: 0.14, R: { p: [0.06, -0.09, -0.62], r: [0, 1.5708, -0.05], grip: 1 } },
9234
+ { t: 0.2, R: { p: [0.07, -0.1, -0.56], r: [0, 1.5708, -0.05], grip: 1 } },
9235
+ { t: 0.38, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } }
9236
+ ],
9237
+ events: [
9238
+ { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9239
+ { t: 0.06, type: "phase", data: { label: "attack_clip.phase.strike" } },
9240
+ { t: 0.14, type: "impact", data: { kind: "punch" } },
9241
+ { t: 0.2, type: "phase", data: { label: "attack_clip.phase.recover" } }
9242
+ ]
9243
+ },
9244
+ {
9245
+ id: "jab",
9246
+ source: { authored: "game-kit", note: "first-person counterpart of attack-clips.json (same duration, same events), for the right hand (the left plays its mirror); authored in the kit, replace with the Elenco team's clips when delivered" },
9247
+ duration: 0.3,
9248
+ keys: [
9249
+ { t: 0, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } },
9250
+ { t: 0.04, R: { p: [0.17, -0.13, -0.34], r: [0.1, 1.45, 0.2], grip: 1 } },
9251
+ { t: 0.11, R: { p: [0.08, -0.08, -0.62], r: [0, 1.5708, -0.05], grip: 1 } },
9252
+ { t: 0.16, R: { p: [0.09, -0.09, -0.57], r: [0, 1.5708, -0.05], grip: 1 } },
9253
+ { t: 0.3, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } }
9254
+ ],
9255
+ events: [
9256
+ { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9257
+ { t: 0.04, type: "phase", data: { label: "attack_clip.phase.strike" } },
9258
+ { t: 0.11, type: "impact", data: { kind: "punch" } },
9259
+ { t: 0.16, type: "phase", data: { label: "attack_clip.phase.recover" } }
9260
+ ]
9261
+ },
9262
+ {
9263
+ id: "hook",
9264
+ source: { authored: "game-kit", note: "first-person counterpart of attack-clips.json (same duration, same events), for the right hand (the left plays its mirror); authored in the kit, replace with the Elenco team's clips when delivered" },
9265
+ duration: 0.42,
9266
+ keys: [
9267
+ { t: 0, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } },
9268
+ { t: 0.08, R: { p: [0.3, -0.12, -0.4], r: [0, 1.5, 0.6], grip: 1 } },
9269
+ { t: 0.18, R: { p: [0.02, -0.06, -0.5], r: [0, 2.5, 1.3], grip: 1 } },
9270
+ { t: 0.25, R: { p: [0, -0.07, -0.47], r: [0, 2.5, 1.3], grip: 1 } },
9271
+ { t: 0.42, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } }
9272
+ ],
9273
+ events: [
9274
+ { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9275
+ { t: 0.08, type: "phase", data: { label: "attack_clip.phase.strike" } },
9276
+ { t: 0.18, type: "impact", data: { kind: "punch" } },
9277
+ { t: 0.25, type: "phase", data: { label: "attack_clip.phase.recover" } }
9278
+ ]
9279
+ },
9280
+ {
9281
+ id: "uppercut",
9282
+ source: { authored: "game-kit", note: "first-person counterpart of attack-clips.json (same duration, same events), for the right hand (the left plays its mirror); authored in the kit, replace with the Elenco team's clips when delivered" },
9283
+ duration: 0.42,
9131
9284
  keys: [
9132
9285
  { t: 0, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } },
9133
- { t: 0.1, R: { p: [0.21, -0.15, -0.29], r: [0.1, 1.45, 0.2], grip: 1 } },
9134
- { t: 0.16, R: { p: [0.06, -0.09, -0.62], r: [0, 1.5708, -0.05], grip: 1 } },
9135
- { t: 0.22, R: { p: [0.07, -0.1, -0.56], r: [0, 1.5708, -0.05], grip: 1 } },
9136
- { t: 0.35, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } }
9286
+ { t: 0.09, R: { p: [0.12, -0.3, -0.34], r: [0, -1.5708, 1.3], grip: 1 } },
9287
+ { t: 0.19, R: { p: [0.05, -0.02, -0.5], r: [0, -1.5708, 1.45], grip: 1 } },
9288
+ { t: 0.26, R: { p: [0.06, -0.04, -0.48], r: [0, -1.5708, 1.45], grip: 1 } },
9289
+ { t: 0.42, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } }
9137
9290
  ],
9138
9291
  events: [
9139
9292
  { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9140
- { t: 0.1, type: "phase", data: { label: "attack_clip.phase.strike" } },
9141
- { t: 0.16, type: "impact", data: { kind: "punch" } },
9142
- { t: 0.22, type: "phase", data: { label: "attack_clip.phase.recover" } }
9293
+ { t: 0.09, type: "phase", data: { label: "attack_clip.phase.strike" } },
9294
+ { t: 0.19, type: "impact", data: { kind: "punch" } },
9295
+ { t: 0.26, type: "phase", data: { label: "attack_clip.phase.recover" } }
9143
9296
  ]
9144
9297
  },
9145
9298
  {
@@ -9670,10 +9823,11 @@ var GameCharactersThree = (() => {
9670
9823
  const palmAt = new THREE.Vector3();
9671
9824
  const fingers = new THREE.Vector3();
9672
9825
  const towards = new THREE.Vector3();
9673
- const fromChannel = (channel, side, base, out) => {
9826
+ const fromChannel = (channel, side, base, out, from = side) => {
9674
9827
  out.p.set(...channel.p);
9675
9828
  out.q.copy(quaternionOf(channel.r));
9676
- if (channel.mirror && side === "L") {
9829
+ const rightNumbers = from === "R" || channel.mirror;
9830
+ if (rightNumbers !== (side === "R")) {
9677
9831
  out.p.x = -out.p.x;
9678
9832
  out.q.copy(mirrorQuaternion(THREE, out.q));
9679
9833
  }
@@ -9738,7 +9892,7 @@ var GameCharactersThree = (() => {
9738
9892
  play(id, options2) {
9739
9893
  const clip = clips.get(id);
9740
9894
  if (!clip) return false;
9741
- run = { clip, time: 0, left: options2?.leg === "left" };
9895
+ run = { clip, time: 0, left: options2?.leg === "left", swap: options2?.hand === "left" };
9742
9896
  fire(clip, -1e-9, 0);
9743
9897
  return true;
9744
9898
  },
@@ -9786,10 +9940,11 @@ var GameCharactersThree = (() => {
9786
9940
  arm.blend = blendSeconds <= 0 ? 1 : Math.min(1, arm.blend + dt / blendSeconds);
9787
9941
  if (arm.blend >= 1) copyPose(arm.shown, desired);
9788
9942
  else mixPose(THREE, arm.shown, arm.from, desired, ease2(arm.blend));
9789
- const channel = sample?.[side];
9943
+ const from = run?.swap ? side === "R" ? "L" : "R" : side;
9944
+ const channel = sample?.[from];
9790
9945
  let pose;
9791
9946
  if (channel && channel.w > 0) {
9792
- fromChannel(channel, side, arm.shown, channelPose);
9947
+ fromChannel(channel, side, arm.shown, channelPose, from);
9793
9948
  mixPose(THREE, mixed, arm.shown, channelPose, channel.w);
9794
9949
  pose = mode === "target" ? mixed : keepOutside(mixed, handReach(mixed));
9795
9950
  } else pose = mode === "target" ? arm.shown : keepOutside(arm.shown, handReach(arm.shown));
@@ -9845,6 +10000,8 @@ var GameCharactersThree = (() => {
9845
10000
 
9846
10001
  // src/player/room-avatar.ts
9847
10002
  var impactOf = (clip) => clip.events?.find((event) => event.type === "impact")?.t ?? null;
10003
+ var isPunch = (clip) => clip.events?.some((event) => event.type === "impact" && event.data?.kind === "punch") ?? false;
10004
+ var GUARD_SPEED = 0.3;
9848
10005
  var toRoomAvatar = (avatar, options = {}) => {
9849
10006
  const arms = options.firstPerson === false ? null : createFirstPersonArms(avatar.three, {
9850
10007
  colors: { skin: avatar.colors.skin, sleeve: avatar.colors.longSleeves ? avatar.colors.shirt : avatar.colors.skin, trouser: avatar.colors.trousers, shoe: avatar.colors.shoes, ...options.firstPerson?.colors },
@@ -9859,28 +10016,59 @@ var GameCharactersThree = (() => {
9859
10016
  if (!found) mirrored.set(clip.id, found = mirrorPoseClip(clip, `${clip.id}_left`));
9860
10017
  return found;
9861
10018
  };
10019
+ const guard = options.guardClip === false ? void 0 : strikes.get(options.guardClip ?? "fight_guard");
10020
+ const guardHold = options.guardHold ?? 1.2;
10021
+ const punchBlend = options.punchBlend ?? 0.05;
10022
+ let strikeLeft = 0;
10023
+ let guardLeft = 0;
10024
+ let guarding = false;
10025
+ const stepGuard = (dt, state) => {
10026
+ if (!guard) return;
10027
+ const free = state.speed < GUARD_SPEED && !state.down && !state.armed && !state.holding && !state.push && !state.itemUse && !state.running;
10028
+ if (strikeLeft > 0) {
10029
+ strikeLeft -= dt;
10030
+ if (strikeLeft <= 0 && guardLeft > 0 && free) {
10031
+ avatar.play(guard);
10032
+ guarding = true;
10033
+ }
10034
+ return;
10035
+ }
10036
+ guardLeft = Math.max(0, guardLeft - dt);
10037
+ if (guarding && (guardLeft <= 0 || !free || avatar.playing !== guard.id)) {
10038
+ if (avatar.playing === guard.id) avatar.stop();
10039
+ guarding = false;
10040
+ }
10041
+ };
9862
10042
  return {
9863
10043
  root: avatar.root,
9864
10044
  avatar,
9865
10045
  arms,
9866
10046
  ...arms ? { firstPerson: { root: arms.root, update: (dt, state) => arms.update(dt, state) } } : {},
9867
- update: (dt, state) => avatar.update(dt, {
9868
- speed: state.speed,
9869
- running: state.running,
9870
- crouch: state.crouch,
9871
- down: state.down,
9872
- armed: state.armed,
9873
- pitch: -state.pitch,
9874
- ...state.holding !== void 0 ? { holding: state.holding } : {},
9875
- ...state.itemUse !== void 0 ? { itemUse: state.itemUse } : {}
9876
- }),
10047
+ update: (dt, state) => {
10048
+ stepGuard(dt, state);
10049
+ avatar.update(dt, {
10050
+ speed: state.speed,
10051
+ running: state.running,
10052
+ crouch: state.crouch,
10053
+ down: state.down,
10054
+ armed: state.armed,
10055
+ pitch: -state.pitch,
10056
+ ...state.holding !== void 0 ? { holding: state.holding } : {},
10057
+ ...state.itemUse !== void 0 ? { itemUse: state.itemUse } : {},
10058
+ ...state.push !== void 0 ? { push: state.push } : {}
10059
+ });
10060
+ },
9877
10061
  hand: (side) => avatar.hand(side),
9878
10062
  strike: (clipId, strikeOptions) => {
9879
10063
  const clip = strikes.get(clipId);
9880
10064
  if (!clip) return null;
9881
- const left = strikeOptions?.leg === "left";
9882
- avatar.play(left ? leftOf(clip) : clip);
9883
- arms?.play(clipId, left ? { leg: "left" } : void 0);
10065
+ const left = strikeOptions?.leg === "left" || strikeOptions?.hand === "left";
10066
+ const punch = guard !== void 0 && isPunch(clip);
10067
+ avatar.play(left ? leftOf(clip) : clip, punch ? { blend: punchBlend } : void 0);
10068
+ strikeLeft = punch ? clip.duration : 0;
10069
+ guardLeft = punch ? guardHold : 0;
10070
+ guarding = false;
10071
+ arms?.play(clipId, strikeOptions?.leg === "left" ? { leg: "left" } : strikeOptions?.hand === "left" ? { hand: "left" } : void 0);
9884
10072
  return impactOf(clip);
9885
10073
  }
9886
10074
  };
@@ -9992,6 +10180,7 @@ var GameCharactersThree = (() => {
9992
10180
  return {
9993
10181
  id: AVATAR_AREA_ID,
9994
10182
  name: textOf().name,
10183
+ ground: { surface: "concrete_slab" },
9995
10184
  group: textOf().group,
9996
10185
  state,
9997
10186
  items: () => [],