All files / components/three Player3DWithTransitions.tsx

10% Statements 2/20
0% Branches 0/21
0% Functions 0/3
10% Lines 2/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 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                                                                                                      2x                                                                                           2x                                                                                                                                                                                                
/**
 * Enhanced Player3D component with stance transition animations
 *
 * Demonstrates integration of stance change visual effects:
 * - StanceAuraParticles for particle system
 * - StanceSymbol3D for floating trigram symbol
 * - StanceTransitionEffect for smooth transitions
 *
 * This wrapper can be used to enhance SkeletalPlayer3D with automatic
 * stance change detection and visual effects.
 *
 * @module components/three/Player3DWithTransitions
 * @category 3D Components
 * @korean 자세전환플레이어3D
 */
 
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useAudio } from "../../audio/AudioProvider";
import { TrigramStance } from "../../types/common";
import type { Player3DUnifiedProps } from "../../types/player-visual";
import { SkeletalPlayer3D } from "./SkeletalPlayer3D";
import StanceAuraParticles from "./StanceAuraParticles";
import StanceSymbol3D from "./StanceSymbol3D";
import StanceTransitionEffect from "./StanceTransitionEffect";
 
/**
 * Props for Player3DWithTransitions component
 */
export interface Player3DWithTransitionsProps extends Player3DUnifiedProps {
  /** Enable stance transition effects (default: true) */
  readonly enableTransitionEffects?: boolean;
  /** Enable particle effects (default: true) */
  readonly enableParticles?: boolean;
  /** Enable floating stance symbol (default: true) */
  readonly enableStanceSymbol?: boolean;
  /** Enable stance change audio (default: true) */
  readonly enableStanceAudio?: boolean;
  /** Transition duration in seconds (default: 0.5) */
  readonly transitionDuration?: number;
  /** Callback when stance transition starts */
  readonly onStanceTransitionStart?: (
    fromStance: TrigramStance,
    toStance: TrigramStance
  ) => void;
  /** Callback when stance transition completes */
  readonly onStanceTransitionComplete?: (stance: TrigramStance) => void;
}
 
/**
 * Audio asset IDs for stance transitions
 */
const AUDIO_ASSETS = {
  STANCE_CHANGE: "stance_change",
} as const;
 
/**
 * Player3DWithTransitions Component
 *
 * Enhanced player component with automatic stance change detection and visual effects.
 * Wraps SkeletalPlayer3D and adds:
 * - Particle system for stance aura
 * - Floating trigram symbol
 * - Smooth transition effects
 * - Audio synchronization
 *
 * Performance optimized:
 * - Effects can be individually disabled for mobile
 * - Uses stance change detection to minimize updates
 * - Reuses components efficiently
 *
 * @example
 * ```tsx
 * <Player3DWithTransitions
 *   playerId="player1"
 *   archetype={PlayerArchetype.MUSA}
 *   stance={currentStance}
 *   position={[0, 0, 0]}
 *   rotation={0}
 *   health={85}
 *   maxHealth={100}
 *   stamina={60}
 *   ki={40}
 *   pain={20}
 *   balance="READY"
 *   consciousness={100}
 *   bloodLoss={0}
 *   currentAnimation="idle"
 *   isMobile={false}
 *   enableTransitionEffects={true}
 *   enableParticles={true}
 *   enableStanceSymbol={true}
 *   onStanceTransitionComplete={(stance) => console.log('Transitioned to:', stance)}
 * />
 * ```
 */
export const Player3DWithTransitions: React.FC<
  Player3DWithTransitionsProps
> = ({
  stance,
  ki,
  isMobile = false,
  enableTransitionEffects = true,
  enableParticles = true,
  enableStanceSymbol = true,
  enableStanceAudio = true,
  transitionDuration = 0.5,
  onStanceTransitionStart,
  onStanceTransitionComplete,
  ...playerProps
}) => {
  const audio = useAudio();
  const prevStanceRef = useRef<TrigramStance>(stance);
  const [isTransitioning, setIsTransitioning] = useState(false);
  const [fromStance, setFromStance] = useState<TrigramStance>(stance);
 
  // Detect stance changes - external effect (audio) justifies useEffect
   
  useEffect(() => {
    const previousStance = prevStanceRef.current;
 
    // Only trigger if stance actually changed
    if (previousStance !== stance) {
      prevStanceRef.current = stance;
 
      // External effects: audio playback (external system) and callbacks
      // These setState calls are intentional - triggered by prop change, not creating infinite loops
      setIsTransitioning(true);
      setFromStance(previousStance);
      onStanceTransitionStart?.(previousStance, stance);
 
      // External system: audio playback
      if (enableStanceAudio) {
        audio.playSFX(AUDIO_ASSETS.STANCE_CHANGE);
      }
    }
  }, [stance, audio, enableStanceAudio, onStanceTransitionStart]);
 
  // Handle transition completion
  const handleTransitionComplete = useCallback(() => {
    setIsTransitioning(false);
    onStanceTransitionComplete?.(stance);
  }, [stance, onStanceTransitionComplete]);
 
  // Calculate particle intensity based on Ki
  const particleIntensity = ki / 100;
 
  return (
    <group data-testid="player3d-with-transitions">
      {/* Base player model */}
      <SkeletalPlayer3D
        stance={stance}
        ki={ki}
        isMobile={isMobile}
        {...playerProps}
      />
 
      {/* Particle effects (stance-specific aura) */}
      {enableParticles && (
        <StanceAuraParticles
          stance={stance}
          intensity={particleIntensity}
          count={isMobile ? 100 : 200} // Reduce particle count on mobile
          animated={true}
          spread={2.0}
        />
      )}
 
      {/* Floating stance symbol */}
      {enableStanceSymbol && (
        <StanceSymbol3D
          stance={stance}
          heightOffset={2.5}
          animated={true}
          scale={isMobile ? 0.8 : 1.0} // Smaller on mobile
          showName={!isMobile} // Hide Korean name on mobile for clarity
        />
      )}
 
      {/* Stance transition effect */}
      {enableTransitionEffects && isTransitioning && (
        <StanceTransitionEffect
          fromStance={fromStance}
          toStance={stance}
          onTransitionComplete={handleTransitionComplete}
          duration={transitionDuration}
          showNameOverlay={!isMobile} // Hide overlay on mobile
        />
      )}
    </group>
  );
};
 
export default Player3DWithTransitions;