All files / systems/animation FallAnimations.ts

93.93% Statements 31/33
91.66% Branches 22/24
100% Functions 4/4
96.77% Lines 30/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400                                              31x                                                                                                 42x     42x 42x   42x     42x           4x 4x   2x   2x     2x         38x 2x         36x 8x       28x 8x       20x 9x   11x                                                             21x                     21x                                                                             31x                                                                                                                     31x                                                                                                                     31x                                                                                                                   10x   4x   2x     4x                           8x    
/**
 * Fall Animation System for Black Trigram
 * 
 * Implements realistic fall down animations for knockdowns, leg sweeps,
 * and loss of consciousness events. Based on Korean martial arts falling
 * techniques (낙법 - Nakbeop).
 * 
 * @module systems/animation/FallAnimations
 * @category Animation
 * @korean 낙법애니메이션
 */
 
import type { FallType } from "./types";
import { TrigramStance } from "../../types/common";
 
/**
 * Fall animation impact frame numbers
 * 
 * Defines which frame in each fall animation represents ground impact.
 * Used to trigger camera shake, audio, and particle effects.
 * 
 * @korean 낙법충격프레임
 */
export const FALL_IMPACT_FRAMES: Record<FallType, number> = {
  forward: 18,   // Frame 18 of 24 - hands hit ground
  backward: 22,  // Frame 22 of 30 - back impacts
  side_left: 20, // Frame 20 of 27 - shoulder/side impacts
  side_right: 20, // Frame 20 of 27 - shoulder/side impacts
};
 
/**
 * Determines fall direction from attack vector and player facing
 * 
 * Calculates which direction the player should fall based on:
 * - Attack vector (direction of incoming force)
 * - Player facing direction (from stance)
 * - Attack impact point (high/low)
 * 
 * Korean terminology:
 * - 전방낙법 (Jeonbang Nakbeop): Forward fall from rear attacks
 * - 후방낙법 (Hubang Nakbeop): Backward fall from frontal attacks
 * - 측방낙법 (Cheukbang Nakbeop): Side fall from lateral attacks
 * 
 * @param attackAngle - Angle of attack in radians (0 = from front)
 * @param playerFacing - Player facing angle in radians
 * @param attackHeight - Attack height: 'high', 'mid', or 'low'
 * @returns Fall type to use for animation
 * 
 * @example
 * ```typescript
 * // Attack from behind while facing forward
 * const fallType = determineFallDirection(Math.PI, 0, 'mid');
 * // Returns: 'forward' (pushed forward)
 * 
 * // Attack from front while facing forward
 * const fallType = determineFallDirection(0, 0, 'mid');
 * // Returns: 'backward' (pushed backward)
 * 
 * // Attack from left side
 * const fallType = determineFallDirection(-Math.PI/2, 0, 'mid');
 * // Returns: 'side_left'
 * ```
 * 
 * @public
 * @korean 낙법방향결정
 */
export function determineFallDirection(
  attackAngle: number,
  playerFacing: number,
  attackHeight: "high" | "mid" | "low" = "mid"
): FallType {
  // Calculate relative attack angle (attack direction relative to player facing)
  let relativeAngle = attackAngle - playerFacing;
  
  // Normalize to -π to π range
  while (relativeAngle > Math.PI) relativeAngle -= 2 * Math.PI;
  while (relativeAngle < -Math.PI) relativeAngle += 2 * Math.PI;
  
  const absAngle = Math.abs(relativeAngle);
  
  // Leg sweeps always cause side falls (more realistic for sweeps)
  if (attackHeight === "low") {
    // Determine which side based on angle
    // Use a small threshold to avoid edge case at exactly 0
    // Threshold represents the angle within which we consider the sweep "frontal"
    // 0.01 radians ≈ 0.57 degrees - small enough to only catch near-perfect frontal sweeps
    // while still allowing accurate left/right determination for most angles
    const SWEEP_ANGLE_THRESHOLD = 0.01;
    if (Math.abs(relativeAngle) < SWEEP_ANGLE_THRESHOLD) {
      // For perfectly frontal sweeps, default to right side fall
      return "side_right";
    }
    Iif (relativeAngle < 0) {
      return "side_left";
    } else {
      return "side_right";
    }
  }
  
  // High attacks to head often cause backward falls
  if (attackHeight === "high" && absAngle < Math.PI / 3) {
    return "backward";
  }
  
  // Determine fall direction based on attack angle
  // Front quadrant (±45°): Backward fall
  if (absAngle < Math.PI / 4) {
    return "backward";
  }
  
  // Rear quadrant (±45° from back): Forward fall
  if (absAngle > (3 * Math.PI) / 4) {
    return "forward";
  }
  
  // Side quadrants: Side falls
  if (relativeAngle < 0) {
    return "side_left";
  } else {
    return "side_right";
  }
}
 
