@adatechnology/game-characters-three 0.4.2 → 0.5.1

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
@@ -41,14 +41,15 @@ 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). 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.
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). `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.
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
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
 
package/dist/index.d.ts CHANGED
@@ -1071,6 +1071,11 @@ type AvatarUpdate = {
1071
1071
  holding?: boolean;
1072
1072
  /** Active scripted item use (`drink`, `sip`, `chug`, `pour`, `open`, `close`, `tilt`). */
1073
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;
1074
1079
  };
1075
1080
  type PlayPoseOptions = {
1076
1081
  onEvent?: (event: AvatarEvent) => void;
@@ -1249,6 +1254,10 @@ type RoomAvatarLike = {
1249
1254
  down: boolean;
1250
1255
  armed: boolean;
1251
1256
  pitch: number;
1257
+ push?: {
1258
+ readonly phase: 'grip' | 'push' | 'strain';
1259
+ readonly progress: number;
1260
+ } | null;
1252
1261
  holding?: boolean;
1253
1262
  itemUse?: AvatarItemUsePose | null;
1254
1263
  }): void;
@@ -1283,12 +1292,19 @@ type ToRoomAvatarOptions = {
1283
1292
  strikeClips?: readonly PoseClip[];
1284
1293
  /** The first-person clips the arms play. Default `FIRST_PERSON_CLIPS` of game-characters-people. */
1285
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;
1286
1301
  };
1287
1302
  /**
1288
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
1289
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
1290
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
1291
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.
1292
1308
  */
1293
1309
  declare const toRoomAvatar: (avatar: PlayerAvatar, options?: ToRoomAvatarOptions) => RoomAvatarLike;
1294
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;
@@ -8780,6 +8781,24 @@ var GameCharactersThree = (() => {
8780
8781
  });
8781
8782
  return { armR: shoulder, armL: shoulder, armRz: AIMED.zRight, armLz: AIMED.zLeft, elbR: AIMED.elbowRight, elbL: AIMED.elbowLeft };
8782
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
+ };
8783
8802
  const holdingPose = (gait, itemUse) => {
8784
8803
  const bob = Math.sin(cycles * TWO_PI2) * 0.05 * gait;
8785
8804
  if (!itemUse) {
@@ -8907,7 +8926,7 @@ var GameCharactersThree = (() => {
8907
8926
  const fireClipEvents = (r, from, to) => {
8908
8927
  for (const event of clipEventsBetween(r.clip, from, to)) emit({ ...event, clip: r.clip.id }, r.onEvent);
8909
8928
  };
8910
- const stepClip = (dt, down) => {
8929
+ const stepClip = (dt, down, crouch) => {
8911
8930
  if (!run) return;
8912
8931
  const r = run;
8913
8932
  const from = r.time;
@@ -8927,7 +8946,9 @@ var GameCharactersThree = (() => {
8927
8946
  return;
8928
8947
  }
8929
8948
  if (down) return;
8930
- 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");
8931
8952
  const current = readPose();
8932
8953
  const blended = {};
8933
8954
  for (const [name, value] of Object.entries(sampled)) if (name in current) blended[name] = current[name] + (value - current[name]) * r.weight;
@@ -8958,7 +8979,8 @@ var GameCharactersThree = (() => {
8958
8979
  else {
8959
8980
  if (running) animator.setPose(rig, runningPose(gait, Boolean(state.armed || state.holding || state.itemUse)));
8960
8981
  if (state.crouch) animator.setPose(rig, crouchPose(gait));
8961
- 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));
8962
8984
  else if (state.holding || state.itemUse) animator.setPose(rig, holdingPose(gait, state.itemUse));
8963
8985
  else if (!running) {
8964
8986
  animator.setPose(rig, {
@@ -8970,15 +8992,15 @@ var GameCharactersThree = (() => {
8970
8992
  }
8971
8993
  }
8972
8994
  placeBody(fall);
8973
- stepClip(dt, down);
8995
+ stepClip(dt, down, Boolean(state.crouch));
8974
8996
  if (smooth) animator.cascade(rig, run && !run.clip.loop ? dt * clipCascade : dt);
8975
- const clipId = run ? run.clip.id : null;
8997
+ const blow = run?.clip.events?.find((event) => (event.type === "impact" || event.type === "grip") && event.data?.kind)?.data?.kind;
8976
8998
  const ph = cycles * TWO_PI2;
8977
8999
  hands.forEach((hand) => {
8978
9000
  const isRight = hand.side < 0;
8979
9001
  const holdRight = isRight && (!!state.holding || !!state.itemUse);
8980
9002
  const holdLeft = !isRight && (state.itemUse?.id === "open" || state.itemUse?.id === "close");
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;
9003
+ const grip = blow === "punch" ? 1.4 : blow === "shove" ? 0.15 : state.armed || holdRight || holdLeft ? 1.22 : running ? 0.85 : 0.12;
8982
9004
  const wristSwing = holdRight || state.armed || running ? 0 : -Math.min(1, gait) * Math.sin(ph - 0.6) * 0.12 * hand.side;
8983
9005
  setHandGrip(hand, grip, wristSwing);
8984
9006
  });
@@ -9018,22 +9040,97 @@ var GameCharactersThree = (() => {
9018
9040
  clips: [
9019
9041
  {
9020
9042
  id: "punch_right",
9021
- source: { authored: "game-kit", note: "authored in the kit; replace with the Elenco team's clip when delivered" },
9022
- 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,
9023
9045
  loop: false,
9024
- channels: "upper",
9046
+ channels: "full",
9025
9047
  keys: [
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 } },
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 } },
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 } }
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 } }
9031
9053
  ],
9032
9054
  events: [
9033
9055
  { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9034
- { t: 0.1, type: "phase", data: { label: "attack_clip.phase.strike" } },
9035
- { t: 0.16, type: "impact", data: { kind: "punch" } },
9036
- { 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" } }
9037
9134
  ]
9038
9135
  },
9039
9136
  {
@@ -9129,19 +9226,73 @@ var GameCharactersThree = (() => {
9129
9226
  {
9130
9227
  id: "punch_right",
9131
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" },
9132
- 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,
9133
9284
  keys: [
9134
9285
  { t: 0, R: { p: [0.15, -0.12, -0.4], r: [0, 1.5, 0.15], grip: 1, w: 0 } },
9135
- { t: 0.1, R: { p: [0.21, -0.15, -0.29], r: [0.1, 1.45, 0.2], grip: 1 } },
9136
- { t: 0.16, R: { p: [0.06, -0.09, -0.62], r: [0, 1.5708, -0.05], grip: 1 } },
9137
- { t: 0.22, R: { p: [0.07, -0.1, -0.56], r: [0, 1.5708, -0.05], grip: 1 } },
9138
- { 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 } }
9139
9290
  ],
9140
9291
  events: [
9141
9292
  { t: 0, type: "phase", data: { label: "attack_clip.phase.windup" } },
9142
- { t: 0.1, type: "phase", data: { label: "attack_clip.phase.strike" } },
9143
- { t: 0.16, type: "impact", data: { kind: "punch" } },
9144
- { 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" } }
9145
9296
  ]
9146
9297
  },
9147
9298
  {
@@ -9849,6 +10000,8 @@ var GameCharactersThree = (() => {
9849
10000
 
9850
10001
  // src/player/room-avatar.ts
9851
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;
9852
10005
  var toRoomAvatar = (avatar, options = {}) => {
9853
10006
  const arms = options.firstPerson === false ? null : createFirstPersonArms(avatar.three, {
9854
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 },
@@ -9863,27 +10016,58 @@ var GameCharactersThree = (() => {
9863
10016
  if (!found) mirrored.set(clip.id, found = mirrorPoseClip(clip, `${clip.id}_left`));
9864
10017
  return found;
9865
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
+ };
9866
10042
  return {
9867
10043
  root: avatar.root,
9868
10044
  avatar,
9869
10045
  arms,
9870
10046
  ...arms ? { firstPerson: { root: arms.root, update: (dt, state) => arms.update(dt, state) } } : {},
9871
- update: (dt, state) => avatar.update(dt, {
9872
- speed: state.speed,
9873
- running: state.running,
9874
- crouch: state.crouch,
9875
- down: state.down,
9876
- armed: state.armed,
9877
- pitch: -state.pitch,
9878
- ...state.holding !== void 0 ? { holding: state.holding } : {},
9879
- ...state.itemUse !== void 0 ? { itemUse: state.itemUse } : {}
9880
- }),
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
+ },
9881
10061
  hand: (side) => avatar.hand(side),
9882
10062
  strike: (clipId, strikeOptions) => {
9883
10063
  const clip = strikes.get(clipId);
9884
10064
  if (!clip) return null;
9885
10065
  const left = strikeOptions?.leg === "left" || strikeOptions?.hand === "left";
9886
- avatar.play(left ? leftOf(clip) : clip);
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;
9887
10071
  arms?.play(clipId, strikeOptions?.leg === "left" ? { leg: "left" } : strikeOptions?.hand === "left" ? { hand: "left" } : void 0);
9888
10072
  return impactOf(clip);
9889
10073
  }
@@ -9996,6 +10180,7 @@ var GameCharactersThree = (() => {
9996
10180
  return {
9997
10181
  id: AVATAR_AREA_ID,
9998
10182
  name: textOf().name,
10183
+ ground: { surface: "concrete_slab" },
9999
10184
  group: textOf().group,
10000
10185
  state,
10001
10186
  items: () => [],