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 | 7x 7x 142x 142x 142x 142x 142x 142x 142x 142x 142x | /**
* Enhanced Player3D component with stance transition animations
*
* Demonstrates integration of stance change visual effects:
* - 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 StanceSymbol3D from "../effects/StanceSymbol3D";
import StanceTransitionEffect from "../effects/StanceTransitionEffect";
import { SkeletalPlayer3D } from "./SkeletalPlayer3D";
/**
* Props for Player3DWithTransitions component
*/
export interface Player3DWithTransitionsProps extends Player3DUnifiedProps {
/** Specific attack animation name (for attack state) */
readonly attackAnimation?: string;
/** Enable stance transition effects (default: true) */
readonly enableTransitionEffects?: 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:
* - 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}
* enableStanceSymbol={true}
* onStanceTransitionComplete={(stance) => console.log('Transitioned to:', stance)}
* />
* ```
*/
export const Player3DWithTransitions: React.FC<
Player3DWithTransitionsProps
> = ({
stance,
ki,
isMobile = false,
attackAnimation,
enableTransitionEffects = 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
Iif (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]);
return (
<group data-testid="player3d-with-transitions">
{/* Base player model */}
<SkeletalPlayer3D
stance={stance}
ki={ki}
isMobile={isMobile}
attackAnimation={attackAnimation}
{...playerProps}
/>
{/* 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;
|