/**
 * Determines fall direction from current trigram stance
 * 
 * Some stances have inherent instability in certain directions.
 * This function returns likely fall directions when balance is lost.
 * 
 * Korean stances and fall tendencies:
 * - 건 (Heaven): Forward bias - aggressive stance
 * - 태 (Lake): Backward bias - fluid retreating
 * - 리 (Fire): Forward bias - aggressive advance
 * - 진 (Thunder): Backward bias - explosive preparation
 * - 손 (Wind): Side bias - lateral movement
 * - 감 (Water): Backward bias - defensive flow
 * - 간 (Mountain): Backward bias - solid defense
 * - 곤 (Earth): Forward bias - grounding takedowns
 * 
 * @param stance - Current trigram stance
 * @param defaultFall - Default fall type if stance doesn't suggest direction
 * @returns Likely fall direction for the stance
 * 
 * @public
 * @korean 자세낙법방향
 */
export function determineFallFromStance(
  stance: TrigramStance,
  defaultFall: FallType = "backward"
): FallType {
  const stanceFallBias: Record<TrigramStance, FallType> = {
    [TrigramStance.GEON]: "forward",   // Heaven - aggressive forward
    [TrigramStance.TAE]: "backward",   // Lake - fluid retreat
    [TrigramStance.LI]: "forward",     // Fire - aggressive strike
    [TrigramStance.JIN]: "backward",   // Thunder - explosive back
    [TrigramStance.SON]: "side_left",  // Wind - lateral pressure
    [TrigramStance.GAM]: "backward",   // Water - flowing back
    [TrigramStance.GAN]: "backward",   // Mountain - defensive back
    [TrigramStance.GON]: "forward",    // Earth - forward throws
  };
  
  return stanceFallBias[stance] ?? defaultFall;
}
 
/**
 * Fall animation keyframe data structure
 * 
 * Defines key poses during fall animations for skeletal rendering.
 * Each keyframe specifies body positions at critical moments.
 * 
 * @korean 낙법키프레임
 */
export interface FallKeyframe {
  /** Frame number (0-indexed) */
  readonly frame: number;
  
  /** Torso rotation (radians) */
  readonly torsoRotation: { x: number; y: number; z: number };
  
  /** Center of mass vertical position (0-1, 1=standing, 0=ground) */
  readonly centerOfMassHeight: number;
  
  /** Description of this keyframe */
  readonly description: {
    readonly korean: string;
    readonly english: string;
  };
}
 
/**
 * Forward fall keyframes (전방낙법)
 * 
 * 24 frames (400ms) sequence:
 * - Frames 0-8: Forward stumble, losing balance
 * - Frames 9-15: Knee collapse, forward momentum
 * - Frames 16-20: Hands extend to brace fall
 * - Frames 21-23: Impact and settle face-down
 * 
 * @korean 전방낙법키프레임
 */
export const FALL_FORWARD_KEYFRAMES: readonly FallKeyframe[] = [
  {
    frame: 0,
    torsoRotation: { x: 0, y: 0, z: 0 },
    centerOfMassHeight: 0.9,
    description: {
      korean: "초기 자세",
      english: "Initial stance",
    },
  },
  {
    frame: 8,
    torsoRotation: { x: 0.3, y: 0, z: 0 }, // Leaning forward
    centerOfMassHeight: 0.75,
    description: {
      korean: "전방으로 비틀거림",
      english: "Forward stumble",
    },
  },
  {
    frame: 15,
    torsoRotation: { x: 0.7, y: 0, z: 0 }, // Falling forward
    centerOfMassHeight: 0.4,
    description: {
      korean: "무릎 붕괴",
      english: "Knee collapse",
    },
  },
  {
    frame: 18,
    torsoRotation: { x: 1.2, y: 0, z: 0 }, // Hands extending
    centerOfMassHeight: 0.15,
    description: {
      korean: "손으로 지면 충격 완화",
      english: "Hands brace impact",
    },
  },
  {
    frame: 23,
    torsoRotation: { x: 1.57, y: 0, z: 0 }, // Face down (90° forward)
    centerOfMassHeight: 0.05,
    description: {
      korean: "엎드려 정지",
      english: "Face-down prone",
    },
  },
] as const;
 
