All files / hooks useMuscleActivation.ts

95% Statements 19/20
100% Branches 15/15
100% Functions 4/4
95% Lines 19/20

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                                                                                                                                                              159x     159x 159x         159x 80x 80x 80x               159x         90x 50x 40x 10x 30x         10x   20x           90x   8x 8x       159x          
/**
 * useMuscleActivation - Shared hook for muscle activation management
 *
 * Manages muscle activation state based on current actions and stamina.
 * Reduces code duplication in skeletal animation components.
 *
 * @module hooks/useMuscleActivation
 * @category Hooks
 * @korean 근육활성화훅
 */
 
import { useEffect, useRef, useState } from "react";
import { MuscleActivationManager } from "../systems/animation/MuscleActivation";
import type { PlayerAnimation } from "../types/player-visual";
 
/**
 * Options for useMuscleActivation hook
 * @korean 근육활성화훅옵션
 */
export interface UseMuscleActivationOptions {
  /** Current animation name */
  readonly currentAnimation: PlayerAnimation;
  /** Specific attack animation name (for attack state) */
  readonly attackAnimation?: string;
  /** Whether player is blocking */
  readonly isBlocking?: boolean;
  /** Current stamina level (0-100) */
  readonly stamina: number;
}
 
/**
 * Return type for useMuscleActivation hook
 * @korean 근육활성화훅반환타입
 */
export interface UseMuscleActivationReturn {
  /** Current muscle activation states (bone name -> activation 0-1) */
  readonly muscleStates: Map<string, number>;
  /** Update muscle activations (call in useFrame) */
  readonly updateMuscleActivations: (delta: number, frameCounter: number) => void;
}
 
/**
 * useMuscleActivation hook
 *
 * Manages muscle activation based on current actions (attack, defend, movement).
 * Updates at 60fps with periodic state syncs to reduce re-renders.
 *
 * @param options - Muscle activation options
 * @returns Muscle states and update function
 *
 * @example
 * ```tsx
 * const { muscleStates, updateMuscleActivations } = useMuscleActivation({
 *   currentAnimation: "attack",
 *   attackAnimation: "jab",
 *   isBlocking: false,
 *   stamina: 85,
 * });
 *
 * // In useFrame callback
 * let frameCounter = 0;
 * useFrame((_, delta) => {
 *   frameCounter = (frameCounter + 1) % 10;
 *   updateMuscleActivations(delta, frameCounter);
 * });
 *
 * // Use muscle states in rendering
 * <BoneRenderer
 *   rig={rig}
 *   muscleStates={muscleStates}
 *   isExhausted={stamina < 20}
 * />
 * ```
 *
 * @korean 근육활성화훅
 */
export function useMuscleActivation(
  options: UseMuscleActivationOptions
): UseMuscleActivationReturn {
  const { currentAnimation, attackAnimation, isBlocking = false, stamina } = options;
 
  // Muscle activation manager
  const muscleManager = useRef(new MuscleActivationManager());
  const [muscleStates, setMuscleStates] = useState<Map<string, number>>(
    new Map()
  );
 
  // Cleanup muscle manager on unmount
  useEffect(() => {
    return () => {
      try {
        muscleManager.current.reset();
      } catch (error) {
        console.warn("MuscleActivationManager reset failed:", error);
      }
    };
  }, []);
 
  // Update muscle activations (called at 60fps in useFrame)
  const updateMuscleActivations = (
    delta: number,
    frameCounter: number
  ): void => {
    // Update muscle system based on current action
    if (currentAnimation === "attack" && attackAnimation) {
      muscleManager.current.update(attackAnimation, stamina, delta);
    } else if (currentAnimation === "defend" || isBlocking) {
      muscleManager.current.update("block", stamina, delta);
    } else if (
      currentAnimation === "walk" ||
      currentAnimation === "stance_change"
    ) {
      // Engage stance/leg/core muscles during movement and stance changes
      muscleManager.current.update("stance_change", stamina, delta);
    } else {
      muscleManager.current.relaxAllMuscles(delta);
    }
 
    // Sync muscle states to React state deterministically
    // (every 10 frames at 60fps = ~6 times/sec)
    // Balances animation smoothness with performance and reduces GC pressure
    if (frameCounter === 0) {
      // Reuse scratch map from manager to avoid repeated allocations
      const scratchMap = muscleManager.current.getScratchMapForSync();
      setMuscleStates(scratchMap);
    }
  };
 
  return {
    muscleStates,
    updateMuscleActivations,
  };
}