/**
 * Backward fall keyframes (후방낙법)
 * 
 * 30 frames (500ms) sequence:
 * - Frames 0-10: Backward stumble, balance loss
 * - Frames 11-18: Sitting motion begins
 * - Frames 19-25: Back impact preparation
 * - Frames 26-29: Full supine position
 * 
 * @korean 후방낙법키프레임
 */
export const FALL_BACKWARD_KEYFRAMES: readonly FallKeyframe[] = [
  {
    frame: 0,
    torsoRotation: { x: 0, y: 0, z: 0 },
    centerOfMassHeight: 0.9,
    description: {
      korean: "초기 자세",
      english: "Initial stance",
    },
  },
  {
    frame: 10,
    torsoRotation: { x: -0.2, y: 0, z: 0 }, // Leaning back
    centerOfMassHeight: 0.8,
    description: {
      korean: "후방으로 비틀거림",
      english: "Backward stumble",
    },
  },
  {
    frame: 18,
    torsoRotation: { x: -0.6, y: 0, z: 0 }, // Sitting motion
    centerOfMassHeight: 0.45,
    description: {
      korean: "앉는 동작",
      english: "Sitting motion",
    },
  },
  {
    frame: 22,
    torsoRotation: { x: -1.2, y: 0, z: 0 }, // Back impact
    centerOfMassHeight: 0.2,
    description: {
      korean: "등 충격",
      english: "Back impact",
    },
  },
  {
    frame: 29,
    torsoRotation: { x: -1.57, y: 0, z: 0 }, // Face up (90° back)
    centerOfMassHeight: 0.05,
    description: {
      korean: "누워 정지",
      english: "Supine position",
    },
  },
] as const;
 
/**
 * Side fall keyframes (측방낙법)
 * 
 * 27 frames (450ms) sequence:
 * - Frames 0-9: Side rotation begins
 * - Frames 10-16: Shoulder roll motion
 * - Frames 17-22: Hip impact
 * - Frames 23-26: Side sprawl position
 * 
 * @korean 측방낙법키프레임
 */
export const FALL_SIDE_KEYFRAMES: readonly FallKeyframe[] = [
  {
    frame: 0,
    torsoRotation: { x: 0, y: 0, z: 0 },
    centerOfMassHeight: 0.9,
    description: {
      korean: "초기 자세",
      english: "Initial stance",
    },
  },
  {
    frame: 9,
    torsoRotation: { x: 0, y: 0.3, z: 0.4 }, // Side rotation
    centerOfMassHeight: 0.7,
    description: {
      korean: "측면 회전 시작",
      english: "Side rotation begins",
    },
  },
  {
    frame: 16,
    torsoRotation: { x: 0.2, y: 0.8, z: 0.9 }, // Shoulder roll
    centerOfMassHeight: 0.4,
    description: {
      korean: "어깨 구르기",
      english: "Shoulder roll",
    },
  },
  {
    frame: 20,
    torsoRotation: { x: 0.3, y: 1.2, z: 1.3 }, // Hip impact
    centerOfMassHeight: 0.2,
    description: {
      korean: "엉덩이 충격",
      english: "Hip impact",
    },
  },
  {
    frame: 26,
    torsoRotation: { x: 0, y: 1.57, z: 1.57 }, // Side position (90° roll)
    centerOfMassHeight: 0.05,
    description: {
      korean: "측면 정지",
      english: "Side sprawl",
    },
  },
] as const;
 
/**
 * Get keyframes for a specific fall type
 * 
 * @param fallType - Type of fall animation
 * @returns Array of keyframes for that fall type
 * 
 * @public
 * @korean 낙법키프레임가져오기
 */
export function getFallKeyframes(fallType: FallType): readonly FallKeyframe[] {
  switch (fallType) {
    case "forward":
      return FALL_FORWARD_KEYFRAMES;
    case "backward":
      return FALL_BACKWARD_KEYFRAMES;
    case "side_left":
    case "side_right":
      return FALL_SIDE_KEYFRAMES;
  }
}
 
/**
 * Get impact frame number for fall type
 * 
 * @param fallType - Type of fall animation
 * @returns Frame number when ground impact occurs
 * 
 * @public
 * @korean 충격프레임가져오기
 */
export function getImpactFrame(fallType: FallType): number {
  return FALL_IMPACT_FRAMES[fallType];
}