blacktrigram 0.7.81 → 0.7.82

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.
Files changed (38) hide show
  1. package/lib/components/screens/combat/CombatScreen3D.js +16 -11
  2. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  3. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js +2 -1
  4. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  5. package/lib/components/screens/combat/hooks/useAICombat.js +2 -1
  6. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  7. package/lib/components/screens/combat/hooks/useCombatActions.js +25 -9
  8. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  9. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js +10 -6
  10. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  11. package/lib/components/screens/intro/IntroScreen3D.js +8 -4
  12. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  13. package/lib/components/screens/training/TrainingScreen3D.js +5 -3
  14. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  15. package/lib/components/screens/training/hooks/useAttackMovement.js +2 -1
  16. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  17. package/lib/components/screens/training/hooks/useTrainingActions.js +2 -1
  18. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  19. package/lib/components/shared/three/effects/ActionFeedback.js +2 -1
  20. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  21. package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
  22. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  23. package/lib/components/shared/three/effects/HitEffects3D.js +2 -1
  24. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  25. package/lib/components/shared/ui/SplashScreen.js +2 -2
  26. package/lib/hooks/useMuscleActivation.js +4 -1
  27. package/lib/hooks/useMuscleActivation.js.map +1 -1
  28. package/lib/hooks/useRoundTransition.js +2 -1
  29. package/lib/hooks/useRoundTransition.js.map +1 -1
  30. package/lib/hooks/useSkeletalAnimation.js +1 -1
  31. package/lib/index.js +2 -2
  32. package/lib/systems/VitalPointSystem.js +2 -1
  33. package/lib/systems/VitalPointSystem.js.map +1 -1
  34. package/lib/systems/animation/core/AnimationStateMachine.js +1 -1
  35. package/lib/systems/index.js +2 -2
  36. package/lib/utils/inputSystem.js +2 -1
  37. package/lib/utils/inputSystem.js.map +1 -1
  38. package/package.json +6 -6
@@ -12,6 +12,7 @@ import { resolveTechniqueAnimation } from "../../../systems/animation/core/Anima
12
12
  import { usePlayerAnimation } from "../../../hooks/usePlayerAnimation.js";
13
13
  import useTechniqueSelection from "../../../hooks/useTechniqueSelection.js";
14
14
  import { useWebGLContextLossHandler } from "../../../hooks/useWebGLContextLossHandler.js";
15
+ import { MovementType, SpeedModifierSystem } from "../../../systems/physics/SpeedModifierSystem.js";
15
16
  import { physicalReachCalculator } from "../../../systems/physics/PhysicalReachCalculator.js";
16
17
  import { getArchetypePhysicalAttributes } from "../../../data/archetypePhysicalAttributes.js";
17
18
  import { getMobileControlsBottom } from "../../../types/constants/layout.js";
@@ -22,7 +23,6 @@ import { useKoreanTheme } from "../../shared/base/useKoreanTheme.js";
22
23
  import { VitalPointMarkers3D } from "../../shared/three/effects/VitalPointMarkers3D.js";
23
24
  import { StanceChangeIndicator } from "../../shared/three/indicators/StanceChangeIndicator.js";
24
25
  import { CombatArena3D } from "../../shared/three/scene/CombatArena3D.js";
25
- import { MovementType, SpeedModifierSystem } from "../../../systems/physics/SpeedModifierSystem.js";
26
26
  import { animationStateToPlayerAnimation, convertPlayerStateToProps } from "../../../utils/player3DHelpers.js";
27
27
  import { MobileControlsOverlay } from "../../shared/mobile/MobileControlsPure.js";
28
28
  import { StanceWheelPure } from "../../shared/mobile/StanceWheelPure.js";
@@ -349,7 +349,8 @@ var TrainingScreen3D = ({ onPlayerUpdate, onReturnToMenu, width = 1200, height =
349
349
  enabled: trainingState.isTraining,
350
350
  onTechniqueExecute: useCallback((technique) => {
351
351
  trainingActions.setFeedback(`${technique.name.korean} 사용! | Used ${technique.name.english}!`);
352
- setAttackAnimation(resolveTechniqueAnimation(technique));
352
+ const animationName = resolveTechniqueAnimation(technique);
353
+ setAttackAnimation(animationName);
353
354
  handleAttackRef.current?.();
354
355
  }, [trainingActions])
355
356
  });
@@ -535,7 +536,8 @@ var TrainingScreen3D = ({ onPlayerUpdate, onReturnToMenu, width = 1200, height =
535
536
  return;
536
537
  }
537
538
  if (key >= "1" && key <= "8") {
538
- handleStanceChangeWithVisualFeedback(parseInt(key) - 1);
539
+ const stanceIndex = parseInt(key) - 1;
540
+ handleStanceChangeWithVisualFeedback(stanceIndex);
539
541
  event.preventDefault();
540
542
  return;
541
543
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TrainingScreen3D.js","names":[],"sources":["../../../../src/components/screens/training/TrainingScreen3D.tsx"],"sourcesContent":["/**\n * TrainingScreen3D - Three.js-based training screen\n *\n * Refactored to use consolidated hooks matching CombatScreen architecture.\n * Provides 3D training dummy with vital point targeting and UI overlays.\n *\n * UI Rendering: All HUD elements are rendered in an absolute-positioned div\n * OUTSIDE the Canvas, matching CombatScreen's reliable rendering pattern.\n * This eliminates the need for Html overlays inside Three.js and ensures\n * HUDs appear immediately without waiting for Canvas initialization.\n *\n * Architecture (Consolidated in PR #1394 + Issue #1398):\n * - TrainingLeftHUD: Anatomy controls, guard indicator\n * - TrainingRightHUD: Training stats, mode selector, vital point selection\n * - TrainingTopHUD: Training controls, archetype selector, return button\n * - TrainingBottomHUD: Technique bar, feedback messages, mobile controls\n * - VitalPointOverlayControlsPure: Vital point overlay controls (pure DOM)\n *\n * All UI components render as pure DOM in the HUD overlay div (lines 1230+).\n * NO Html components from @react-three/drei are used inside the Canvas.\n * This ensures clean separation of 3D rendering and UI layers.\n *\n * @korean 훈련화면3D - 훈련 상태 훅을 사용한 리팩토링된 3D 훈련 화면\n */\n\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { AccelerationUpdater } from \"../../../systems/movement/helpers/AccelerationUpdater\";\nimport {\n isRunningSpeed,\n STEP_DISTANCE_THRESHOLDS,\n} from \"../../../systems/movement/helpers/accelerationUtils\";\nimport {\n Bloom,\n EffectComposer,\n Noise,\n Vignette,\n} from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { useCombatAudio } from \"../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../data/archetypePhysicalAttributes\";\nimport { usePlayerAnimation } from \"../../../hooks/usePlayerAnimation\";\nimport { useTechniqueSelection } from \"../../../hooks/useTechniqueSelection\";\nimport { GestureEvent } from \"../../../hooks/useTouchControls\";\nimport { useWebGLContextLossHandler } from \"../../../hooks/useWebGLContextLossHandler\";\nimport { PlayerState } from \"../../../systems\";\nimport {\n AnimationEvents,\n AnimationState,\n AnimationType,\n resolveTechniqueAnimation,\n} from \"../../../systems/animation\";\nimport { getAnimationForTechniqueOrDefault } from \"../../../systems/animation/core/TechniqueAnimationMapping\";\nimport { physicalReachCalculator } from \"../../../systems/physics\";\nimport {\n MovementType,\n SpeedModifierSystem,\n} from \"../../../systems/physics/SpeedModifierSystem\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport {\n CombatState,\n PlayerArchetype,\n Position,\n Technique,\n TrigramStance,\n} from \"../../../types\";\nimport { getPerformanceSettings } from \"../../../types/constants\";\nimport { getMobileControlsBottom } from \"../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { DEFAULT_BODY_RADIUS_METERS } from \"../../../types/physicsConstants\";\nimport { usePlayerMovement } from \"../../../utils/inputSystem\";\nimport { calculateDistance3D } from \"../../../utils/math\";\nimport { getHUDPositionScale } from \"../../../utils/responsiveLayoutHelpers\";\nimport { createCameraConfig } from \"../../../utils/sharedPhysicsConfig\";\nimport {\n animationStateToPlayerAnimation,\n convertPlayerStateToProps,\n} from \"../../../utils/player3DHelpers\";\nimport { useKoreanTheme } from \"../../shared/base/useKoreanTheme\";\nimport {\n GestureRecognizerPure,\n StanceWheelPure,\n} from \"../../shared/mobile\";\nimport {\n MobileControlsOverlay,\n type ButtonEventType,\n type Direction,\n type DPadEventType,\n} from \"../../shared/mobile/MobileControlsPure\";\nimport {\n Player3DWithTransitions,\n VitalPointMarkers3D,\n type BodyRegionFilter,\n} from \"../../shared/three\";\nimport { StanceChangeIndicator } from \"../../shared/three/indicators/StanceChangeIndicator\";\nimport { CombatArena3D } from \"../../shared/three/scene/CombatArena3D\";\nimport { VitalPointOverlayControlsPure } from \"../../shared/ui/VitalPointOverlayControlsPure\";\nimport AnatomyOverlay3D, {\n type AnatomyLayer,\n} from \"./components/AnatomyOverlay3D\";\nimport FootPlacementMarkers3D from \"./components/FootPlacementMarkers3D\";\nimport HitFeedbackEffect3D from \"./components/HitFeedbackEffect3D\";\nimport type { DifficultyMode } from \"./components/TrainingDummy3D\";\nimport TrainingDummy3D from \"./components/TrainingDummy3D\";\nimport {\n TrainingBottomHUD,\n TrainingLeftHUD,\n TrainingRightHUD,\n TrainingTopHUD,\n} from \"./components/hud\";\nimport { useAttackMovement } from \"./hooks/useAttackMovement\";\nimport useTrainingActions from \"./hooks/useTrainingActions\";\nimport { useTrainingLayout } from \"./hooks/useTrainingLayout\";\nimport useTrainingState from \"./hooks/useTrainingState\";\n\n/**\n * AnimationUpdater - Component that updates player animation at 60fps\n *\n * @korean 훈련애니메이션업데이터 - 60fps로 플레이어 애니메이션을 업데이트하는 컴포넌트\n */\ninterface TrainingAnimationUpdaterProps {\n readonly playerAnimation: ReturnType<typeof usePlayerAnimation>;\n}\n\nconst TrainingAnimationUpdater: React.FC<TrainingAnimationUpdaterProps> = ({\n playerAnimation,\n}) => {\n useFrame((_state, delta) => {\n playerAnimation.update(delta);\n });\n\n return null;\n};\n\n/**\n * Props for the TrainingScreen3D component\n */\nexport interface TrainingScreen3DProps {\n /** Callback to update player state */\n readonly onPlayerUpdate: (updates: Partial<PlayerState>) => void;\n /** Callback when returning to menu */\n readonly onReturnToMenu: () => void;\n /** Canvas width in pixels. Defaults to 1200 */\n readonly width?: number;\n /** Canvas height in pixels. Defaults to 800 */\n readonly height?: number;\n /** Initial archetype from IntroScreen selection. Defaults to MUSA */\n readonly initialArchetype?: PlayerArchetype;\n}\n\n/**\n * TrainingScreen3D Component\n * Three.js-based training screen with 3D dummy and Html UI\n *\n * Uses consolidated hooks for state management matching CombatScreen architecture.\n */\nexport const TrainingScreen3D: React.FC<TrainingScreen3DProps> = ({\n onPlayerUpdate,\n onReturnToMenu,\n width = 1200,\n height = 800,\n initialArchetype = PlayerArchetype.MUSA,\n}) => {\n\n\n const { state: trainingState, actions: trainingActions } = useTrainingState();\n\n const audio = useAudio();\n \n const { playBoneImpactSound, playAttackSound, playStanceChangeSound } =\n useCombatAudio();\n\n const { trainingAreaBounds, isMobile, isPortrait, screenSize } =\n useTrainingLayout(width, height);\n\n const theme = useKoreanTheme({\n variant: \"primary\",\n size: \"md\",\n isMobile,\n });\n\n const positionScale = React.useMemo(\n () => getHUDPositionScale(screenSize, isMobile),\n [screenSize, isMobile],\n );\n\n const difficulty: DifficultyMode = \"normal\";\n const vitalPointCount = 70; // Show all 70 vital points\n\n const [selectedArchetype, setSelectedArchetype] =\n React.useState<PlayerArchetype>(initialArchetype);\n\n const [overlayVisible, setOverlayVisible] = React.useState(false);\n const [severityFilters, setSeverityFilters] = React.useState<\n import(\"../../../types/common\").VitalPointSeverity[]\n >([]);\n const [regionFilter, setRegionFilter] =\n React.useState<BodyRegionFilter>(\"all\");\n const [searchQuery, setSearchQuery] = React.useState(\"\");\n const [showLabels, setShowLabels] = React.useState(true);\n const [animated, setAnimated] = React.useState(true);\n const [scale, setScale] = React.useState(1.2);\n\n\n const [attackAnimation, setAttackAnimation] = React.useState<\n string | undefined\n >(undefined);\n\n React.useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"v\" || e.key === \"V\") {\n setOverlayVisible((prev) => !prev);\n audio.playSFX(\"menu_select\");\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, [audio]);\n\n\n const contextLossCountRef = useRef(0);\n\n useWebGLContextLossHandler({\n onContextLost: () => {\n console.warn(\"⚠️ WebGL context lost in TrainingScreen\");\n contextLossCountRef.current += 1;\n },\n onContextRestored: () => {\n console.log(\"✓ WebGL context restored in TrainingScreen\");\n },\n autoRestore: true,\n });\n\n\n const speedModifierSystem = useMemo(() => new SpeedModifierSystem(), []);\n\n const [speedModifiers, setSpeedModifiers] = useState({\n finalSpeed: 6.0, // BASE_WALK_SPEED (6.0 m/s for responsive combat)\n baseSpeed: 6.0,\n finalAcceleration: 12.0, // BASE_ACCELERATION (12.0 m/s² for quick response)\n });\n\n const [walkRunSpeeds, setWalkRunSpeeds] = useState({\n walkSpeed: 6.0,\n runSpeed: 10.0,\n });\n\n\n const initialPositionMeters = useMemo<Position>(\n () => ({\n x: trainingAreaBounds.worldWidthMeters * 0.0, // Centered laterally\n y: 0, // Centered vertically\n }),\n [trainingAreaBounds],\n );\n\n const handlePositionChange = useCallback(\n (newPosition: Position) => {\n onPlayerUpdate({ position: newPosition });\n },\n [onPlayerUpdate],\n );\n\n const movementBounds = useMemo(\n () => ({\n worldWidthMeters: trainingAreaBounds.worldWidthMeters,\n worldDepthMeters: trainingAreaBounds.worldDepthMeters,\n }),\n [trainingAreaBounds.worldWidthMeters, trainingAreaBounds.worldDepthMeters],\n );\n\n \n const movementTimeRef = useRef(0);\n const lastDirectionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n \n const [accelerationBasedSpeed, setAccelerationBasedSpeed] = useState(\n walkRunSpeeds.walkSpeed\n );\n \n const isRunning = isRunningSpeed(accelerationBasedSpeed, walkRunSpeeds.runSpeed);\n\n const { playerPosition, isMoving, velocity } = usePlayerMovement({\n enabled: true, // Always allow movement in training screen\n bounds: movementBounds, // Use memoized bounds object\n onPositionChange: handlePositionChange, // Use memoized callback\n initialPositionMeters,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n legInjuryFactor: 0, // No injury in training mode\n isRunning, // Use computed acceleration-based running state\n maxSpeedOverride: accelerationBasedSpeed,\n accelerationOverride: speedModifiers.finalAcceleration,\n });\n\n const player3DPosition = useMemo<[number, number, number]>(() => {\n return [playerPosition.x, 0, playerPosition.y];\n }, [playerPosition]);\n\n const dummyPosition = useMemo<[number, number, number]>(\n () => [trainingAreaBounds.worldWidthMeters * 0.15, 0, 0],\n [trainingAreaBounds.worldWidthMeters],\n );\n\n const centerToCenterDistance = useMemo(\n () => calculateDistance3D(player3DPosition, dummyPosition),\n [player3DPosition, dummyPosition],\n );\n\n const distanceToDummy = useMemo(\n () => Math.max(0, centerToCenterDistance - DEFAULT_BODY_RADIUS_METERS),\n [centerToCenterDistance],\n );\n\n const lastFacingRotationRef = useRef<number>(0);\n\n const playerRotation = useMemo(() => {\n if (isMoving && velocity && (velocity.x !== 0 || velocity.y !== 0)) {\n return Math.atan2(velocity.x, velocity.y);\n } else {\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return Math.atan2(dx, dz);\n }\n }, [isMoving, velocity, player3DPosition, dummyPosition]);\n\n useEffect(() => {\n lastFacingRotationRef.current = playerRotation;\n }, [playerRotation]);\n\n\n const [currentLaterality, setCurrentLaterality] = useState<\"left\" | \"right\">(\"right\");\n \n const stepCounterRef = useRef(0);\n const lastPositionRef = useRef<Position>(playerPosition);\n \n useEffect(() => {\n if (!isMoving) {\n stepCounterRef.current = 0;\n lastPositionRef.current = playerPosition;\n return;\n }\n\n const dx = playerPosition.x - lastPositionRef.current.x;\n const dy = playerPosition.y - lastPositionRef.current.y;\n const distanceMoved = Math.sqrt(dx * dx + dy * dy);\n \n const stepThreshold = isRunning \n ? STEP_DISTANCE_THRESHOLDS.RUN \n : STEP_DISTANCE_THRESHOLDS.WALK;\n stepCounterRef.current += distanceMoved;\n \n const stepsCrossed = Math.floor(stepCounterRef.current / stepThreshold);\n if (stepsCrossed > 0) {\n if (stepsCrossed % 2 === 1) {\n setCurrentLaterality(prev => prev === \"right\" ? \"left\" : \"right\");\n }\n stepCounterRef.current -= stepsCrossed * stepThreshold;\n }\n \n lastPositionRef.current = playerPosition;\n }, [playerPosition, isMoving, isRunning]);\n\n\n const pendingAttackRef = useRef<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>(null);\n\n const handleDummyHitRef = useRef<\n (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean\n >(() => false);\n\n const playerAnimationRef = useRef<ReturnType<\n typeof usePlayerAnimation\n > | null>(null);\n\n const playerAnimationEvents = useMemo<AnimationEvents>(\n () => ({\n onFrame: (frame, state) => {\n if (state === \"attack\" && frame === 6 && pendingAttackRef.current) {\n const attackData = pendingAttackRef.current;\n handleDummyHitRef.current(attackData.vitalPoint, {\n animationType: attackData.animationType,\n techniqueId: attackData.techniqueId,\n });\n pendingAttackRef.current = null;\n }\n },\n onAnimationComplete: (state) => {\n if (state === \"stance_change\") {\n playStanceChangeSound();\n const currentStance =\n TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex];\n if (currentStance && playerAnimationRef.current) {\n playerAnimationRef.current.transitionToStanceGuard(currentStance);\n }\n }\n },\n }),\n [playStanceChangeSound, trainingState.currentStanceIndex],\n );\n\n const playerAnimation = usePlayerAnimation({\n events: playerAnimationEvents,\n });\n\n useEffect(() => {\n playerAnimationRef.current = playerAnimation;\n }, [playerAnimation]);\n\n\n const currentStance = useMemo(\n () => TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n [trainingState.currentStanceIndex],\n );\n\n const [previousStanceIndex, setPreviousStanceIndex] = useState<number>(0);\n\n const currentTechniqueAnimationTypeRef = useRef<AnimationType>(\n AnimationType.JAB,\n );\n\n\n const trainingPlayerState = useMemo<PlayerState>(() => {\n return {\n id: \"training-player\",\n name: { korean: \"훈련생\", english: \"Trainee\" },\n archetype: selectedArchetype,\n health: 100,\n maxHealth: 100,\n ki: 100,\n maxKi: 100,\n stamina: 100,\n maxStamina: 100,\n energy: 100,\n maxEnergy: 100,\n attackPower: 10,\n defense: 10,\n speed: 10,\n technique: 10,\n pain: 0,\n consciousness: 100,\n balance: 100,\n momentum: 0,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n combatState: CombatState.IDLE,\n position: playerPosition,\n isBlocking: false,\n isStunned: false,\n isCountering: false,\n lastActionTime: 0,\n recoveryTime: 0,\n lastStanceChangeTime: 0,\n statusEffects: [],\n activeEffects: [],\n vitalPoints: [],\n totalDamageReceived: 0,\n totalDamageDealt: 0,\n hitsTaken: 0,\n hitsLanded: trainingState.stats.hits,\n perfectStrikes: trainingState.perfectStrikes,\n vitalPointHits: 0,\n misses: trainingState.stats.misses,\n accuracy: trainingState.stats.accuracy,\n comboCount: trainingState.stats.combo,\n };\n }, [playerPosition, trainingState, selectedArchetype]);\n\n useEffect(() => {\n const updateSpeedModifiers = () => {\n const walkModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.WALKING,\n false, // isCrouching\n );\n\n const runModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.RUNNING,\n false, // isCrouching\n );\n\n setSpeedModifiers({\n finalSpeed: walkModifiers.finalSpeed,\n baseSpeed: walkModifiers.baseSpeed,\n finalAcceleration: walkModifiers.finalAcceleration,\n });\n\n setWalkRunSpeeds({\n walkSpeed: walkModifiers.finalSpeed,\n runSpeed: runModifiers.finalSpeed,\n });\n };\n\n updateSpeedModifiers();\n\n const intervalId = setInterval(updateSpeedModifiers, 200);\n\n return () => clearInterval(intervalId);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [trainingPlayerState]); // speedModifierSystem is memoized and never changes\n\n\n const isPlayerAttacking = useMemo(\n () => playerAnimation?.currentState === \"attack\",\n [playerAnimation],\n );\n\n const attackDirection = useMemo(() => {\n if (!isPlayerAttacking) {\n return new THREE.Vector3(0, 0, 1); // Default forward direction\n }\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return new THREE.Vector3(dx, 0, dz).normalize();\n }, [dummyPosition, player3DPosition, isPlayerAttacking]);\n\n const {\n currentPosition: player3DPositionWithAttackMovement,\n } = useAttackMovement({\n isAttacking: isPlayerAttacking,\n // eslint-disable-next-line react-hooks/refs -- ref value is set synchronously before isAttacking becomes true; hook only reads this at attack start\n animationType: currentTechniqueAnimationTypeRef.current,\n currentStance: trainingPlayerState.currentStance,\n basePosition: player3DPosition,\n attackDirection,\n animationDuration: 0.4,\n });\n\n const finalPlayer3DPosition = isPlayerAttacking\n ? player3DPositionWithAttackMovement\n : player3DPosition;\n\n\n const handleAttackRef = useRef<(() => void) | null>(null);\n\n const techniqueSelection = useTechniqueSelection({\n player: trainingPlayerState,\n enabled: trainingState.isTraining,\n onTechniqueExecute: useCallback(\n (technique: Technique) => {\n trainingActions.setFeedback(\n `${technique.name.korean} 사용! | Used ${technique.name.english}!`,\n );\n\n const animationName = resolveTechniqueAnimation(technique);\n setAttackAnimation(animationName);\n\n\n handleAttackRef.current?.();\n },\n [trainingActions],\n ),\n });\n\n const selectedTechniqueId = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0 || selectedIdx < 0 || selectedIdx >= techniques.length) {\n return undefined;\n }\n return techniques[selectedIdx]?.id;\n }, [techniqueSelection.availableTechniques, techniqueSelection.selectedIndex]);\n\n const {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n } = useTrainingActions({\n state: trainingState,\n actions: trainingActions,\n playerPosition,\n player3DPosition,\n dummyPosition,\n playerArchetype: selectedArchetype,\n playerStance: currentStance,\n currentTechniqueAnimationTypeRef, // Ref for technique's animation type\n audio,\n playBoneImpactSound, // Pass bone impact audio function from useCombatAudio\n playAttackSound, // Pass attack sound function from useCombatAudio\n selectedTechniqueId, // Pass selected technique ID for intensity-based attack sounds\n onPlayerUpdate: (updates) => {\n onPlayerUpdate(updates);\n },\n playerAnimation: {\n transitionTo: playerAnimation.transitionTo,\n transitionToAttack: playerAnimation.transitionToAttack,\n transitionToStanceGuard: playerAnimation.transitionToStanceGuard,\n currentState: playerAnimation.currentState,\n },\n pendingAttackRef, // Share the ref with animation events\n });\n\n useEffect(() => {\n handleAttackRef.current = handleAttack;\n }, [handleAttack]);\n\n useEffect(() => {\n handleDummyHitRef.current = handleDummyHit;\n }, [handleDummyHit]);\n\n const handleStanceChangeWithVisualFeedback = useCallback(\n (stanceIndex: number) => {\n setPreviousStanceIndex(trainingState.currentStanceIndex);\n handleStanceChange(stanceIndex);\n },\n [handleStanceChange, trainingState.currentStanceIndex],\n );\n\n\n const prevIsMovingRef = useRef<boolean>(isMoving);\n const prevIsRunningRef = useRef<boolean>(isRunning);\n const prevStanceRef = useRef<TrigramStance>(currentStance);\n \n useEffect(() => {\n const isMovingChanged = prevIsMovingRef.current !== isMoving;\n const isRunningChanged = prevIsRunningRef.current !== isRunning;\n const stanceChanged = prevStanceRef.current !== currentStance;\n \n if (isMovingChanged || isRunningChanged) {\n if (isMoving) {\n if (isRunning) {\n playerAnimation.transitionTo(AnimationState.RUN);\n } else {\n playerAnimation.transitionTo(AnimationState.WALK);\n }\n } else if (playerAnimation.currentState === AnimationState.WALK || \n playerAnimation.currentState === AnimationState.RUN) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevIsMovingRef.current = isMoving;\n prevIsRunningRef.current = isRunning;\n }\n \n if (stanceChanged && !isMoving) {\n if (playerAnimation.currentState === AnimationState.IDLE || \n playerAnimation.isInStanceGuard()) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevStanceRef.current = currentStance;\n }\n }, [isMoving, isRunning, currentStance, playerAnimation]);\n\n\n const cooldownsMap = useMemo(() => {\n const map = new Map<string, number>();\n techniqueSelection.activeCooldowns.forEach((cd) => {\n map.set(cd.techniqueId, cd.remaining);\n });\n return map;\n }, [techniqueSelection.activeCooldowns]);\n\n const { currentTechniqueReach, currentAnimationType } = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const currentTechnique =\n techniques[Math.min(selectedIdx, techniques.length - 1)];\n if (!currentTechnique) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const animConfig = getAnimationForTechniqueOrDefault(currentTechnique.id);\n const physicalAttributes =\n getArchetypePhysicalAttributes(selectedArchetype);\n const reach = physicalReachCalculator.calculateMaxReach(\n physicalAttributes,\n animConfig.type,\n currentStance,\n );\n return {\n currentTechniqueReach: reach,\n currentAnimationType: animConfig.type,\n };\n }, [\n techniqueSelection.availableTechniques,\n techniqueSelection.selectedIndex,\n selectedArchetype,\n currentStance,\n ]);\n\n useEffect(() => {\n currentTechniqueAnimationTypeRef.current = currentAnimationType;\n }, [currentAnimationType]);\n\n\n const activeMobileKeyRef = useRef<string | null>(null);\n\n const mobileControlsEnabled = isMobile;\n\n const handleMobileMove = useCallback(\n (direction: Direction | null, eventType: DPadEventType) => {\n const directionMap: Record<Direction, string> = {\n up: \"w\",\n \"up-right\": \"w\",\n right: \"d\",\n \"down-right\": \"s\",\n down: \"s\",\n \"down-left\": \"s\",\n left: \"a\",\n \"up-left\": \"w\",\n };\n\n if (eventType === \"start\" && direction) {\n if (\n activeMobileKeyRef.current &&\n activeMobileKeyRef.current !== directionMap[direction]\n ) {\n const prevKey = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key: prevKey,\n code: `Key${prevKey.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n }\n\n const key = directionMap[direction];\n activeMobileKeyRef.current = key;\n window.dispatchEvent(\n new KeyboardEvent(\"keydown\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n } else if (eventType === \"end\") {\n if (activeMobileKeyRef.current) {\n const key = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n activeMobileKeyRef.current = null;\n }\n }\n },\n [],\n );\n\n const handleMobileAttack = useCallback(() => {\n handleAttack();\n }, [handleAttack]);\n\n const handleMobileBlock = useCallback(\n (eventType: ButtonEventType) => {\n if (eventType === \"start\") {\n audio.playSFX(\"block\");\n }\n },\n [audio],\n );\n\n const handleMobileGesture = useCallback(\n (gesture: GestureEvent) => {\n switch (gesture.type) {\n case \"swipe-right\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"d\" }));\n break;\n case \"swipe-left\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"a\" }));\n break;\n case \"swipe-up\":\n if (trainingState.isTraining) {\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \" \" }));\n }\n break;\n case \"swipe-down\":\n trainingActions.resetDummy();\n break;\n case \"two-finger-tap\":\n trainingActions.setTrainingMode(\n trainingState.trainingMode === \"vital_point\"\n ? \"basics\"\n : \"vital_point\",\n );\n audio.playSFX(\"menu_select\");\n break;\n }\n },\n [trainingState, trainingActions, audio],\n );\n\n const handleMobileStanceChange = useCallback(\n (stanceIndex: number) => {\n handleStanceChangeWithVisualFeedback(stanceIndex);\n },\n [handleStanceChangeWithVisualFeedback],\n );\n\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n const key = event.key.toLowerCase();\n\n if (key === \"escape\") {\n onReturnToMenu();\n return;\n }\n\n if (key >= \"1\" && key <= \"8\") {\n const stanceIndex = parseInt(key) - 1;\n handleStanceChangeWithVisualFeedback(stanceIndex);\n event.preventDefault();\n return;\n }\n\n if (key === \" \") {\n handleAttack();\n event.preventDefault();\n return;\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onReturnToMenu, handleStanceChangeWithVisualFeedback, handleAttack]);\n\n\n const hasMountedRef = useRef(false);\n\n useEffect(() => {\n let audioStarted = false;\n\n const startMusic = async () => {\n try {\n await audio.fadeIn(\"cyberpunk_fusion\", 2000);\n audioStarted = true;\n } catch (err) {\n console.warn(\"Failed to start training music:\", err);\n trainingActions.setFeedback(\n \"오디오 초기화 실패 | Audio initialization failed\",\n );\n }\n };\n\n void startMusic();\n\n return () => {\n if (audioStarted) {\n void audio\n .fadeOut(2000)\n .then(() => audio.stopMusic())\n .catch((err) => console.warn(\"Failed to stop training music:\", err));\n }\n };\n }, [audio, trainingActions]);\n\n useEffect(() => {\n if (!hasMountedRef.current) {\n hasMountedRef.current = true;\n handleStartTraining();\n }\n }, [handleStartTraining]);\n\n\n useEffect(() => {\n if (trainingState.showFeedback) {\n const timer = setTimeout(() => trainingActions.hideFeedback(), 1500);\n return () => clearTimeout(timer);\n }\n }, [trainingState.showFeedback, trainingState.feedback, trainingActions]);\n\n useEffect(() => {\n if (!trainingState.isTraining || !trainingState.sessionStartTime) return;\n\n const interval = setInterval(() => {\n trainingActions.updateSessionDuration(\n Math.floor((Date.now() - (trainingState.sessionStartTime ?? 0)) / 1000),\n );\n }, 1000);\n\n return () => clearInterval(interval);\n }, [\n trainingState.isTraining,\n trainingState.sessionStartTime,\n trainingActions,\n ]);\n\n const prevTrainingModeRef = useRef<typeof trainingState.trainingMode>(\n trainingState.trainingMode,\n );\n const isFirstModeEffectRef = useRef<boolean>(true);\n const isTrainingRef = useRef<boolean>(trainingState.isTraining);\n const modeChangeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n isTrainingRef.current = trainingState.isTraining;\n }, [trainingState.isTraining]);\n\n const handleStartTrainingRef = useRef(handleStartTraining);\n const handleStopTrainingRef = useRef(handleStopTraining);\n\n useEffect(() => {\n handleStartTrainingRef.current = handleStartTraining;\n handleStopTrainingRef.current = handleStopTraining;\n }, [handleStartTraining, handleStopTraining]);\n\n useEffect(() => {\n if (isFirstModeEffectRef.current) {\n isFirstModeEffectRef.current = false;\n prevTrainingModeRef.current = trainingState.trainingMode;\n return;\n }\n\n const previousMode = prevTrainingModeRef.current;\n const modeChanged = previousMode !== trainingState.trainingMode;\n\n if (!modeChanged) {\n return;\n }\n\n prevTrainingModeRef.current = trainingState.trainingMode;\n\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n\n if (isTrainingRef.current) {\n handleStopTrainingRef.current();\n }\n\n modeChangeTimerRef.current = setTimeout(() => {\n handleStartTrainingRef.current();\n modeChangeTimerRef.current = null;\n }, 100);\n\n return () => {\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n };\n }, [trainingState.trainingMode]); // Only depend on training mode to avoid unnecessary re-runs\n\n\n const handleEffectComplete = useCallback(\n (effectId: number) => {\n trainingActions.removeHitEffect(effectId);\n },\n [trainingActions],\n );\n\n\n const handleAnatomyLayerToggle = useCallback(\n (layer: AnatomyLayer) => {\n trainingActions.toggleAnatomyLayer(layer);\n audio.playSFX(\"menu_click\");\n },\n [trainingActions, audio],\n );\n\n const handleVitalPointClick = useCallback(\n (pointId: string) => {\n trainingActions.setSelectedVitalPoint(pointId);\n audio.playSFX(\"menu_select\");\n },\n [trainingActions, audio],\n );\n\n\n const cameraConfig = useMemo(() => {\n const base = createCameraConfig(isMobile);\n if (!isPortrait) return base;\n return {\n ...base,\n fov: Math.min(80, base.fov + 15),\n position: [base.position[0], base.position[1], base.position[2] + 4] as [\n number,\n number,\n number,\n ],\n };\n }, [isMobile, isPortrait]);\n\n\n const performanceSettings = useMemo(() => {\n return getPerformanceSettings(width, isMobile);\n }, [width, isMobile]);\n\n\n return (\n <div\n style={{\n width: `${width}px`,\n height: `${height}px`,\n position: \"relative\",\n overflow: \"hidden\", // Prevent content from extending beyond container\n }}\n data-testid=\"training-screen-3d\"\n >\n <Canvas\n style={{ width: `${width}px`, height: `${height}px` }}\n gl={{\n antialias: performanceSettings.antialias,\n alpha: false,\n powerPreference: \"high-performance\",\n failIfMajorPerformanceCaveat: false, // Don't fail in software renderer\n preserveDrawingBuffer: true, // Help with context stability\n }}\n dpr={performanceSettings.dpr}\n shadows={false} // Temporarily disable shadows\n onCreated={({ gl }) => {\n gl.setClearColor(theme.colors.UI_BACKGROUND_DARK, 1);\n }}\n camera={cameraConfig}\n >\n {/* Lighting - base lighting, arena provides additional */}\n <ambientLight intensity={0.6} />\n <directionalLight position={[10, 10, 5]} intensity={1.2} />\n\n {/* Combat Arena 3D Environment - uses physics-based world dimensions */}\n <CombatArena3D\n lighting=\"cyberpunk\"\n scale={trainingAreaBounds.scale}\n worldWidthMeters={trainingAreaBounds.worldWidthMeters}\n worldDepthMeters={trainingAreaBounds.worldDepthMeters}\n />\n\n {/* Animation updater - 60fps updates */}\n <TrainingAnimationUpdater playerAnimation={playerAnimation} />\n\n {/* Acceleration updater - tracks movement time and updates speed */}\n <AccelerationUpdater\n isMoving={isMoving}\n velocity={velocity}\n movementTimeRef={movementTimeRef}\n lastDirectionRef={lastDirectionRef}\n onSpeedUpdate={setAccelerationBasedSpeed}\n walkSpeed={walkRunSpeeds.walkSpeed}\n runSpeed={walkRunSpeeds.runSpeed}\n />\n\n {/* Training dummy at fixed position */}\n <TrainingDummy3D\n position={dummyPosition}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n isTraining={trainingState.isTraining}\n health={trainingState.dummyHealth}\n onVitalPointHit={handleDummyHit}\n onDefeated={handleDummyDefeated}\n difficulty={difficulty}\n vitalPointCount={vitalPointCount}\n isMobile={isMobile}\n />\n\n {/* Anatomy overlay for educational visualization */}\n {trainingState.visibleAnatomyLayers.length > 0 && (\n <AnatomyOverlay3D\n position={dummyPosition}\n visibleLayers={trainingState.visibleAnatomyLayers}\n opacity={0.6}\n isMobile={isMobile}\n />\n )}\n\n {/* Vital Point Overlay - Show all 70 points on dummy */}\n {overlayVisible && (\n <VitalPointMarkers3D\n position={dummyPosition}\n visible={overlayVisible}\n severityFilter={severityFilters}\n regionFilter={regionFilter}\n searchQuery={searchQuery}\n showLabels={showLabels}\n scale={scale}\n animated={animated}\n selectedPoint={trainingState.selectedVitalPoint}\n onPointClick={handleVitalPointClick}\n />\n )}\n\n {/* Player model */}\n <Player3DWithTransitions\n {...convertPlayerStateToProps(\n trainingPlayerState,\n finalPlayer3DPosition,\n playerRotation,\n {\n isMobile,\n facing: \"right\",\n enableFacialExpressions: true,\n enableEyeTracking: true,\n opponentPosition: dummyPosition,\n },\n )}\n currentAnimation={animationStateToPlayerAnimation(\n playerAnimation.currentState,\n )}\n attackAnimation={attackAnimation}\n laterality={currentLaterality}\n enableTransitionEffects={!isMobile}\n enableStanceSymbol={true}\n enableStanceAudio={true}\n />\n\n {/* Foot Placement Markers for Footwork Drills */}\n {trainingState.trainingMode === \"footwork\" &&\n trainingState.footworkDrillActive && (\n <FootPlacementMarkers3D\n centerPosition={dummyPosition}\n pattern={\n trainingState.footworkDrillType === \"free_practice\"\n ? \"none\"\n : trainingState.footworkDrillType\n }\n currentStep={trainingState.footworkDrillStep}\n visible={true}\n scale={1.0}\n animated={true}\n />\n )}\n\n {/* Hit effects */}\n {trainingState.hitEffects.map((effect) => (\n <HitFeedbackEffect3D\n key={effect.id}\n position={effect.position}\n type={effect.type}\n damage={effect.damage}\n visible={effect.visible}\n onComplete={() => handleEffectComplete(effect.id)}\n isMobile={isMobile}\n />\n ))}\n\n {/* Stance Change Visual Indicator */}\n <StanceChangeIndicator\n currentStance={trainingState.currentStanceIndex}\n previousStance={previousStanceIndex}\n isMobile={isMobile}\n />\n\n {/* NOTE: Mobile controls moved OUTSIDE Canvas for reliable touch events */}\n {/* See MobileControlsPure component rendered after HUDs */}\n\n {/* Post-processing Effects - desktop high tier only for Android WebGL stability */}\n {performanceSettings.postProcessing && (\n <EffectComposer multisampling={4}>\n <Bloom\n luminanceThreshold={0.9}\n luminanceSmoothing={0.9}\n mipmapBlur\n intensity={0.8}\n radius={0.4}\n />\n <Noise opacity={0.03} />\n <Vignette eskil={false} offset={0.1} darkness={0.3} />\n </EffectComposer>\n )}\n </Canvas>\n\n {/* Html UI Overlays (positioned absolutely over Canvas) - matches CombatScreen pattern */}\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: Z_INDEX.HUD,\n overflow: \"clip\",\n }}\n data-testid=\"training-hud-overlay\"\n >\n {/* Left HUD - Anatomy Controls, Guard Indicator.\n Hidden on mobile because the side HUD occludes the compressed\n arena in both portrait and landscape. Anatomy layer toggles remain\n available on larger viewports where there is room for side panels. */}\n {!isMobile && (\n <TrainingLeftHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n visibleAnatomyLayers={trainingState.visibleAnatomyLayers}\n onAnatomyLayerToggle={handleAnatomyLayerToggle}\n currentStanceIndex={trainingState.currentStanceIndex}\n isInGuard={playerAnimation.isInStanceGuard()}\n />\n )}\n\n {/* Top HUD - Training Controls, Archetype Selector, Return Button. */}\n <TrainingTopHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n isTraining={trainingState.isTraining}\n onStartTraining={handleStartTraining}\n onStopTraining={handleStopTraining}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n overlayVisible={overlayVisible}\n onReturnToMenu={onReturnToMenu}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Right HUD - Mode Selector, Stats, Vital Point Selection.\n Hidden on mobile to keep the training dojang visible and usable.\n The core start/stop, archetype, vital-point toggle, technique bar,\n stance wheel, gestures, and touch controls remain available. */}\n {!isMobile && (\n <TrainingRightHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n trainingMode={trainingState.trainingMode}\n onModeChange={trainingActions.setTrainingMode}\n stats={{\n ...trainingState.stats,\n sessionDuration: trainingState.sessionDuration,\n bestCombo: trainingState.bestCombo,\n perfectStrikes: trainingState.perfectStrikes,\n }}\n distanceToDummy={distanceToDummy}\n effectiveReach={currentTechniqueReach}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n onVitalPointSelect={trainingActions.setSelectedVitalPoint}\n footworkDrillType={trainingState.footworkDrillType}\n footworkDrillStep={trainingState.footworkDrillStep}\n footworkDrillActive={trainingState.footworkDrillActive}\n onStartFootworkDrill={trainingActions.startFootworkDrill}\n onStopFootworkDrill={trainingActions.stopFootworkDrill}\n onAdvanceFootworkStep={trainingActions.advanceFootworkStep}\n />\n )}\n {/* Bottom HUD - Technique Bar, Feedback Messages, Mobile Controls */}\n <TrainingBottomHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n techniques={techniqueSelection.availableTechniques}\n player={trainingPlayerState}\n selectedIndex={techniqueSelection.selectedIndex}\n cooldowns={cooldownsMap}\n onTechniqueSelect={techniqueSelection.selectTechnique}\n showFeedback={trainingState.showFeedback}\n feedbackMessage={trainingState.feedback}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Vital Point Overlay Controls - Pure DOM overlay (outside Canvas) */}\n {overlayVisible && (\n <VitalPointOverlayControlsPure\n visible={overlayVisible}\n onVisibleChange={setOverlayVisible}\n severityFilters={severityFilters}\n onSeverityFiltersChange={setSeverityFilters}\n regionFilter={regionFilter}\n onRegionFilterChange={setRegionFilter}\n searchQuery={searchQuery}\n onSearchQueryChange={setSearchQuery}\n showLabels={showLabels}\n onShowLabelsChange={setShowLabels}\n animated={animated}\n onAnimatedChange={setAnimated}\n scale={scale}\n onScaleChange={setScale}\n screenPosition={{ top: \"180px\", left: \"20px\" }}\n isMobile={isMobile}\n />\n )}\n\n {/* Mobile Controls - Pure DOM overlay (outside Canvas for reliable touch) */}\n {isMobile && (\n <>\n <MobileControlsOverlay\n onMove={handleMobileMove}\n onAttack={handleMobileAttack}\n onBlock={handleMobileBlock}\n disabled={!mobileControlsEnabled}\n bottom={getMobileControlsBottom(height)}\n opacity={0.85}\n viewportWidth={width}\n viewportHeight={height}\n />\n\n <StanceWheelPure\n currentStance={trainingState.currentStanceIndex}\n onStanceChange={handleMobileStanceChange}\n expanded={trainingState.stanceWheelExpanded}\n onToggle={trainingActions.toggleStanceWheel}\n disabled={!mobileControlsEnabled}\n opacity={0.8}\n />\n\n <GestureRecognizerPure\n onGesture={handleMobileGesture}\n enabled={mobileControlsEnabled}\n showFeedback={true}\n minSwipeDistance={50}\n />\n </>\n )}\n </div>\n </div>\n );\n};\n\nexport default TrainingScreen3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmIA,IAAM,4BAAqE,EACzE,sBACI;CACJ,UAAU,QAAQ,UAAU;EAC1B,gBAAgB,OAAO,KAAK;CAC9B,CAAC;CAED,OAAO;AACT;;;;;;;AAwBA,IAAa,oBAAqD,EAChE,gBACA,gBACA,QAAQ,MACR,SAAS,KACT,mBAAmB,gBAAgB,WAC/B;CAGJ,MAAM,EAAE,OAAO,eAAe,SAAS,oBAAoB,iBAAiB;CAE5E,MAAM,QAAQ,SAAS;CAEvB,MAAM,EAAE,qBAAqB,iBAAiB,0BAC5C,eAAe;CAEjB,MAAM,EAAE,oBAAoB,UAAU,YAAY,eAChD,kBAAkB,OAAO,MAAM;CAEjC,MAAM,QAAQ,eAAe;EAC3B,SAAS;EACT,MAAM;EACN;CACF,CAAC;CAED,MAAM,gBAAgB,MAAM,cACpB,oBAAoB,YAAY,QAAQ,GAC9C,CAAC,YAAY,QAAQ,CACvB;CAEA,MAAM,aAA6B;CACnC,MAAM,kBAAkB;CAExB,MAAM,CAAC,mBAAmB,wBACxB,MAAM,SAA0B,gBAAgB;CAElD,MAAM,CAAC,gBAAgB,qBAAqB,MAAM,SAAS,KAAK;CAChE,MAAM,CAAC,iBAAiB,sBAAsB,MAAM,SAElD,CAAC,CAAC;CACJ,MAAM,CAAC,cAAc,mBACnB,MAAM,SAA2B,KAAK;CACxC,MAAM,CAAC,aAAa,kBAAkB,MAAM,SAAS,EAAE;CACvD,MAAM,CAAC,YAAY,iBAAiB,MAAM,SAAS,IAAI;CACvD,MAAM,CAAC,UAAU,eAAe,MAAM,SAAS,IAAI;CACnD,MAAM,CAAC,OAAO,YAAY,MAAM,SAAS,GAAG;CAG5C,MAAM,CAAC,iBAAiB,sBAAsB,MAAM,SAElD,KAAA,CAAS;CAEX,MAAM,gBAAgB;EACpB,MAAM,iBAAiB,MAAqB;GAC1C,IAAI,EAAE,QAAQ,OAAO,EAAE,QAAQ,KAAK;IAClC,mBAAmB,SAAS,CAAC,IAAI;IACjC,MAAM,QAAQ,aAAa;GAC7B;EACF;EAEA,OAAO,iBAAiB,WAAW,aAAa;EAChD,aAAa;GACX,OAAO,oBAAoB,WAAW,aAAa;EACrD;CACF,GAAG,CAAC,KAAK,CAAC;CAGV,MAAM,sBAAsB,OAAO,CAAC;CAEpC,2BAA2B;EACzB,qBAAqB;GACnB,QAAQ,KAAK,yCAAyC;GACtD,oBAAoB,WAAW;EACjC;EACA,yBAAyB;GACvB,QAAQ,IAAI,4CAA4C;EAC1D;EACA,aAAa;CACf,CAAC;CAGD,MAAM,sBAAsB,cAAc,IAAI,oBAAoB,GAAG,CAAC,CAAC;CAEvE,MAAM,CAAC,gBAAgB,qBAAqB,SAAS;EACnD,YAAY;EACZ,WAAW;EACX,mBAAmB;CACrB,CAAC;CAED,MAAM,CAAC,eAAe,oBAAoB,SAAS;EACjD,WAAW;EACX,UAAU;CACZ,CAAC;CAGD,MAAM,wBAAwB,eACrB;EACL,GAAG,mBAAmB,mBAAmB;EACzC,GAAG;CACL,IACA,CAAC,kBAAkB,CACrB;CAEA,MAAM,uBAAuB,aAC1B,gBAA0B;EACzB,eAAe,EAAE,UAAU,YAAY,CAAC;CAC1C,GACA,CAAC,cAAc,CACjB;CAEA,MAAM,iBAAiB,eACd;EACL,kBAAkB,mBAAmB;EACrC,kBAAkB,mBAAmB;CACvC,IACA,CAAC,mBAAmB,kBAAkB,mBAAmB,gBAAgB,CAC3E;CAGA,MAAM,kBAAkB,OAAO,CAAC;CAChC,MAAM,mBAAmB,OAAiC;EAAE,GAAG;EAAG,GAAG;CAAE,CAAC;CAExE,MAAM,CAAC,wBAAwB,6BAA6B,SAC1D,cAAc,SAChB;CAEA,MAAM,YAAY,eAAe,wBAAwB,cAAc,QAAQ;CAE/E,MAAM,EAAE,gBAAgB,UAAU,aAAa,kBAAkB;EAC/D,SAAS;EACT,QAAQ;EACR,kBAAkB;EAClB;EACA,eAAe,sBAAsB,cAAc;EACnD,iBAAiB;EACjB;EACA,kBAAkB;EAClB,sBAAsB,eAAe;CACvC,CAAC;CAED,MAAM,mBAAmB,cAAwC;EAC/D,OAAO;GAAC,eAAe;GAAG;GAAG,eAAe;EAAC;CAC/C,GAAG,CAAC,cAAc,CAAC;CAEnB,MAAM,gBAAgB,cACd;EAAC,mBAAmB,mBAAmB;EAAM;EAAG;CAAC,GACvD,CAAC,mBAAmB,gBAAgB,CACtC;CAEA,MAAM,yBAAyB,cACvB,oBAAoB,kBAAkB,aAAa,GACzD,CAAC,kBAAkB,aAAa,CAClC;CAEA,MAAM,kBAAkB,cAChB,KAAK,IAAI,GAAG,yBAAyB,0BAA0B,GACrE,CAAC,sBAAsB,CACzB;CAEA,MAAM,wBAAwB,OAAe,CAAC;CAE9C,MAAM,iBAAiB,cAAc;EACnC,IAAI,YAAY,aAAa,SAAS,MAAM,KAAK,SAAS,MAAM,IAC9D,OAAO,KAAK,MAAM,SAAS,GAAG,SAAS,CAAC;OACnC;GACL,MAAM,KAAK,cAAc,KAAK,iBAAiB;GAC/C,MAAM,KAAK,cAAc,KAAK,iBAAiB;GAC/C,OAAO,KAAK,MAAM,IAAI,EAAE;EAC1B;CACF,GAAG;EAAC;EAAU;EAAU;EAAkB;CAAa,CAAC;CAExD,gBAAgB;EACd,sBAAsB,UAAU;CAClC,GAAG,CAAC,cAAc,CAAC;CAGnB,MAAM,CAAC,mBAAmB,wBAAwB,SAA2B,OAAO;CAEpF,MAAM,iBAAiB,OAAO,CAAC;CAC/B,MAAM,kBAAkB,OAAiB,cAAc;CAEvD,gBAAgB;EACd,IAAI,CAAC,UAAU;GACb,eAAe,UAAU;GACzB,gBAAgB,UAAU;GAC1B;EACF;EAEA,MAAM,KAAK,eAAe,IAAI,gBAAgB,QAAQ;EACtD,MAAM,KAAK,eAAe,IAAI,gBAAgB,QAAQ;EACtD,MAAM,gBAAgB,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE;EAEjD,MAAM,gBAAgB,YAClB,yBAAyB,MACzB,yBAAyB;EAC7B,eAAe,WAAW;EAE1B,MAAM,eAAe,KAAK,MAAM,eAAe,UAAU,aAAa;EACtE,IAAI,eAAe,GAAG;GACpB,IAAI,eAAe,MAAM,GACvB,sBAAqB,SAAQ,SAAS,UAAU,SAAS,OAAO;GAElE,eAAe,WAAW,eAAe;EAC3C;EAEA,gBAAgB,UAAU;CAC5B,GAAG;EAAC;EAAgB;EAAU;CAAS,CAAC;CAGxC,MAAM,mBAAmB,OAMf,IAAI;CAEd,MAAM,oBAAoB,aAQlB,KAAK;CAEb,MAAM,qBAAqB,OAEjB,IAAI;CA4Bd,MAAM,kBAAkB,mBAAmB,EACzC,QA3B4B,eACrB;EACL,UAAU,OAAO,UAAU;GACzB,IAAI,UAAU,YAAY,UAAU,KAAK,iBAAiB,SAAS;IACjE,MAAM,aAAa,iBAAiB;IACpC,kBAAkB,QAAQ,WAAW,YAAY;KAC/C,eAAe,WAAW;KAC1B,aAAa,WAAW;IAC1B,CAAC;IACD,iBAAiB,UAAU;GAC7B;EACF;EACA,sBAAsB,UAAU;GAC9B,IAAI,UAAU,iBAAiB;IAC7B,sBAAsB;IACtB,MAAM,gBACJ,sBAAsB,cAAc;IACtC,IAAI,iBAAiB,mBAAmB,SACtC,mBAAmB,QAAQ,wBAAwB,aAAa;GAEpE;EACF;CACF,IACA,CAAC,uBAAuB,cAAc,kBAAkB,CAIhD,EACV,CAAC;CAED,gBAAgB;EACd,mBAAmB,UAAU;CAC/B,GAAG,CAAC,eAAe,CAAC;CAGpB,MAAM,gBAAgB,cACd,sBAAsB,cAAc,qBAC1C,CAAC,cAAc,kBAAkB,CACnC;CAEA,MAAM,CAAC,qBAAqB,0BAA0B,SAAiB,CAAC;CAExE,MAAM,mCAAmC,OACvC,cAAc,GAChB;CAGA,MAAM,sBAAsB,cAA2B;EACrD,OAAO;GACL,IAAI;GACJ,MAAM;IAAE,QAAQ;IAAO,SAAS;GAAU;GAC1C,WAAW;GACX,QAAQ;GACR,WAAW;GACX,IAAI;GACJ,OAAO;GACP,SAAS;GACT,YAAY;GACZ,QAAQ;GACR,WAAW;GACX,aAAa;GACb,SAAS;GACT,OAAO;GACP,WAAW;GACX,MAAM;GACN,eAAe;GACf,SAAS;GACT,UAAU;GACV,eAAe,sBAAsB,cAAc;GACnD,aAAa,YAAY;GACzB,UAAU;GACV,YAAY;GACZ,WAAW;GACX,cAAc;GACd,gBAAgB;GAChB,cAAc;GACd,sBAAsB;GACtB,eAAe,CAAC;GAChB,eAAe,CAAC;GAChB,aAAa,CAAC;GACd,qBAAqB;GACrB,kBAAkB;GAClB,WAAW;GACX,YAAY,cAAc,MAAM;GAChC,gBAAgB,cAAc;GAC9B,gBAAgB;GAChB,QAAQ,cAAc,MAAM;GAC5B,UAAU,cAAc,MAAM;GAC9B,YAAY,cAAc,MAAM;EAClC;CACF,GAAG;EAAC;EAAgB;EAAe;CAAiB,CAAC;CAErD,gBAAgB;EACd,MAAM,6BAA6B;GACjC,MAAM,gBAAgB,oBAAoB,wBACxC,qBACA,aAAa,SACb,KACF;GAEA,MAAM,eAAe,oBAAoB,wBACvC,qBACA,aAAa,SACb,KACF;GAEA,kBAAkB;IAChB,YAAY,cAAc;IAC1B,WAAW,cAAc;IACzB,mBAAmB,cAAc;GACnC,CAAC;GAED,iBAAiB;IACf,WAAW,cAAc;IACzB,UAAU,aAAa;GACzB,CAAC;EACH;EAEA,qBAAqB;EAErB,MAAM,aAAa,YAAY,sBAAsB,GAAG;EAExD,aAAa,cAAc,UAAU;CAEvC,GAAG,CAAC,mBAAmB,CAAC;CAGxB,MAAM,oBAAoB,cAClB,iBAAiB,iBAAiB,UACxC,CAAC,eAAe,CAClB;CAEA,MAAM,kBAAkB,cAAc;EACpC,IAAI,CAAC,mBACH,OAAO,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;EAElC,MAAM,KAAK,cAAc,KAAK,iBAAiB;EAC/C,MAAM,KAAK,cAAc,KAAK,iBAAiB;EAC/C,OAAO,IAAI,MAAM,QAAQ,IAAI,GAAG,EAAE,CAAC,CAAC,UAAU;CAChD,GAAG;EAAC;EAAe;EAAkB;CAAiB,CAAC;CAEvD,MAAM,EACJ,iBAAiB,uCACf,kBAAkB;EACpB,aAAa;EAEb,eAAe,iCAAiC;EAChD,eAAe,oBAAoB;EACnC,cAAc;EACd;EACA,mBAAmB;CACrB,CAAC;CAED,MAAM,wBAAwB,oBAC1B,qCACA;CAGJ,MAAM,kBAAkB,OAA4B,IAAI;CAExD,MAAM,qBAAqB,sBAAsB;EAC/C,QAAQ;EACR,SAAS,cAAc;EACvB,oBAAoB,aACjB,cAAyB;GACxB,gBAAgB,YACd,GAAG,UAAU,KAAK,OAAO,cAAc,UAAU,KAAK,QAAQ,EAChE;GAGA,mBADsB,0BAA0B,SAC7B,CAAa;GAGhC,gBAAgB,UAAU;EAC5B,GACA,CAAC,eAAe,CAClB;CACF,CAAC;CAWD,MAAM,EACJ,qBACA,oBACA,gBACA,qBACA,oBACA,iBACE,mBAAmB;EACrB,OAAO;EACP,SAAS;EACT;EACA;EACA;EACA,iBAAiB;EACjB,cAAc;EACd;EACA;EACA;EACA;EACA,qBA5B0B,cAAc;GACxC,MAAM,aAAa,mBAAmB;GACtC,MAAM,cAAc,mBAAmB;GACvC,IAAI,WAAW,WAAW,KAAK,cAAc,KAAK,eAAe,WAAW,QAC1E;GAEF,OAAO,WAAW,YAAY,EAAE;EAClC,GAAG,CAAC,mBAAmB,qBAAqB,mBAAmB,aAAa,CAqB1E;EACA,iBAAiB,YAAY;GAC3B,eAAe,OAAO;EACxB;EACA,iBAAiB;GACf,cAAc,gBAAgB;GAC9B,oBAAoB,gBAAgB;GACpC,yBAAyB,gBAAgB;GACzC,cAAc,gBAAgB;EAChC;EACA;CACF,CAAC;CAED,gBAAgB;EACd,gBAAgB,UAAU;CAC5B,GAAG,CAAC,YAAY,CAAC;CAEjB,gBAAgB;EACd,kBAAkB,UAAU;CAC9B,GAAG,CAAC,cAAc,CAAC;CAEnB,MAAM,uCAAuC,aAC1C,gBAAwB;EACvB,uBAAuB,cAAc,kBAAkB;EACvD,mBAAmB,WAAW;CAChC,GACA,CAAC,oBAAoB,cAAc,kBAAkB,CACvD;CAGA,MAAM,kBAAkB,OAAgB,QAAQ;CAChD,MAAM,mBAAmB,OAAgB,SAAS;CAClD,MAAM,gBAAgB,OAAsB,aAAa;CAEzD,gBAAgB;EACd,MAAM,kBAAkB,gBAAgB,YAAY;EACpD,MAAM,mBAAmB,iBAAiB,YAAY;EACtD,MAAM,gBAAgB,cAAc,YAAY;EAEhD,IAAI,mBAAmB,kBAAkB;GACvC,IAAI,UACF,IAAI,WACF,gBAAgB,aAAa,eAAe,GAAG;QAE/C,gBAAgB,aAAa,eAAe,IAAI;QAE7C,IAAI,gBAAgB,iBAAiB,eAAe,QAChD,gBAAgB,iBAAiB,eAAe,KACzD,gBAAgB,wBAAwB,aAAa;GAEvD,gBAAgB,UAAU;GAC1B,iBAAiB,UAAU;EAC7B;EAEA,IAAI,iBAAiB,CAAC,UAAU;GAC9B,IAAI,gBAAgB,iBAAiB,eAAe,QAChD,gBAAgB,gBAAgB,GAClC,gBAAgB,wBAAwB,aAAa;GAEvD,cAAc,UAAU;EAC1B;CACF,GAAG;EAAC;EAAU;EAAW;EAAe;CAAe,CAAC;CAGxD,MAAM,eAAe,cAAc;EACjC,MAAM,sBAAM,IAAI,IAAoB;EACpC,mBAAmB,gBAAgB,SAAS,OAAO;GACjD,IAAI,IAAI,GAAG,aAAa,GAAG,SAAS;EACtC,CAAC;EACD,OAAO;CACT,GAAG,CAAC,mBAAmB,eAAe,CAAC;CAEvC,MAAM,EAAE,uBAAuB,yBAAyB,cAAc;EACpE,MAAM,aAAa,mBAAmB;EACtC,MAAM,cAAc,mBAAmB;EACvC,IAAI,WAAW,WAAW,GACxB,OAAO;GACL,uBAAuB;GACvB,sBAAsB,cAAc;EACtC;EAEF,MAAM,mBACJ,WAAW,KAAK,IAAI,aAAa,WAAW,SAAS,CAAC;EACxD,IAAI,CAAC,kBACH,OAAO;GACL,uBAAuB;GACvB,sBAAsB,cAAc;EACtC;EAEF,MAAM,aAAa,kCAAkC,iBAAiB,EAAE;EACxE,MAAM,qBACJ,+BAA+B,iBAAiB;EAMlD,OAAO;GACL,uBANY,wBAAwB,kBACpC,oBACA,WAAW,MACX,aAGuB;GACvB,sBAAsB,WAAW;EACnC;CACF,GAAG;EACD,mBAAmB;EACnB,mBAAmB;EACnB;EACA;CACF,CAAC;CAED,gBAAgB;EACd,iCAAiC,UAAU;CAC7C,GAAG,CAAC,oBAAoB,CAAC;CAGzB,MAAM,qBAAqB,OAAsB,IAAI;CAErD,MAAM,wBAAwB;CAE9B,MAAM,mBAAmB,aACtB,WAA6B,cAA6B;EACzD,MAAM,eAA0C;GAC9C,IAAI;GACJ,YAAY;GACZ,OAAO;GACP,cAAc;GACd,MAAM;GACN,aAAa;GACb,MAAM;GACN,WAAW;EACb;EAEA,IAAI,cAAc,WAAW,WAAW;GACtC,IACE,mBAAmB,WACnB,mBAAmB,YAAY,aAAa,YAC5C;IACA,MAAM,UAAU,mBAAmB;IACnC,OAAO,cACL,IAAI,cAAc,SAAS;KACzB,KAAK;KACL,MAAM,MAAM,QAAQ,YAAY;KAChC,SAAS;KACT,YAAY;IACd,CAAC,CACH;GACF;GAEA,MAAM,MAAM,aAAa;GACzB,mBAAmB,UAAU;GAC7B,OAAO,cACL,IAAI,cAAc,WAAW;IAC3B;IACA,MAAM,MAAM,IAAI,YAAY;IAC5B,SAAS;IACT,YAAY;GACd,CAAC,CACH;EACF,OAAO,IAAI,cAAc;OACnB,mBAAmB,SAAS;IAC9B,MAAM,MAAM,mBAAmB;IAC/B,OAAO,cACL,IAAI,cAAc,SAAS;KACzB;KACA,MAAM,MAAM,IAAI,YAAY;KAC5B,SAAS;KACT,YAAY;IACd,CAAC,CACH;IACA,mBAAmB,UAAU;GAC/B;;CAEJ,GACA,CAAC,CACH;CAEA,MAAM,qBAAqB,kBAAkB;EAC3C,aAAa;CACf,GAAG,CAAC,YAAY,CAAC;CAEjB,MAAM,oBAAoB,aACvB,cAA+B;EAC9B,IAAI,cAAc,SAChB,MAAM,QAAQ,OAAO;CAEzB,GACA,CAAC,KAAK,CACR;CAEA,MAAM,sBAAsB,aACzB,YAA0B;EACzB,QAAQ,QAAQ,MAAhB;GACE,KAAK;IACH,OAAO,cAAc,IAAI,cAAc,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC;IAC/D;GACF,KAAK;IACH,OAAO,cAAc,IAAI,cAAc,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC;IAC/D;GACF,KAAK;IACH,IAAI,cAAc,YAChB,OAAO,cAAc,IAAI,cAAc,WAAW,EAAE,KAAK,IAAI,CAAC,CAAC;IAEjE;GACF,KAAK;IACH,gBAAgB,WAAW;IAC3B;GACF,KAAK;IACH,gBAAgB,gBACd,cAAc,iBAAiB,gBAC3B,WACA,aACN;IACA,MAAM,QAAQ,aAAa;IAC3B;EACJ;CACF,GACA;EAAC;EAAe;EAAiB;CAAK,CACxC;CAEA,MAAM,2BAA2B,aAC9B,gBAAwB;EACvB,qCAAqC,WAAW;CAClD,GACA,CAAC,oCAAoC,CACvC;CAGA,gBAAgB;EACd,MAAM,iBAAiB,UAAyB;GAC9C,MAAM,MAAM,MAAM,IAAI,YAAY;GAElC,IAAI,QAAQ,UAAU;IACpB,eAAe;IACf;GACF;GAEA,IAAI,OAAO,OAAO,OAAO,KAAK;IAE5B,qCADoB,SAAS,GAAG,IAAI,CACY;IAChD,MAAM,eAAe;IACrB;GACF;GAEA,IAAI,QAAQ,KAAK;IACf,aAAa;IACb,MAAM,eAAe;IACrB;GACF;EACF;EAEA,OAAO,iBAAiB,WAAW,aAAa;EAChD,aAAa,OAAO,oBAAoB,WAAW,aAAa;CAClE,GAAG;EAAC;EAAgB;EAAsC;CAAY,CAAC;CAGvE,MAAM,gBAAgB,OAAO,KAAK;CAElC,gBAAgB;EACd,IAAI,eAAe;EAEnB,MAAM,aAAa,YAAY;GAC7B,IAAI;IACF,MAAM,MAAM,OAAO,oBAAoB,GAAI;IAC3C,eAAe;GACjB,SAAS,KAAK;IACZ,QAAQ,KAAK,mCAAmC,GAAG;IACnD,gBAAgB,YACd,0CACF;GACF;EACF;EAEA,WAAgB;EAEhB,aAAa;GACX,IAAI,cACF,MACG,QAAQ,GAAI,CAAC,CACb,WAAW,MAAM,UAAU,CAAC,CAAC,CAC7B,OAAO,QAAQ,QAAQ,KAAK,kCAAkC,GAAG,CAAC;EAEzE;CACF,GAAG,CAAC,OAAO,eAAe,CAAC;CAE3B,gBAAgB;EACd,IAAI,CAAC,cAAc,SAAS;GAC1B,cAAc,UAAU;GACxB,oBAAoB;EACtB;CACF,GAAG,CAAC,mBAAmB,CAAC;CAGxB,gBAAgB;EACd,IAAI,cAAc,cAAc;GAC9B,MAAM,QAAQ,iBAAiB,gBAAgB,aAAa,GAAG,IAAI;GACnE,aAAa,aAAa,KAAK;EACjC;CACF,GAAG;EAAC,cAAc;EAAc,cAAc;EAAU;CAAe,CAAC;CAExE,gBAAgB;EACd,IAAI,CAAC,cAAc,cAAc,CAAC,cAAc,kBAAkB;EAElE,MAAM,WAAW,kBAAkB;GACjC,gBAAgB,sBACd,KAAK,OAAO,KAAK,IAAI,KAAK,cAAc,oBAAoB,MAAM,GAAI,CACxE;EACF,GAAG,GAAI;EAEP,aAAa,cAAc,QAAQ;CACrC,GAAG;EACD,cAAc;EACd,cAAc;EACd;CACF,CAAC;CAED,MAAM,sBAAsB,OAC1B,cAAc,YAChB;CACA,MAAM,uBAAuB,OAAgB,IAAI;CACjD,MAAM,gBAAgB,OAAgB,cAAc,UAAU;CAC9D,MAAM,qBAAqB,OAA6C,IAAI;CAE5E,gBAAgB;EACd,cAAc,UAAU,cAAc;CACxC,GAAG,CAAC,cAAc,UAAU,CAAC;CAE7B,MAAM,yBAAyB,OAAO,mBAAmB;CACzD,MAAM,wBAAwB,OAAO,kBAAkB;CAEvD,gBAAgB;EACd,uBAAuB,UAAU;EACjC,sBAAsB,UAAU;CAClC,GAAG,CAAC,qBAAqB,kBAAkB,CAAC;CAE5C,gBAAgB;EACd,IAAI,qBAAqB,SAAS;GAChC,qBAAqB,UAAU;GAC/B,oBAAoB,UAAU,cAAc;GAC5C;EACF;EAKA,IAAI,EAHiB,oBAAoB,YACJ,cAAc,eAGjD;EAGF,oBAAoB,UAAU,cAAc;EAE5C,IAAI,mBAAmB,SAAS;GAC9B,aAAa,mBAAmB,OAAO;GACvC,mBAAmB,UAAU;EAC/B;EAEA,IAAI,cAAc,SAChB,sBAAsB,QAAQ;EAGhC,mBAAmB,UAAU,iBAAiB;GAC5C,uBAAuB,QAAQ;GAC/B,mBAAmB,UAAU;EAC/B,GAAG,GAAG;EAEN,aAAa;GACX,IAAI,mBAAmB,SAAS;IAC9B,aAAa,mBAAmB,OAAO;IACvC,mBAAmB,UAAU;GAC/B;EACF;CACF,GAAG,CAAC,cAAc,YAAY,CAAC;CAG/B,MAAM,uBAAuB,aAC1B,aAAqB;EACpB,gBAAgB,gBAAgB,QAAQ;CAC1C,GACA,CAAC,eAAe,CAClB;CAGA,MAAM,2BAA2B,aAC9B,UAAwB;EACvB,gBAAgB,mBAAmB,KAAK;EACxC,MAAM,QAAQ,YAAY;CAC5B,GACA,CAAC,iBAAiB,KAAK,CACzB;CAEA,MAAM,wBAAwB,aAC3B,YAAoB;EACnB,gBAAgB,sBAAsB,OAAO;EAC7C,MAAM,QAAQ,aAAa;CAC7B,GACA,CAAC,iBAAiB,KAAK,CACzB;CAGA,MAAM,eAAe,cAAc;EACjC,MAAM,OAAO,mBAAmB,QAAQ;EACxC,IAAI,CAAC,YAAY,OAAO;EACxB,OAAO;GACL,GAAG;GACH,KAAK,KAAK,IAAI,IAAI,KAAK,MAAM,EAAE;GAC/B,UAAU;IAAC,KAAK,SAAS;IAAI,KAAK,SAAS;IAAI,KAAK,SAAS,KAAK;GAAC;EAKrE;CACF,GAAG,CAAC,UAAU,UAAU,CAAC;CAGzB,MAAM,sBAAsB,cAAc;EACxC,OAAO,uBAAuB,OAAO,QAAQ;CAC/C,GAAG,CAAC,OAAO,QAAQ,CAAC;CAGpB,OACE,qBAAC,OAAD;EACE,OAAO;GACL,OAAO,GAAG,MAAM;GAChB,QAAQ,GAAG,OAAO;GAClB,UAAU;GACV,UAAU;EACZ;EACA,eAAY;YAPd,CASE,qBAAC,QAAD;GACE,OAAO;IAAE,OAAO,GAAG,MAAM;IAAK,QAAQ,GAAG,OAAO;GAAI;GACpD,IAAI;IACF,WAAW,oBAAoB;IAC/B,OAAO;IACP,iBAAiB;IACjB,8BAA8B;IAC9B,uBAAuB;GACzB;GACA,KAAK,oBAAoB;GACzB,SAAS;GACT,YAAY,EAAE,SAAS;IACrB,GAAG,cAAc,MAAM,OAAO,oBAAoB,CAAC;GACrD;GACA,QAAQ;aAdV;IAiBE,oBAAC,gBAAD,EAAc,WAAW,GAAM,CAAA;IAC/B,oBAAC,oBAAD;KAAkB,UAAU;MAAC;MAAI;MAAI;KAAC;KAAG,WAAW;IAAM,CAAA;IAG1D,oBAAC,eAAD;KACE,UAAS;KACT,OAAO,mBAAmB;KAC1B,kBAAkB,mBAAmB;KACrC,kBAAkB,mBAAmB;IACtC,CAAA;IAGD,oBAAC,0BAAD,EAA2C,gBAAkB,CAAA;IAG7D,oBAAC,qBAAD;KACY;KACA;KACO;KACC;KAClB,eAAe;KACf,WAAW,cAAc;KACzB,UAAU,cAAc;IACzB,CAAA;IAGD,oBAAC,iBAAD;KACE,UAAU;KACV,oBAAoB,cAAc;KAClC,YAAY,cAAc;KAC1B,QAAQ,cAAc;KACtB,iBAAiB;KACjB,YAAY;KACA;KACK;KACP;IACX,CAAA;IAGA,cAAc,qBAAqB,SAAS,KAC3C,oBAAC,kBAAD;KACE,UAAU;KACV,eAAe,cAAc;KAC7B,SAAS;KACC;IACX,CAAA;IAIF,kBACC,oBAAC,qBAAD;KACE,UAAU;KACV,SAAS;KACT,gBAAgB;KACF;KACD;KACD;KACL;KACG;KACV,eAAe,cAAc;KAC7B,cAAc;IACf,CAAA;IAIH,oBAAC,yBAAD;KACE,GAAI,0BACF,qBACA,uBACA,gBACA;MACE;MACA,QAAQ;MACR,yBAAyB;MACzB,mBAAmB;MACnB,kBAAkB;KACpB,CACF;KACA,kBAAkB,gCAChB,gBAAgB,YAClB;KACiB;KACjB,YAAY;KACZ,yBAAyB,CAAC;KAC1B,oBAAoB;KACpB,mBAAmB;IACpB,CAAA;IAGA,cAAc,iBAAiB,cAC9B,cAAc,uBACZ,oBAAC,wBAAD;KACE,gBAAgB;KAChB,SACE,cAAc,sBAAsB,kBAChC,SACA,cAAc;KAEpB,aAAa,cAAc;KAC3B,SAAS;KACT,OAAO;KACP,UAAU;IACX,CAAA;IAIJ,cAAc,WAAW,KAAK,WAC7B,oBAAC,qBAAD;KAEE,UAAU,OAAO;KACjB,MAAM,OAAO;KACb,QAAQ,OAAO;KACf,SAAS,OAAO;KAChB,kBAAkB,qBAAqB,OAAO,EAAE;KACtC;IACX,GAPM,OAAO,EAOb,CACF;IAGD,oBAAC,uBAAD;KACE,eAAe,cAAc;KAC7B,gBAAgB;KACN;IACX,CAAA;IAMA,oBAAoB,kBACnB,qBAAC,gBAAD;KAAgB,eAAe;eAA/B;MACE,oBAAC,OAAD;OACE,oBAAoB;OACpB,oBAAoB;OACpB,YAAA;OACA,WAAW;OACX,QAAQ;MACT,CAAA;MACD,oBAAC,OAAD,EAAO,SAAS,IAAO,CAAA;MACvB,oBAAC,UAAD;OAAU,OAAO;OAAO,QAAQ;OAAK,UAAU;MAAM,CAAA;KACvC;;GAEZ;MAGR,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,KAAK;IACL,MAAM;IACN,OAAO;IACP,QAAQ;IACR,eAAe;IACf,QAAQ,QAAQ;IAChB,UAAU;GACZ;GACA,eAAY;aAXd;IAiBG,CAAC,YACA,oBAAC,iBAAD;KACS;KACC;KACE;KACK;KACf,sBAAsB,cAAc;KACpC,sBAAsB;KACtB,oBAAoB,cAAc;KAClC,WAAW,gBAAgB,gBAAgB;IAC5C,CAAA;IAIH,oBAAC,gBAAD;KACS;KACC;KACE;KACK;KACf,YAAY,cAAc;KAC1B,iBAAiB;KACjB,gBAAgB;KACG;KACnB,mBAAmB;KACH;KACA;KAChB,YAAY,UAAU,MAAM,QAAQ,KAAK;IAC1C,CAAA;IAMA,CAAC,YACA,oBAAC,kBAAD;KACS;KACC;KACE;KACK;KACf,cAAc,cAAc;KAC5B,cAAc,gBAAgB;KAC9B,OAAO;MACL,GAAG,cAAc;MACjB,iBAAiB,cAAc;MAC/B,WAAW,cAAc;MACzB,gBAAgB,cAAc;KAChC;KACiB;KACjB,gBAAgB;KAChB,oBAAoB,cAAc;KAClC,oBAAoB,gBAAgB;KACpC,mBAAmB,cAAc;KACjC,mBAAmB,cAAc;KACjC,qBAAqB,cAAc;KACnC,sBAAsB,gBAAgB;KACtC,qBAAqB,gBAAgB;KACrC,uBAAuB,gBAAgB;IACxC,CAAA;IAGH,oBAAC,mBAAD;KACS;KACC;KACE;KACK;KACf,YAAY,mBAAmB;KAC/B,QAAQ;KACR,eAAe,mBAAmB;KAClC,WAAW;KACX,mBAAmB,mBAAmB;KACtC,cAAc,cAAc;KAC5B,iBAAiB,cAAc;KACZ;KACnB,mBAAmB;KACnB,YAAY,UAAU,MAAM,QAAQ,KAAK;IAC1C,CAAA;IAGA,kBACC,oBAAC,+BAAD;KACE,SAAS;KACT,iBAAiB;KACA;KACjB,yBAAyB;KACX;KACd,sBAAsB;KACT;KACb,qBAAqB;KACT;KACZ,oBAAoB;KACV;KACV,kBAAkB;KACX;KACP,eAAe;KACf,gBAAgB;MAAE,KAAK;MAAS,MAAM;KAAO;KACnC;IACX,CAAA;IAIF,YACC,qBAAA,UAAA,EAAA,UAAA;KACE,oBAAC,uBAAD;MACE,QAAQ;MACR,UAAU;MACV,SAAS;MACT,UAAU,CAAC;MACX,QAAQ,wBAAwB,MAAM;MACtC,SAAS;MACT,eAAe;MACf,gBAAgB;KACjB,CAAA;KAED,oBAAC,iBAAD;MACE,eAAe,cAAc;MAC7B,gBAAgB;MAChB,UAAU,cAAc;MACxB,UAAU,gBAAgB;MAC1B,UAAU,CAAC;MACX,SAAS;KACV,CAAA;KAED,oBAAC,uBAAD;MACE,WAAW;MACX,SAAS;MACT,cAAc;MACd,kBAAkB;KACnB,CAAA;IACD,EAAA,CAAA;GAED;IACF;;AAET"}
1
+ {"version":3,"file":"TrainingScreen3D.js","names":[],"sources":["../../../../src/components/screens/training/TrainingScreen3D.tsx"],"sourcesContent":["/**\n * TrainingScreen3D - Three.js-based training screen\n *\n * Refactored to use consolidated hooks matching CombatScreen architecture.\n * Provides 3D training dummy with vital point targeting and UI overlays.\n *\n * UI Rendering: All HUD elements are rendered in an absolute-positioned div\n * OUTSIDE the Canvas, matching CombatScreen's reliable rendering pattern.\n * This eliminates the need for Html overlays inside Three.js and ensures\n * HUDs appear immediately without waiting for Canvas initialization.\n *\n * Architecture (Consolidated in PR #1394 + Issue #1398):\n * - TrainingLeftHUD: Anatomy controls, guard indicator\n * - TrainingRightHUD: Training stats, mode selector, vital point selection\n * - TrainingTopHUD: Training controls, archetype selector, return button\n * - TrainingBottomHUD: Technique bar, feedback messages, mobile controls\n * - VitalPointOverlayControlsPure: Vital point overlay controls (pure DOM)\n *\n * All UI components render as pure DOM in the HUD overlay div (lines 1230+).\n * NO Html components from @react-three/drei are used inside the Canvas.\n * This ensures clean separation of 3D rendering and UI layers.\n *\n * @korean 훈련화면3D - 훈련 상태 훅을 사용한 리팩토링된 3D 훈련 화면\n */\n\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { AccelerationUpdater } from \"../../../systems/movement/helpers/AccelerationUpdater\";\nimport {\n isRunningSpeed,\n STEP_DISTANCE_THRESHOLDS,\n} from \"../../../systems/movement/helpers/accelerationUtils\";\nimport {\n Bloom,\n EffectComposer,\n Noise,\n Vignette,\n} from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { useCombatAudio } from \"../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../data/archetypePhysicalAttributes\";\nimport { usePlayerAnimation } from \"../../../hooks/usePlayerAnimation\";\nimport { useTechniqueSelection } from \"../../../hooks/useTechniqueSelection\";\nimport { GestureEvent } from \"../../../hooks/useTouchControls\";\nimport { useWebGLContextLossHandler } from \"../../../hooks/useWebGLContextLossHandler\";\nimport { PlayerState } from \"../../../systems\";\nimport {\n AnimationEvents,\n AnimationState,\n AnimationType,\n resolveTechniqueAnimation,\n} from \"../../../systems/animation\";\nimport { getAnimationForTechniqueOrDefault } from \"../../../systems/animation/core/TechniqueAnimationMapping\";\nimport { physicalReachCalculator } from \"../../../systems/physics\";\nimport {\n MovementType,\n SpeedModifierSystem,\n} from \"../../../systems/physics/SpeedModifierSystem\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport {\n CombatState,\n PlayerArchetype,\n Position,\n Technique,\n TrigramStance,\n} from \"../../../types\";\nimport { getPerformanceSettings } from \"../../../types/constants\";\nimport { getMobileControlsBottom } from \"../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { DEFAULT_BODY_RADIUS_METERS } from \"../../../types/physicsConstants\";\nimport { usePlayerMovement } from \"../../../utils/inputSystem\";\nimport { calculateDistance3D } from \"../../../utils/math\";\nimport { getHUDPositionScale } from \"../../../utils/responsiveLayoutHelpers\";\nimport { createCameraConfig } from \"../../../utils/sharedPhysicsConfig\";\nimport {\n animationStateToPlayerAnimation,\n convertPlayerStateToProps,\n} from \"../../../utils/player3DHelpers\";\nimport { useKoreanTheme } from \"../../shared/base/useKoreanTheme\";\nimport {\n GestureRecognizerPure,\n StanceWheelPure,\n} from \"../../shared/mobile\";\nimport {\n MobileControlsOverlay,\n type ButtonEventType,\n type Direction,\n type DPadEventType,\n} from \"../../shared/mobile/MobileControlsPure\";\nimport {\n Player3DWithTransitions,\n VitalPointMarkers3D,\n type BodyRegionFilter,\n} from \"../../shared/three\";\nimport { StanceChangeIndicator } from \"../../shared/three/indicators/StanceChangeIndicator\";\nimport { CombatArena3D } from \"../../shared/three/scene/CombatArena3D\";\nimport { VitalPointOverlayControlsPure } from \"../../shared/ui/VitalPointOverlayControlsPure\";\nimport AnatomyOverlay3D, {\n type AnatomyLayer,\n} from \"./components/AnatomyOverlay3D\";\nimport FootPlacementMarkers3D from \"./components/FootPlacementMarkers3D\";\nimport HitFeedbackEffect3D from \"./components/HitFeedbackEffect3D\";\nimport type { DifficultyMode } from \"./components/TrainingDummy3D\";\nimport TrainingDummy3D from \"./components/TrainingDummy3D\";\nimport {\n TrainingBottomHUD,\n TrainingLeftHUD,\n TrainingRightHUD,\n TrainingTopHUD,\n} from \"./components/hud\";\nimport { useAttackMovement } from \"./hooks/useAttackMovement\";\nimport useTrainingActions from \"./hooks/useTrainingActions\";\nimport { useTrainingLayout } from \"./hooks/useTrainingLayout\";\nimport useTrainingState from \"./hooks/useTrainingState\";\n\n/**\n * AnimationUpdater - Component that updates player animation at 60fps\n *\n * @korean 훈련애니메이션업데이터 - 60fps로 플레이어 애니메이션을 업데이트하는 컴포넌트\n */\ninterface TrainingAnimationUpdaterProps {\n readonly playerAnimation: ReturnType<typeof usePlayerAnimation>;\n}\n\nconst TrainingAnimationUpdater: React.FC<TrainingAnimationUpdaterProps> = ({\n playerAnimation,\n}) => {\n useFrame((_state, delta) => {\n playerAnimation.update(delta);\n });\n\n return null;\n};\n\n/**\n * Props for the TrainingScreen3D component\n */\nexport interface TrainingScreen3DProps {\n /** Callback to update player state */\n readonly onPlayerUpdate: (updates: Partial<PlayerState>) => void;\n /** Callback when returning to menu */\n readonly onReturnToMenu: () => void;\n /** Canvas width in pixels. Defaults to 1200 */\n readonly width?: number;\n /** Canvas height in pixels. Defaults to 800 */\n readonly height?: number;\n /** Initial archetype from IntroScreen selection. Defaults to MUSA */\n readonly initialArchetype?: PlayerArchetype;\n}\n\n/**\n * TrainingScreen3D Component\n * Three.js-based training screen with 3D dummy and Html UI\n *\n * Uses consolidated hooks for state management matching CombatScreen architecture.\n */\nexport const TrainingScreen3D: React.FC<TrainingScreen3DProps> = ({\n onPlayerUpdate,\n onReturnToMenu,\n width = 1200,\n height = 800,\n initialArchetype = PlayerArchetype.MUSA,\n}) => {\n\n\n const { state: trainingState, actions: trainingActions } = useTrainingState();\n\n const audio = useAudio();\n \n const { playBoneImpactSound, playAttackSound, playStanceChangeSound } =\n useCombatAudio();\n\n const { trainingAreaBounds, isMobile, isPortrait, screenSize } =\n useTrainingLayout(width, height);\n\n const theme = useKoreanTheme({\n variant: \"primary\",\n size: \"md\",\n isMobile,\n });\n\n const positionScale = React.useMemo(\n () => getHUDPositionScale(screenSize, isMobile),\n [screenSize, isMobile],\n );\n\n const difficulty: DifficultyMode = \"normal\";\n const vitalPointCount = 70; // Show all 70 vital points\n\n const [selectedArchetype, setSelectedArchetype] =\n React.useState<PlayerArchetype>(initialArchetype);\n\n const [overlayVisible, setOverlayVisible] = React.useState(false);\n const [severityFilters, setSeverityFilters] = React.useState<\n import(\"../../../types/common\").VitalPointSeverity[]\n >([]);\n const [regionFilter, setRegionFilter] =\n React.useState<BodyRegionFilter>(\"all\");\n const [searchQuery, setSearchQuery] = React.useState(\"\");\n const [showLabels, setShowLabels] = React.useState(true);\n const [animated, setAnimated] = React.useState(true);\n const [scale, setScale] = React.useState(1.2);\n\n\n const [attackAnimation, setAttackAnimation] = React.useState<\n string | undefined\n >(undefined);\n\n React.useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"v\" || e.key === \"V\") {\n setOverlayVisible((prev) => !prev);\n audio.playSFX(\"menu_select\");\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, [audio]);\n\n\n const contextLossCountRef = useRef(0);\n\n useWebGLContextLossHandler({\n onContextLost: () => {\n console.warn(\"⚠️ WebGL context lost in TrainingScreen\");\n contextLossCountRef.current += 1;\n },\n onContextRestored: () => {\n console.log(\"✓ WebGL context restored in TrainingScreen\");\n },\n autoRestore: true,\n });\n\n\n const speedModifierSystem = useMemo(() => new SpeedModifierSystem(), []);\n\n const [speedModifiers, setSpeedModifiers] = useState({\n finalSpeed: 6.0, // BASE_WALK_SPEED (6.0 m/s for responsive combat)\n baseSpeed: 6.0,\n finalAcceleration: 12.0, // BASE_ACCELERATION (12.0 m/s² for quick response)\n });\n\n const [walkRunSpeeds, setWalkRunSpeeds] = useState({\n walkSpeed: 6.0,\n runSpeed: 10.0,\n });\n\n\n const initialPositionMeters = useMemo<Position>(\n () => ({\n x: trainingAreaBounds.worldWidthMeters * 0.0, // Centered laterally\n y: 0, // Centered vertically\n }),\n [trainingAreaBounds],\n );\n\n const handlePositionChange = useCallback(\n (newPosition: Position) => {\n onPlayerUpdate({ position: newPosition });\n },\n [onPlayerUpdate],\n );\n\n const movementBounds = useMemo(\n () => ({\n worldWidthMeters: trainingAreaBounds.worldWidthMeters,\n worldDepthMeters: trainingAreaBounds.worldDepthMeters,\n }),\n [trainingAreaBounds.worldWidthMeters, trainingAreaBounds.worldDepthMeters],\n );\n\n \n const movementTimeRef = useRef(0);\n const lastDirectionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n \n const [accelerationBasedSpeed, setAccelerationBasedSpeed] = useState(\n walkRunSpeeds.walkSpeed\n );\n \n const isRunning = isRunningSpeed(accelerationBasedSpeed, walkRunSpeeds.runSpeed);\n\n const { playerPosition, isMoving, velocity } = usePlayerMovement({\n enabled: true, // Always allow movement in training screen\n bounds: movementBounds, // Use memoized bounds object\n onPositionChange: handlePositionChange, // Use memoized callback\n initialPositionMeters,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n legInjuryFactor: 0, // No injury in training mode\n isRunning, // Use computed acceleration-based running state\n maxSpeedOverride: accelerationBasedSpeed,\n accelerationOverride: speedModifiers.finalAcceleration,\n });\n\n const player3DPosition = useMemo<[number, number, number]>(() => {\n return [playerPosition.x, 0, playerPosition.y];\n }, [playerPosition]);\n\n const dummyPosition = useMemo<[number, number, number]>(\n () => [trainingAreaBounds.worldWidthMeters * 0.15, 0, 0],\n [trainingAreaBounds.worldWidthMeters],\n );\n\n const centerToCenterDistance = useMemo(\n () => calculateDistance3D(player3DPosition, dummyPosition),\n [player3DPosition, dummyPosition],\n );\n\n const distanceToDummy = useMemo(\n () => Math.max(0, centerToCenterDistance - DEFAULT_BODY_RADIUS_METERS),\n [centerToCenterDistance],\n );\n\n const lastFacingRotationRef = useRef<number>(0);\n\n const playerRotation = useMemo(() => {\n if (isMoving && velocity && (velocity.x !== 0 || velocity.y !== 0)) {\n return Math.atan2(velocity.x, velocity.y);\n } else {\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return Math.atan2(dx, dz);\n }\n }, [isMoving, velocity, player3DPosition, dummyPosition]);\n\n useEffect(() => {\n lastFacingRotationRef.current = playerRotation;\n }, [playerRotation]);\n\n\n const [currentLaterality, setCurrentLaterality] = useState<\"left\" | \"right\">(\"right\");\n \n const stepCounterRef = useRef(0);\n const lastPositionRef = useRef<Position>(playerPosition);\n \n useEffect(() => {\n if (!isMoving) {\n stepCounterRef.current = 0;\n lastPositionRef.current = playerPosition;\n return;\n }\n\n const dx = playerPosition.x - lastPositionRef.current.x;\n const dy = playerPosition.y - lastPositionRef.current.y;\n const distanceMoved = Math.sqrt(dx * dx + dy * dy);\n \n const stepThreshold = isRunning \n ? STEP_DISTANCE_THRESHOLDS.RUN \n : STEP_DISTANCE_THRESHOLDS.WALK;\n stepCounterRef.current += distanceMoved;\n \n const stepsCrossed = Math.floor(stepCounterRef.current / stepThreshold);\n if (stepsCrossed > 0) {\n if (stepsCrossed % 2 === 1) {\n setCurrentLaterality(prev => prev === \"right\" ? \"left\" : \"right\");\n }\n stepCounterRef.current -= stepsCrossed * stepThreshold;\n }\n \n lastPositionRef.current = playerPosition;\n }, [playerPosition, isMoving, isRunning]);\n\n\n const pendingAttackRef = useRef<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>(null);\n\n const handleDummyHitRef = useRef<\n (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean\n >(() => false);\n\n const playerAnimationRef = useRef<ReturnType<\n typeof usePlayerAnimation\n > | null>(null);\n\n const playerAnimationEvents = useMemo<AnimationEvents>(\n () => ({\n onFrame: (frame, state) => {\n if (state === \"attack\" && frame === 6 && pendingAttackRef.current) {\n const attackData = pendingAttackRef.current;\n handleDummyHitRef.current(attackData.vitalPoint, {\n animationType: attackData.animationType,\n techniqueId: attackData.techniqueId,\n });\n pendingAttackRef.current = null;\n }\n },\n onAnimationComplete: (state) => {\n if (state === \"stance_change\") {\n playStanceChangeSound();\n const currentStance =\n TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex];\n if (currentStance && playerAnimationRef.current) {\n playerAnimationRef.current.transitionToStanceGuard(currentStance);\n }\n }\n },\n }),\n [playStanceChangeSound, trainingState.currentStanceIndex],\n );\n\n const playerAnimation = usePlayerAnimation({\n events: playerAnimationEvents,\n });\n\n useEffect(() => {\n playerAnimationRef.current = playerAnimation;\n }, [playerAnimation]);\n\n\n const currentStance = useMemo(\n () => TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n [trainingState.currentStanceIndex],\n );\n\n const [previousStanceIndex, setPreviousStanceIndex] = useState<number>(0);\n\n const currentTechniqueAnimationTypeRef = useRef<AnimationType>(\n AnimationType.JAB,\n );\n\n\n const trainingPlayerState = useMemo<PlayerState>(() => {\n return {\n id: \"training-player\",\n name: { korean: \"훈련생\", english: \"Trainee\" },\n archetype: selectedArchetype,\n health: 100,\n maxHealth: 100,\n ki: 100,\n maxKi: 100,\n stamina: 100,\n maxStamina: 100,\n energy: 100,\n maxEnergy: 100,\n attackPower: 10,\n defense: 10,\n speed: 10,\n technique: 10,\n pain: 0,\n consciousness: 100,\n balance: 100,\n momentum: 0,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n combatState: CombatState.IDLE,\n position: playerPosition,\n isBlocking: false,\n isStunned: false,\n isCountering: false,\n lastActionTime: 0,\n recoveryTime: 0,\n lastStanceChangeTime: 0,\n statusEffects: [],\n activeEffects: [],\n vitalPoints: [],\n totalDamageReceived: 0,\n totalDamageDealt: 0,\n hitsTaken: 0,\n hitsLanded: trainingState.stats.hits,\n perfectStrikes: trainingState.perfectStrikes,\n vitalPointHits: 0,\n misses: trainingState.stats.misses,\n accuracy: trainingState.stats.accuracy,\n comboCount: trainingState.stats.combo,\n };\n }, [playerPosition, trainingState, selectedArchetype]);\n\n useEffect(() => {\n const updateSpeedModifiers = () => {\n const walkModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.WALKING,\n false, // isCrouching\n );\n\n const runModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.RUNNING,\n false, // isCrouching\n );\n\n setSpeedModifiers({\n finalSpeed: walkModifiers.finalSpeed,\n baseSpeed: walkModifiers.baseSpeed,\n finalAcceleration: walkModifiers.finalAcceleration,\n });\n\n setWalkRunSpeeds({\n walkSpeed: walkModifiers.finalSpeed,\n runSpeed: runModifiers.finalSpeed,\n });\n };\n\n updateSpeedModifiers();\n\n const intervalId = setInterval(updateSpeedModifiers, 200);\n\n return () => clearInterval(intervalId);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [trainingPlayerState]); // speedModifierSystem is memoized and never changes\n\n\n const isPlayerAttacking = useMemo(\n () => playerAnimation?.currentState === \"attack\",\n [playerAnimation],\n );\n\n const attackDirection = useMemo(() => {\n if (!isPlayerAttacking) {\n return new THREE.Vector3(0, 0, 1); // Default forward direction\n }\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return new THREE.Vector3(dx, 0, dz).normalize();\n }, [dummyPosition, player3DPosition, isPlayerAttacking]);\n\n const {\n currentPosition: player3DPositionWithAttackMovement,\n } = useAttackMovement({\n isAttacking: isPlayerAttacking,\n // eslint-disable-next-line react-hooks/refs -- ref value is set synchronously before isAttacking becomes true; hook only reads this at attack start\n animationType: currentTechniqueAnimationTypeRef.current,\n currentStance: trainingPlayerState.currentStance,\n basePosition: player3DPosition,\n attackDirection,\n animationDuration: 0.4,\n });\n\n const finalPlayer3DPosition = isPlayerAttacking\n ? player3DPositionWithAttackMovement\n : player3DPosition;\n\n\n const handleAttackRef = useRef<(() => void) | null>(null);\n\n const techniqueSelection = useTechniqueSelection({\n player: trainingPlayerState,\n enabled: trainingState.isTraining,\n onTechniqueExecute: useCallback(\n (technique: Technique) => {\n trainingActions.setFeedback(\n `${technique.name.korean} 사용! | Used ${technique.name.english}!`,\n );\n\n const animationName = resolveTechniqueAnimation(technique);\n setAttackAnimation(animationName);\n\n\n handleAttackRef.current?.();\n },\n [trainingActions],\n ),\n });\n\n const selectedTechniqueId = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0 || selectedIdx < 0 || selectedIdx >= techniques.length) {\n return undefined;\n }\n return techniques[selectedIdx]?.id;\n }, [techniqueSelection.availableTechniques, techniqueSelection.selectedIndex]);\n\n const {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n } = useTrainingActions({\n state: trainingState,\n actions: trainingActions,\n playerPosition,\n player3DPosition,\n dummyPosition,\n playerArchetype: selectedArchetype,\n playerStance: currentStance,\n currentTechniqueAnimationTypeRef, // Ref for technique's animation type\n audio,\n playBoneImpactSound, // Pass bone impact audio function from useCombatAudio\n playAttackSound, // Pass attack sound function from useCombatAudio\n selectedTechniqueId, // Pass selected technique ID for intensity-based attack sounds\n onPlayerUpdate: (updates) => {\n onPlayerUpdate(updates);\n },\n playerAnimation: {\n transitionTo: playerAnimation.transitionTo,\n transitionToAttack: playerAnimation.transitionToAttack,\n transitionToStanceGuard: playerAnimation.transitionToStanceGuard,\n currentState: playerAnimation.currentState,\n },\n pendingAttackRef, // Share the ref with animation events\n });\n\n useEffect(() => {\n handleAttackRef.current = handleAttack;\n }, [handleAttack]);\n\n useEffect(() => {\n handleDummyHitRef.current = handleDummyHit;\n }, [handleDummyHit]);\n\n const handleStanceChangeWithVisualFeedback = useCallback(\n (stanceIndex: number) => {\n setPreviousStanceIndex(trainingState.currentStanceIndex);\n handleStanceChange(stanceIndex);\n },\n [handleStanceChange, trainingState.currentStanceIndex],\n );\n\n\n const prevIsMovingRef = useRef<boolean>(isMoving);\n const prevIsRunningRef = useRef<boolean>(isRunning);\n const prevStanceRef = useRef<TrigramStance>(currentStance);\n \n useEffect(() => {\n const isMovingChanged = prevIsMovingRef.current !== isMoving;\n const isRunningChanged = prevIsRunningRef.current !== isRunning;\n const stanceChanged = prevStanceRef.current !== currentStance;\n \n if (isMovingChanged || isRunningChanged) {\n if (isMoving) {\n if (isRunning) {\n playerAnimation.transitionTo(AnimationState.RUN);\n } else {\n playerAnimation.transitionTo(AnimationState.WALK);\n }\n } else if (playerAnimation.currentState === AnimationState.WALK || \n playerAnimation.currentState === AnimationState.RUN) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevIsMovingRef.current = isMoving;\n prevIsRunningRef.current = isRunning;\n }\n \n if (stanceChanged && !isMoving) {\n if (playerAnimation.currentState === AnimationState.IDLE || \n playerAnimation.isInStanceGuard()) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevStanceRef.current = currentStance;\n }\n }, [isMoving, isRunning, currentStance, playerAnimation]);\n\n\n const cooldownsMap = useMemo(() => {\n const map = new Map<string, number>();\n techniqueSelection.activeCooldowns.forEach((cd) => {\n map.set(cd.techniqueId, cd.remaining);\n });\n return map;\n }, [techniqueSelection.activeCooldowns]);\n\n const { currentTechniqueReach, currentAnimationType } = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const currentTechnique =\n techniques[Math.min(selectedIdx, techniques.length - 1)];\n if (!currentTechnique) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const animConfig = getAnimationForTechniqueOrDefault(currentTechnique.id);\n const physicalAttributes =\n getArchetypePhysicalAttributes(selectedArchetype);\n const reach = physicalReachCalculator.calculateMaxReach(\n physicalAttributes,\n animConfig.type,\n currentStance,\n );\n return {\n currentTechniqueReach: reach,\n currentAnimationType: animConfig.type,\n };\n }, [\n techniqueSelection.availableTechniques,\n techniqueSelection.selectedIndex,\n selectedArchetype,\n currentStance,\n ]);\n\n useEffect(() => {\n currentTechniqueAnimationTypeRef.current = currentAnimationType;\n }, [currentAnimationType]);\n\n\n const activeMobileKeyRef = useRef<string | null>(null);\n\n const mobileControlsEnabled = isMobile;\n\n const handleMobileMove = useCallback(\n (direction: Direction | null, eventType: DPadEventType) => {\n const directionMap: Record<Direction, string> = {\n up: \"w\",\n \"up-right\": \"w\",\n right: \"d\",\n \"down-right\": \"s\",\n down: \"s\",\n \"down-left\": \"s\",\n left: \"a\",\n \"up-left\": \"w\",\n };\n\n if (eventType === \"start\" && direction) {\n if (\n activeMobileKeyRef.current &&\n activeMobileKeyRef.current !== directionMap[direction]\n ) {\n const prevKey = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key: prevKey,\n code: `Key${prevKey.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n }\n\n const key = directionMap[direction];\n activeMobileKeyRef.current = key;\n window.dispatchEvent(\n new KeyboardEvent(\"keydown\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n } else if (eventType === \"end\") {\n if (activeMobileKeyRef.current) {\n const key = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n activeMobileKeyRef.current = null;\n }\n }\n },\n [],\n );\n\n const handleMobileAttack = useCallback(() => {\n handleAttack();\n }, [handleAttack]);\n\n const handleMobileBlock = useCallback(\n (eventType: ButtonEventType) => {\n if (eventType === \"start\") {\n audio.playSFX(\"block\");\n }\n },\n [audio],\n );\n\n const handleMobileGesture = useCallback(\n (gesture: GestureEvent) => {\n switch (gesture.type) {\n case \"swipe-right\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"d\" }));\n break;\n case \"swipe-left\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"a\" }));\n break;\n case \"swipe-up\":\n if (trainingState.isTraining) {\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \" \" }));\n }\n break;\n case \"swipe-down\":\n trainingActions.resetDummy();\n break;\n case \"two-finger-tap\":\n trainingActions.setTrainingMode(\n trainingState.trainingMode === \"vital_point\"\n ? \"basics\"\n : \"vital_point\",\n );\n audio.playSFX(\"menu_select\");\n break;\n }\n },\n [trainingState, trainingActions, audio],\n );\n\n const handleMobileStanceChange = useCallback(\n (stanceIndex: number) => {\n handleStanceChangeWithVisualFeedback(stanceIndex);\n },\n [handleStanceChangeWithVisualFeedback],\n );\n\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n const key = event.key.toLowerCase();\n\n if (key === \"escape\") {\n onReturnToMenu();\n return;\n }\n\n if (key >= \"1\" && key <= \"8\") {\n const stanceIndex = parseInt(key) - 1;\n handleStanceChangeWithVisualFeedback(stanceIndex);\n event.preventDefault();\n return;\n }\n\n if (key === \" \") {\n handleAttack();\n event.preventDefault();\n return;\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onReturnToMenu, handleStanceChangeWithVisualFeedback, handleAttack]);\n\n\n const hasMountedRef = useRef(false);\n\n useEffect(() => {\n let audioStarted = false;\n\n const startMusic = async () => {\n try {\n await audio.fadeIn(\"cyberpunk_fusion\", 2000);\n audioStarted = true;\n } catch (err) {\n console.warn(\"Failed to start training music:\", err);\n trainingActions.setFeedback(\n \"오디오 초기화 실패 | Audio initialization failed\",\n );\n }\n };\n\n void startMusic();\n\n return () => {\n if (audioStarted) {\n void audio\n .fadeOut(2000)\n .then(() => audio.stopMusic())\n .catch((err) => console.warn(\"Failed to stop training music:\", err));\n }\n };\n }, [audio, trainingActions]);\n\n useEffect(() => {\n if (!hasMountedRef.current) {\n hasMountedRef.current = true;\n handleStartTraining();\n }\n }, [handleStartTraining]);\n\n\n useEffect(() => {\n if (trainingState.showFeedback) {\n const timer = setTimeout(() => trainingActions.hideFeedback(), 1500);\n return () => clearTimeout(timer);\n }\n }, [trainingState.showFeedback, trainingState.feedback, trainingActions]);\n\n useEffect(() => {\n if (!trainingState.isTraining || !trainingState.sessionStartTime) return;\n\n const interval = setInterval(() => {\n trainingActions.updateSessionDuration(\n Math.floor((Date.now() - (trainingState.sessionStartTime ?? 0)) / 1000),\n );\n }, 1000);\n\n return () => clearInterval(interval);\n }, [\n trainingState.isTraining,\n trainingState.sessionStartTime,\n trainingActions,\n ]);\n\n const prevTrainingModeRef = useRef<typeof trainingState.trainingMode>(\n trainingState.trainingMode,\n );\n const isFirstModeEffectRef = useRef<boolean>(true);\n const isTrainingRef = useRef<boolean>(trainingState.isTraining);\n const modeChangeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n isTrainingRef.current = trainingState.isTraining;\n }, [trainingState.isTraining]);\n\n const handleStartTrainingRef = useRef(handleStartTraining);\n const handleStopTrainingRef = useRef(handleStopTraining);\n\n useEffect(() => {\n handleStartTrainingRef.current = handleStartTraining;\n handleStopTrainingRef.current = handleStopTraining;\n }, [handleStartTraining, handleStopTraining]);\n\n useEffect(() => {\n if (isFirstModeEffectRef.current) {\n isFirstModeEffectRef.current = false;\n prevTrainingModeRef.current = trainingState.trainingMode;\n return;\n }\n\n const previousMode = prevTrainingModeRef.current;\n const modeChanged = previousMode !== trainingState.trainingMode;\n\n if (!modeChanged) {\n return;\n }\n\n prevTrainingModeRef.current = trainingState.trainingMode;\n\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n\n if (isTrainingRef.current) {\n handleStopTrainingRef.current();\n }\n\n modeChangeTimerRef.current = setTimeout(() => {\n handleStartTrainingRef.current();\n modeChangeTimerRef.current = null;\n }, 100);\n\n return () => {\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n };\n }, [trainingState.trainingMode]); // Only depend on training mode to avoid unnecessary re-runs\n\n\n const handleEffectComplete = useCallback(\n (effectId: number) => {\n trainingActions.removeHitEffect(effectId);\n },\n [trainingActions],\n );\n\n\n const handleAnatomyLayerToggle = useCallback(\n (layer: AnatomyLayer) => {\n trainingActions.toggleAnatomyLayer(layer);\n audio.playSFX(\"menu_click\");\n },\n [trainingActions, audio],\n );\n\n const handleVitalPointClick = useCallback(\n (pointId: string) => {\n trainingActions.setSelectedVitalPoint(pointId);\n audio.playSFX(\"menu_select\");\n },\n [trainingActions, audio],\n );\n\n\n const cameraConfig = useMemo(() => {\n const base = createCameraConfig(isMobile);\n if (!isPortrait) return base;\n return {\n ...base,\n fov: Math.min(80, base.fov + 15),\n position: [base.position[0], base.position[1], base.position[2] + 4] as [\n number,\n number,\n number,\n ],\n };\n }, [isMobile, isPortrait]);\n\n\n const performanceSettings = useMemo(() => {\n return getPerformanceSettings(width, isMobile);\n }, [width, isMobile]);\n\n\n return (\n <div\n style={{\n width: `${width}px`,\n height: `${height}px`,\n position: \"relative\",\n overflow: \"hidden\", // Prevent content from extending beyond container\n }}\n data-testid=\"training-screen-3d\"\n >\n <Canvas\n style={{ width: `${width}px`, height: `${height}px` }}\n gl={{\n antialias: performanceSettings.antialias,\n alpha: false,\n powerPreference: \"high-performance\",\n failIfMajorPerformanceCaveat: false, // Don't fail in software renderer\n preserveDrawingBuffer: true, // Help with context stability\n }}\n dpr={performanceSettings.dpr}\n shadows={false} // Temporarily disable shadows\n onCreated={({ gl }) => {\n gl.setClearColor(theme.colors.UI_BACKGROUND_DARK, 1);\n }}\n camera={cameraConfig}\n >\n {/* Lighting - base lighting, arena provides additional */}\n <ambientLight intensity={0.6} />\n <directionalLight position={[10, 10, 5]} intensity={1.2} />\n\n {/* Combat Arena 3D Environment - uses physics-based world dimensions */}\n <CombatArena3D\n lighting=\"cyberpunk\"\n scale={trainingAreaBounds.scale}\n worldWidthMeters={trainingAreaBounds.worldWidthMeters}\n worldDepthMeters={trainingAreaBounds.worldDepthMeters}\n />\n\n {/* Animation updater - 60fps updates */}\n <TrainingAnimationUpdater playerAnimation={playerAnimation} />\n\n {/* Acceleration updater - tracks movement time and updates speed */}\n <AccelerationUpdater\n isMoving={isMoving}\n velocity={velocity}\n movementTimeRef={movementTimeRef}\n lastDirectionRef={lastDirectionRef}\n onSpeedUpdate={setAccelerationBasedSpeed}\n walkSpeed={walkRunSpeeds.walkSpeed}\n runSpeed={walkRunSpeeds.runSpeed}\n />\n\n {/* Training dummy at fixed position */}\n <TrainingDummy3D\n position={dummyPosition}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n isTraining={trainingState.isTraining}\n health={trainingState.dummyHealth}\n onVitalPointHit={handleDummyHit}\n onDefeated={handleDummyDefeated}\n difficulty={difficulty}\n vitalPointCount={vitalPointCount}\n isMobile={isMobile}\n />\n\n {/* Anatomy overlay for educational visualization */}\n {trainingState.visibleAnatomyLayers.length > 0 && (\n <AnatomyOverlay3D\n position={dummyPosition}\n visibleLayers={trainingState.visibleAnatomyLayers}\n opacity={0.6}\n isMobile={isMobile}\n />\n )}\n\n {/* Vital Point Overlay - Show all 70 points on dummy */}\n {overlayVisible && (\n <VitalPointMarkers3D\n position={dummyPosition}\n visible={overlayVisible}\n severityFilter={severityFilters}\n regionFilter={regionFilter}\n searchQuery={searchQuery}\n showLabels={showLabels}\n scale={scale}\n animated={animated}\n selectedPoint={trainingState.selectedVitalPoint}\n onPointClick={handleVitalPointClick}\n />\n )}\n\n {/* Player model */}\n <Player3DWithTransitions\n {...convertPlayerStateToProps(\n trainingPlayerState,\n finalPlayer3DPosition,\n playerRotation,\n {\n isMobile,\n facing: \"right\",\n enableFacialExpressions: true,\n enableEyeTracking: true,\n opponentPosition: dummyPosition,\n },\n )}\n currentAnimation={animationStateToPlayerAnimation(\n playerAnimation.currentState,\n )}\n attackAnimation={attackAnimation}\n laterality={currentLaterality}\n enableTransitionEffects={!isMobile}\n enableStanceSymbol={true}\n enableStanceAudio={true}\n />\n\n {/* Foot Placement Markers for Footwork Drills */}\n {trainingState.trainingMode === \"footwork\" &&\n trainingState.footworkDrillActive && (\n <FootPlacementMarkers3D\n centerPosition={dummyPosition}\n pattern={\n trainingState.footworkDrillType === \"free_practice\"\n ? \"none\"\n : trainingState.footworkDrillType\n }\n currentStep={trainingState.footworkDrillStep}\n visible={true}\n scale={1.0}\n animated={true}\n />\n )}\n\n {/* Hit effects */}\n {trainingState.hitEffects.map((effect) => (\n <HitFeedbackEffect3D\n key={effect.id}\n position={effect.position}\n type={effect.type}\n damage={effect.damage}\n visible={effect.visible}\n onComplete={() => handleEffectComplete(effect.id)}\n isMobile={isMobile}\n />\n ))}\n\n {/* Stance Change Visual Indicator */}\n <StanceChangeIndicator\n currentStance={trainingState.currentStanceIndex}\n previousStance={previousStanceIndex}\n isMobile={isMobile}\n />\n\n {/* NOTE: Mobile controls moved OUTSIDE Canvas for reliable touch events */}\n {/* See MobileControlsPure component rendered after HUDs */}\n\n {/* Post-processing Effects - desktop high tier only for Android WebGL stability */}\n {performanceSettings.postProcessing && (\n <EffectComposer multisampling={4}>\n <Bloom\n luminanceThreshold={0.9}\n luminanceSmoothing={0.9}\n mipmapBlur\n intensity={0.8}\n radius={0.4}\n />\n <Noise opacity={0.03} />\n <Vignette eskil={false} offset={0.1} darkness={0.3} />\n </EffectComposer>\n )}\n </Canvas>\n\n {/* Html UI Overlays (positioned absolutely over Canvas) - matches CombatScreen pattern */}\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: Z_INDEX.HUD,\n overflow: \"clip\",\n }}\n data-testid=\"training-hud-overlay\"\n >\n {/* Left HUD - Anatomy Controls, Guard Indicator.\n Hidden on mobile because the side HUD occludes the compressed\n arena in both portrait and landscape. Anatomy layer toggles remain\n available on larger viewports where there is room for side panels. */}\n {!isMobile && (\n <TrainingLeftHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n visibleAnatomyLayers={trainingState.visibleAnatomyLayers}\n onAnatomyLayerToggle={handleAnatomyLayerToggle}\n currentStanceIndex={trainingState.currentStanceIndex}\n isInGuard={playerAnimation.isInStanceGuard()}\n />\n )}\n\n {/* Top HUD - Training Controls, Archetype Selector, Return Button. */}\n <TrainingTopHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n isTraining={trainingState.isTraining}\n onStartTraining={handleStartTraining}\n onStopTraining={handleStopTraining}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n overlayVisible={overlayVisible}\n onReturnToMenu={onReturnToMenu}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Right HUD - Mode Selector, Stats, Vital Point Selection.\n Hidden on mobile to keep the training dojang visible and usable.\n The core start/stop, archetype, vital-point toggle, technique bar,\n stance wheel, gestures, and touch controls remain available. */}\n {!isMobile && (\n <TrainingRightHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n trainingMode={trainingState.trainingMode}\n onModeChange={trainingActions.setTrainingMode}\n stats={{\n ...trainingState.stats,\n sessionDuration: trainingState.sessionDuration,\n bestCombo: trainingState.bestCombo,\n perfectStrikes: trainingState.perfectStrikes,\n }}\n distanceToDummy={distanceToDummy}\n effectiveReach={currentTechniqueReach}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n onVitalPointSelect={trainingActions.setSelectedVitalPoint}\n footworkDrillType={trainingState.footworkDrillType}\n footworkDrillStep={trainingState.footworkDrillStep}\n footworkDrillActive={trainingState.footworkDrillActive}\n onStartFootworkDrill={trainingActions.startFootworkDrill}\n onStopFootworkDrill={trainingActions.stopFootworkDrill}\n onAdvanceFootworkStep={trainingActions.advanceFootworkStep}\n />\n )}\n {/* Bottom HUD - Technique Bar, Feedback Messages, Mobile Controls */}\n <TrainingBottomHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n techniques={techniqueSelection.availableTechniques}\n player={trainingPlayerState}\n selectedIndex={techniqueSelection.selectedIndex}\n cooldowns={cooldownsMap}\n onTechniqueSelect={techniqueSelection.selectTechnique}\n showFeedback={trainingState.showFeedback}\n feedbackMessage={trainingState.feedback}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Vital Point Overlay Controls - Pure DOM overlay (outside Canvas) */}\n {overlayVisible && (\n <VitalPointOverlayControlsPure\n visible={overlayVisible}\n onVisibleChange={setOverlayVisible}\n severityFilters={severityFilters}\n onSeverityFiltersChange={setSeverityFilters}\n regionFilter={regionFilter}\n onRegionFilterChange={setRegionFilter}\n searchQuery={searchQuery}\n onSearchQueryChange={setSearchQuery}\n showLabels={showLabels}\n onShowLabelsChange={setShowLabels}\n animated={animated}\n onAnimatedChange={setAnimated}\n scale={scale}\n onScaleChange={setScale}\n screenPosition={{ top: \"180px\", left: \"20px\" }}\n isMobile={isMobile}\n />\n )}\n\n {/* Mobile Controls - Pure DOM overlay (outside Canvas for reliable touch) */}\n {isMobile && (\n <>\n <MobileControlsOverlay\n onMove={handleMobileMove}\n onAttack={handleMobileAttack}\n onBlock={handleMobileBlock}\n disabled={!mobileControlsEnabled}\n bottom={getMobileControlsBottom(height)}\n opacity={0.85}\n viewportWidth={width}\n viewportHeight={height}\n />\n\n <StanceWheelPure\n currentStance={trainingState.currentStanceIndex}\n onStanceChange={handleMobileStanceChange}\n expanded={trainingState.stanceWheelExpanded}\n onToggle={trainingActions.toggleStanceWheel}\n disabled={!mobileControlsEnabled}\n opacity={0.8}\n />\n\n <GestureRecognizerPure\n onGesture={handleMobileGesture}\n enabled={mobileControlsEnabled}\n showFeedback={true}\n minSwipeDistance={50}\n />\n </>\n )}\n </div>\n </div>\n );\n};\n\nexport default TrainingScreen3D;\n"],"mappings":"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"}
@@ -75,7 +75,8 @@ function useAttackMovement(config) {
75
75
  const recovering = physicsRef.current.isInRecoveryPhase(elapsedTime, result);
76
76
  setIsLunging(lunging);
77
77
  setIsRecovering(recovering);
78
- setProgress(Math.min(1, elapsedTime / result.totalDuration));
78
+ const totalProgress = Math.min(1, elapsedTime / result.totalDuration);
79
+ setProgress(totalProgress);
79
80
  if (lunging || recovering) {
80
81
  const basePos = new THREE.Vector3(...basePosition);
81
82
  const newPosition = physicsRef.current.applyAttackMovement(basePos, result, elapsedTime, recovering);
@@ -1 +1 @@
1
- {"version":3,"file":"useAttackMovement.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useAttackMovement.ts"],"sourcesContent":["/**\n * useAttackMovement Hook - Track Attack Movement for 3D Characters\n *\n * Custom hook for managing attack movement physics during animations.\n * Tracks lunge and recovery phases with smooth easing curves.\n *\n * @korean 공격이동훅 - 공격 이동 추적\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { AnimationType } from \"@/systems/animation\";\nimport { AttackMovementPhysics } from \"@/systems/physics\";\nimport { TrigramStance } from \"@/types/common\";\n\n/**\n * Configuration for useAttackMovement hook\n */\nexport interface UseAttackMovementConfig {\n /** Whether character is currently attacking */\n readonly isAttacking: boolean;\n /** Animation type for current attack */\n readonly animationType?: AnimationType;\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Base position of character (when not attacking) */\n readonly basePosition: [number, number, number];\n /** Direction vector for attack (normalized) */\n readonly attackDirection?: THREE.Vector3;\n /** Animation duration in seconds (default: 0.4) */\n readonly animationDuration?: number;\n}\n\n/**\n * Return value from useAttackMovement hook\n */\nexport interface UseAttackMovementResult {\n /** Current 3D position including attack movement */\n readonly currentPosition: [number, number, number];\n /** Whether in forward lunge phase */\n readonly isLunging: boolean;\n /** Whether in recovery return phase */\n readonly isRecovering: boolean;\n /** Progress through attack movement (0-1) */\n readonly progress: number;\n}\n\n/**\n * useAttackMovement hook\n *\n * Tracks attack movement physics and returns current position.\n * Automatically handles lunge and recovery phases with smooth easing.\n *\n * @param config - Attack movement configuration\n * @returns Current position and movement state\n *\n * @example\n * ```typescript\n * const { currentPosition, isLunging } = useAttackMovement({\n * isAttacking: playerIsAttacking,\n * animationType: AnimationType.ROUNDHOUSE_KICK,\n * currentStance: TrigramStance.LI,\n * basePosition: [0, 0, 0],\n * attackDirection: new THREE.Vector3(1, 0, 0),\n * });\n *\n * // Use currentPosition for rendering\n * <CharacterModel position={currentPosition} />\n * ```\n *\n * @korean 공격이동사용\n */\nexport function useAttackMovement(\n config: UseAttackMovementConfig\n): UseAttackMovementResult {\n const {\n isAttacking,\n animationType,\n currentStance,\n basePosition,\n attackDirection,\n animationDuration = 0.4,\n } = config;\n\n const physicsRef = useRef(new AttackMovementPhysics());\n\n const attackStartTimeRef = useRef<number | null>(null);\n const attackMovementResultRef = useRef<ReturnType<\n typeof physicsRef.current.calculateAttackMovement\n > | null>(null);\n\n const [currentPosition, setCurrentPosition] = useState<\n [number, number, number]\n >(basePosition);\n const [isLunging, setIsLunging] = useState(false);\n const [isRecovering, setIsRecovering] = useState(false);\n const [progress, setProgress] = useState(0);\n\n const wasAttackingRef = useRef(false);\n\n useEffect(() => {\n if (!isAttacking) {\n setCurrentPosition(basePosition);\n }\n }, [isAttacking, basePosition]);\n\n useEffect(() => {\n if (isAttacking && !wasAttackingRef.current) {\n attackStartTimeRef.current = performance.now() / 1000;\n\n if (animationType && attackDirection) {\n attackMovementResultRef.current =\n physicsRef.current.calculateAttackMovement({\n animationType,\n currentStance,\n direction: attackDirection.clone().normalize(),\n animationDuration,\n });\n }\n } else if (!isAttacking && wasAttackingRef.current) {\n attackStartTimeRef.current = null;\n attackMovementResultRef.current = null;\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(0);\n }\n\n wasAttackingRef.current = isAttacking;\n\n if (!isAttacking || !attackStartTimeRef.current || !attackMovementResultRef.current) {\n return;\n }\n\n const result = attackMovementResultRef.current;\n let animationFrameId: number;\n\n const updatePosition = () => {\n if (attackStartTimeRef.current === null) return;\n\n const currentTime = performance.now() / 1000;\n const elapsedTime = currentTime - attackStartTimeRef.current;\n\n const lunging = physicsRef.current.isInLungePhase(\n elapsedTime,\n result.lungeDuration\n );\n const recovering = physicsRef.current.isInRecoveryPhase(\n elapsedTime,\n result\n );\n\n setIsLunging(lunging);\n setIsRecovering(recovering);\n\n const totalProgress = Math.min(1.0, elapsedTime / result.totalDuration);\n setProgress(totalProgress);\n\n if (lunging || recovering) {\n const basePos = new THREE.Vector3(...basePosition);\n const newPosition = physicsRef.current.applyAttackMovement(\n basePos,\n result,\n elapsedTime,\n recovering\n );\n\n setCurrentPosition([newPosition.x, newPosition.y, newPosition.z]);\n\n animationFrameId = requestAnimationFrame(updatePosition);\n } else {\n setCurrentPosition(basePosition);\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(1.0);\n }\n };\n\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n }, [\n isAttacking,\n animationType,\n currentStance,\n attackDirection,\n animationDuration,\n basePosition,\n ]);\n\n return {\n currentPosition,\n isLunging,\n isRecovering,\n progress,\n };\n}\n\nexport default useAttackMovement;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAgB,kBACd,QACyB;CACzB,MAAM,EACJ,aACA,eACA,eACA,cACA,iBACA,oBAAoB,OAClB;CAEJ,MAAM,aAAa,OAAO,IAAI,sBAAsB,CAAC;CAErD,MAAM,qBAAqB,OAAsB,IAAI;CACrD,MAAM,0BAA0B,OAEtB,IAAI;CAEd,MAAM,CAAC,iBAAiB,sBAAsB,SAE5C,YAAY;CACd,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,UAAU,eAAe,SAAS,CAAC;CAE1C,MAAM,kBAAkB,OAAO,KAAK;CAEpC,gBAAgB;EACd,IAAI,CAAC,aACH,mBAAmB,YAAY;CAEnC,GAAG,CAAC,aAAa,YAAY,CAAC;CAE9B,gBAAgB;EACd,IAAI,eAAe,CAAC,gBAAgB,SAAS;GAC3C,mBAAmB,UAAU,YAAY,IAAI,IAAI;GAEjD,IAAI,iBAAiB,iBACnB,wBAAwB,UACtB,WAAW,QAAQ,wBAAwB;IACzC;IACA;IACA,WAAW,gBAAgB,MAAM,CAAC,CAAC,UAAU;IAC7C;GACF,CAAC;EAEP,OAAO,IAAI,CAAC,eAAe,gBAAgB,SAAS;GAClD,mBAAmB,UAAU;GAC7B,wBAAwB,UAAU;GAClC,aAAa,KAAK;GAClB,gBAAgB,KAAK;GACrB,YAAY,CAAC;EACf;EAEA,gBAAgB,UAAU;EAE1B,IAAI,CAAC,eAAe,CAAC,mBAAmB,WAAW,CAAC,wBAAwB,SAC1E;EAGF,MAAM,SAAS,wBAAwB;EACvC,IAAI;EAEJ,MAAM,uBAAuB;GAC3B,IAAI,mBAAmB,YAAY,MAAM;GAGzC,MAAM,cADc,YAAY,IAAI,IAAI,MACN,mBAAmB;GAErD,MAAM,UAAU,WAAW,QAAQ,eACjC,aACA,OAAO,aACT;GACA,MAAM,aAAa,WAAW,QAAQ,kBACpC,aACA,MACF;GAEA,aAAa,OAAO;GACpB,gBAAgB,UAAU;GAG1B,YADsB,KAAK,IAAI,GAAK,cAAc,OAAO,aAC7C,CAAa;GAEzB,IAAI,WAAW,YAAY;IACzB,MAAM,UAAU,IAAI,MAAM,QAAQ,GAAG,YAAY;IACjD,MAAM,cAAc,WAAW,QAAQ,oBACrC,SACA,QACA,aACA,UACF;IAEA,mBAAmB;KAAC,YAAY;KAAG,YAAY;KAAG,YAAY;IAAC,CAAC;IAEhE,mBAAmB,sBAAsB,cAAc;GACzD,OAAO;IACL,mBAAmB,YAAY;IAC/B,aAAa,KAAK;IAClB,gBAAgB,KAAK;IACrB,YAAY,CAAG;GACjB;EACF;EAEA,mBAAmB,sBAAsB,cAAc;EAEvD,aAAa;GACX,IAAI,kBACF,qBAAqB,gBAAgB;EAEzC;CACF,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useAttackMovement.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useAttackMovement.ts"],"sourcesContent":["/**\n * useAttackMovement Hook - Track Attack Movement for 3D Characters\n *\n * Custom hook for managing attack movement physics during animations.\n * Tracks lunge and recovery phases with smooth easing curves.\n *\n * @korean 공격이동훅 - 공격 이동 추적\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { AnimationType } from \"@/systems/animation\";\nimport { AttackMovementPhysics } from \"@/systems/physics\";\nimport { TrigramStance } from \"@/types/common\";\n\n/**\n * Configuration for useAttackMovement hook\n */\nexport interface UseAttackMovementConfig {\n /** Whether character is currently attacking */\n readonly isAttacking: boolean;\n /** Animation type for current attack */\n readonly animationType?: AnimationType;\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Base position of character (when not attacking) */\n readonly basePosition: [number, number, number];\n /** Direction vector for attack (normalized) */\n readonly attackDirection?: THREE.Vector3;\n /** Animation duration in seconds (default: 0.4) */\n readonly animationDuration?: number;\n}\n\n/**\n * Return value from useAttackMovement hook\n */\nexport interface UseAttackMovementResult {\n /** Current 3D position including attack movement */\n readonly currentPosition: [number, number, number];\n /** Whether in forward lunge phase */\n readonly isLunging: boolean;\n /** Whether in recovery return phase */\n readonly isRecovering: boolean;\n /** Progress through attack movement (0-1) */\n readonly progress: number;\n}\n\n/**\n * useAttackMovement hook\n *\n * Tracks attack movement physics and returns current position.\n * Automatically handles lunge and recovery phases with smooth easing.\n *\n * @param config - Attack movement configuration\n * @returns Current position and movement state\n *\n * @example\n * ```typescript\n * const { currentPosition, isLunging } = useAttackMovement({\n * isAttacking: playerIsAttacking,\n * animationType: AnimationType.ROUNDHOUSE_KICK,\n * currentStance: TrigramStance.LI,\n * basePosition: [0, 0, 0],\n * attackDirection: new THREE.Vector3(1, 0, 0),\n * });\n *\n * // Use currentPosition for rendering\n * <CharacterModel position={currentPosition} />\n * ```\n *\n * @korean 공격이동사용\n */\nexport function useAttackMovement(\n config: UseAttackMovementConfig\n): UseAttackMovementResult {\n const {\n isAttacking,\n animationType,\n currentStance,\n basePosition,\n attackDirection,\n animationDuration = 0.4,\n } = config;\n\n const physicsRef = useRef(new AttackMovementPhysics());\n\n const attackStartTimeRef = useRef<number | null>(null);\n const attackMovementResultRef = useRef<ReturnType<\n typeof physicsRef.current.calculateAttackMovement\n > | null>(null);\n\n const [currentPosition, setCurrentPosition] = useState<\n [number, number, number]\n >(basePosition);\n const [isLunging, setIsLunging] = useState(false);\n const [isRecovering, setIsRecovering] = useState(false);\n const [progress, setProgress] = useState(0);\n\n const wasAttackingRef = useRef(false);\n\n useEffect(() => {\n if (!isAttacking) {\n setCurrentPosition(basePosition);\n }\n }, [isAttacking, basePosition]);\n\n useEffect(() => {\n if (isAttacking && !wasAttackingRef.current) {\n attackStartTimeRef.current = performance.now() / 1000;\n\n if (animationType && attackDirection) {\n attackMovementResultRef.current =\n physicsRef.current.calculateAttackMovement({\n animationType,\n currentStance,\n direction: attackDirection.clone().normalize(),\n animationDuration,\n });\n }\n } else if (!isAttacking && wasAttackingRef.current) {\n attackStartTimeRef.current = null;\n attackMovementResultRef.current = null;\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(0);\n }\n\n wasAttackingRef.current = isAttacking;\n\n if (!isAttacking || !attackStartTimeRef.current || !attackMovementResultRef.current) {\n return;\n }\n\n const result = attackMovementResultRef.current;\n let animationFrameId: number;\n\n const updatePosition = () => {\n if (attackStartTimeRef.current === null) return;\n\n const currentTime = performance.now() / 1000;\n const elapsedTime = currentTime - attackStartTimeRef.current;\n\n const lunging = physicsRef.current.isInLungePhase(\n elapsedTime,\n result.lungeDuration\n );\n const recovering = physicsRef.current.isInRecoveryPhase(\n elapsedTime,\n result\n );\n\n setIsLunging(lunging);\n setIsRecovering(recovering);\n\n const totalProgress = Math.min(1.0, elapsedTime / result.totalDuration);\n setProgress(totalProgress);\n\n if (lunging || recovering) {\n const basePos = new THREE.Vector3(...basePosition);\n const newPosition = physicsRef.current.applyAttackMovement(\n basePos,\n result,\n elapsedTime,\n recovering\n );\n\n setCurrentPosition([newPosition.x, newPosition.y, newPosition.z]);\n\n animationFrameId = requestAnimationFrame(updatePosition);\n } else {\n setCurrentPosition(basePosition);\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(1.0);\n }\n };\n\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n }, [\n isAttacking,\n animationType,\n currentStance,\n attackDirection,\n animationDuration,\n basePosition,\n ]);\n\n return {\n currentPosition,\n isLunging,\n isRecovering,\n progress,\n };\n}\n\nexport default useAttackMovement;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAgB,kBACd,QACyB;CACzB,MAAM,EACJ,aACA,eACA,eACA,cACA,iBACA,oBAAoB,OAClB;CAEJ,MAAM,aAAa,OAAO,IAAI,sBAAsB,CAAC;CAErD,MAAM,qBAAqB,OAAsB,IAAI;CACrD,MAAM,0BAA0B,OAEtB,IAAI;CAEd,MAAM,CAAC,iBAAiB,sBAAsB,SAE5C,YAAY;CACd,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,UAAU,eAAe,SAAS,CAAC;CAE1C,MAAM,kBAAkB,OAAO,KAAK;CAEpC,gBAAgB;EACd,IAAI,CAAC,aACH,mBAAmB,YAAY;CAEnC,GAAG,CAAC,aAAa,YAAY,CAAC;CAE9B,gBAAgB;EACd,IAAI,eAAe,CAAC,gBAAgB,SAAS;GAC3C,mBAAmB,UAAU,YAAY,IAAI,IAAI;GAEjD,IAAI,iBAAiB,iBACnB,wBAAwB,UACtB,WAAW,QAAQ,wBAAwB;IACzC;IACA;IACA,WAAW,gBAAgB,MAAM,CAAC,CAAC,UAAU;IAC7C;GACF,CAAC;EAEP,OAAO,IAAI,CAAC,eAAe,gBAAgB,SAAS;GAClD,mBAAmB,UAAU;GAC7B,wBAAwB,UAAU;GAClC,aAAa,KAAK;GAClB,gBAAgB,KAAK;GACrB,YAAY,CAAC;EACf;EAEA,gBAAgB,UAAU;EAE1B,IAAI,CAAC,eAAe,CAAC,mBAAmB,WAAW,CAAC,wBAAwB,SAC1E;EAGF,MAAM,SAAS,wBAAwB;EACvC,IAAI;EAEJ,MAAM,uBAAuB;GAC3B,IAAI,mBAAmB,YAAY,MAAM;GAGzC,MAAM,cADc,YAAY,IAAI,IAAI,MACN,mBAAmB;GAErD,MAAM,UAAU,WAAW,QAAQ,eACjC,aACA,OAAO,aACT;GACA,MAAM,aAAa,WAAW,QAAQ,kBACpC,aACA,MACF;GAEA,aAAa,OAAO;GACpB,gBAAgB,UAAU;GAE1B,MAAM,gBAAgB,KAAK,IAAI,GAAK,cAAc,OAAO,aAAa;GACtE,YAAY,aAAa;GAEzB,IAAI,WAAW,YAAY;IACzB,MAAM,UAAU,IAAI,MAAM,QAAQ,GAAG,YAAY;IACjD,MAAM,cAAc,WAAW,QAAQ,oBACrC,SACA,QACA,aACA,UACF;IAEA,mBAAmB;KAAC,YAAY;KAAG,YAAY;KAAG,YAAY;IAAC,CAAC;IAEhE,mBAAmB,sBAAsB,cAAc;GACzD,OAAO;IACL,mBAAmB,YAAY;IAC/B,aAAa,KAAK;IAClB,gBAAgB,KAAK;IACrB,YAAY,CAAG;GACjB;EACF;EAEA,mBAAmB,sBAAsB,cAAc;EAEvD,aAAa;GACX,IAAI,kBACF,qBAAqB,gBAAgB;EAEzC;CACF,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA;CACF;AACF"}
@@ -245,8 +245,9 @@ function useTrainingActions(config) {
245
245
  currentTechniqueAnimationTypeRef.current = animationType;
246
246
  }
247
247
  const startTime = performance.now() / 1e3;
248
+ const accuracy = calculateHitAccuracy(player3DPosition, dummyPosition, playerArchetype, playerStance, animationType, techniqueToUse?.reachConfig);
248
249
  pendingAttackRef.current = {
249
- accuracy: calculateHitAccuracy(player3DPosition, dummyPosition, playerArchetype, playerStance, animationType, techniqueToUse?.reachConfig),
250
+ accuracy,
250
251
  vitalPoint: state.selectedVitalPoint ?? "generic",
251
252
  animationType,
252
253
  startTime,
@@ -1 +1 @@
1
- {"version":3,"file":"useTrainingActions.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingActions.ts"],"sourcesContent":["/**\n * useTrainingActions Hook - Training Action Handlers\n *\n * Custom hook for managing training action handlers.\n * Mirrors useCombatActions pattern for consistency.\n *\n * @korean 훈련액션훅 - 훈련 액션 핸들러 관리\n */\n\nimport { useCallback, useRef } from \"react\";\nimport {\n AudioBodyRegion,\n ImpactIntensity,\n} from \"../../../../audio/types\";\nimport type { AttackIntensity } from \"../../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport {\n AnimationState,\n AnimationType,\n getAnimationDurationOrFallback,\n getAnimationForTechnique,\n} from \"../../../../systems/animation\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { getTechniquesByStance } from \"../../../../systems/trigram/techniques\";\nimport { KoreanTechniquesSystem } from \"../../../../systems/trigram/KoreanTechniques\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport type { KoreanTechnique } from \"../../../../systems/vitalpoint/types\";\nimport {\n CombatAttackType,\n DamageType,\n PlayerArchetype,\n Position,\n TrigramStance,\n} from \"../../../../types/common\";\nimport {\n DEFAULT_BODY_RADIUS_METERS,\n METERS_TO_TRAINING_UNITS,\n} from \"../../../../types/physicsConstants\";\nimport { calculateDistance3D } from \"../../../../utils/math\";\nimport { TrainingActions, TrainingScreenState } from \"./useTrainingState\";\n\nexport interface UseTrainingActionsConfig {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n readonly playerPosition: Position;\n readonly player3DPosition: [number, number, number];\n readonly dummyPosition: [number, number, number];\n readonly playerArchetype: import(\"../../../../types/common\").PlayerArchetype;\n readonly playerStance: TrigramStance;\n /** Ref to current selected technique's animation type for distance-based hit detection */\n readonly currentTechniqueAnimationTypeRef: React.MutableRefObject<AnimationType>;\n readonly audio: {\n readonly playSFX: (sound: string, volume?: number) => Promise<void>;\n };\n /** Attack sound function from useCombatAudio for playing attack whoosh sounds */\n readonly playAttackSound?: (intensity: AttackIntensity) => Promise<void>;\n /** Bone impact audio function from useCombatAudio or similar hook */\n readonly playBoneImpactSound?: (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly onPlayerUpdate: (updates: {\n currentStance?: TrigramStance;\n lastActionTime?: number;\n position?: Position;\n }) => void;\n readonly playerAnimation: {\n readonly transitionTo: (state: AnimationState) => boolean;\n readonly transitionToAttack: (durationSeconds: number) => boolean;\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n readonly currentState: string;\n };\n /** External ref to store pending attack data - shared with animation events */\n readonly pendingAttackRef: React.MutableRefObject<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>;\n /** Currently selected technique ID (from technique bar) */\n readonly selectedTechniqueId?: string;\n /** Callback to set the visual attack animation */\n readonly setAttackAnimation?: (animationName: string | undefined) => void;\n}\n\nexport interface UseTrainingActionsReturn {\n readonly handleStartTraining: () => void;\n readonly handleStopTraining: () => void;\n readonly handleDummyHit: (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean;\n readonly handleDummyDefeated: () => void;\n readonly handleStanceChange: (stanceIndex: number) => void;\n readonly handleAttack: () => void;\n}\n\n/**\n * Get the best default technique for an archetype based on current stance.\n *\n * Each archetype has preferred combat styles:\n * - MUSA: Direct strikes, joint techniques (BLUNT/JOINT damage)\n * - AMSALJA: Nerve strikes, pressure points (NERVE/PRESSURE damage)\n * - HACKER: Precise calculated strikes (high accuracy)\n * - JEONGBO_YOWON: Psychological pressure, submissions (PRESSURE/JOINT)\n * - JOJIK_POKRYEOKBAE: High damage brutal strikes\n *\n * @korean 원형별 기본 기술 선택 - 8개 자세에 따른 최적 기술\n */\nfunction getDefaultTechniqueForArchetype(\n archetype: PlayerArchetype,\n stance: TrigramStance,\n): KoreanTechnique | undefined {\n const techniques = getTechniquesByStance(stance);\n if (techniques.length === 0) return undefined;\n\n const scoredTechniques = techniques.map((tech) => {\n let score = 0;\n const damageType = tech.damageType;\n const attackType = tech.type;\n const isAdvanced =\n (tech.kiCost || 0) >= 10 || (tech.staminaCost || 0) >= 15;\n\n switch (archetype) {\n case PlayerArchetype.MUSA:\n if (damageType === DamageType.JOINT) score += 30;\n if (damageType === DamageType.CRUSHING) score += 25;\n if (damageType === DamageType.BLUNT) score += 20;\n if (attackType === CombatAttackType.STRIKE) score += 15;\n if (attackType === CombatAttackType.PUNCH) score += 10;\n if (isAdvanced) score += 10;\n break;\n\n case PlayerArchetype.AMSALJA:\n if (damageType === DamageType.NERVE) score += 35;\n if (damageType === DamageType.PRESSURE) score += 30;\n if (attackType === CombatAttackType.NERVE_STRIKE) score += 25;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 25;\n if (attackType === CombatAttackType.THRUST) score += 15;\n if ((tech.accuracy || 0) >= 0.9) score += 10;\n break;\n\n case PlayerArchetype.HACKER:\n if ((tech.accuracy || 0) >= 0.9) score += 30;\n if ((tech.accuracy || 0) >= 0.8) score += 15;\n if (damageType === DamageType.NERVE) score += 20;\n if (damageType === DamageType.INTERNAL) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n if (damageType === DamageType.PRESSURE) score += 30;\n if (damageType === DamageType.JOINT) score += 25;\n if (attackType === CombatAttackType.GRAPPLE) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n if ((tech.damage || 0) >= 35) score += 35;\n if ((tech.damage || 0) >= 30) score += 20;\n if (damageType === DamageType.SLASHING) score += 20;\n if (damageType === DamageType.PIERCING) score += 15;\n if (damageType === DamageType.CRUSHING) score += 15;\n if (attackType === CombatAttackType.KICK) score += 10;\n break;\n }\n\n return { technique: tech, score };\n });\n\n scoredTechniques.sort((a, b) => b.score - a.score);\n return scoredTechniques[0]?.technique;\n}\n\n/**\n * Calculate hit accuracy based on distance and effective reach.\n * Uses PhysicalReachCalculator for animation-aware reach calculation.\n *\n * Distance logic matches CombatSystem: if out of reach, guaranteed miss (accuracy 0).\n * Training scene coordinates are in meters, using METERS_TO_TRAINING_UNITS = 1.0.\n *\n * **IMPORTANT**: Distance calculation accounts for body radius.\n * Center-to-center distance is adjusted by subtracting target body radius\n * because attacks hit the body surface, not the center point.\n * The target body radius is calculated from physical attributes for archetypes,\n * or uses DEFAULT_BODY_RADIUS_METERS for training dummies.\n *\n * Example: If player center is 1.5m from dummy center, but dummy body\n * extends 0.23m from center, the effective distance to hit is 1.27m.\n *\n * @korean 거리 기반 명중률 계산 - 사정거리 밖이면 빗나감\n */\nfunction calculateHitAccuracy(\n playerPos: [number, number, number],\n dummyPos: [number, number, number],\n archetype: import(\"../../../../types/common\").PlayerArchetype,\n stance: TrigramStance,\n animationType?: AnimationType,\n reachConfig?: import(\"../../../../types/physics\").PhysicalReachConfig,\n): number {\n const centerToCenterDistance = calculateDistance3D(playerPos, dummyPos);\n\n const playerPhysicalAttributes = getArchetypePhysicalAttributes(archetype);\n\n const targetBodyRadius = DEFAULT_BODY_RADIUS_METERS;\n\n const effectiveDistance = Math.max(\n 0,\n centerToCenterDistance - targetBodyRadius,\n );\n\n if (animationType !== undefined) {\n const maxReachMeters = physicalReachCalculator.calculateMaxReach(\n playerPhysicalAttributes,\n animationType,\n stance,\n reachConfig, // Use technique's designed reach if provided\n );\n\n const reachInUnits = maxReachMeters * METERS_TO_TRAINING_UNITS;\n\n if (effectiveDistance > reachInUnits) {\n return 0;\n }\n\n return Math.max(0.7, 1.0 - (effectiveDistance / reachInUnits) * 0.3);\n }\n\n const defaultReach = 0.7 * METERS_TO_TRAINING_UNITS;\n if (effectiveDistance > defaultReach) {\n return 0; // Out of reach = miss\n }\n return Math.max(0.5, 1.0 - (effectiveDistance / defaultReach) * 0.5);\n}\n\n/**\n * useTrainingActions hook\n * Provides training action handlers with proper memoization\n */\nexport function useTrainingActions(\n config: UseTrainingActionsConfig,\n): UseTrainingActionsReturn {\n const {\n state,\n actions,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n audio,\n playBoneImpactSound,\n playAttackSound,\n onPlayerUpdate,\n playerAnimation,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n } = config;\n\n const dummyResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleStartTraining = useCallback(() => {\n actions.startTraining();\n audio.playSFX(\"menu_select\");\n }, [actions, audio]);\n\n const handleStopTraining = useCallback(() => {\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n dummyResetTimeoutRef.current = null;\n }\n actions.stopTraining();\n audio.playSFX(\"menu_back\");\n }, [actions, audio]);\n\n const handleDummyDefeated = useCallback(() => {\n actions.setFeedback(\"훈련 더미 무력화! | Dummy Defeated!\");\n audio.playSFX(\"ki_release\");\n\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n }\n\n dummyResetTimeoutRef.current = setTimeout(() => {\n actions.resetDummy();\n }, 2000);\n }, [actions, audio]);\n\n const handleDummyHit = useCallback(\n (\n _vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ): boolean => {\n const animationType = attackContext?.animationType;\n\n let reachConfig: import(\"../../../../types/physics\").PhysicalReachConfig | undefined;\n const resolvedTechniqueId =\n attackContext?.techniqueId ?? selectedTechniqueId;\n if (resolvedTechniqueId) {\n const technique = KoreanTechniquesSystem.getTechniqueById(resolvedTechniqueId);\n reachConfig = technique?.reachConfig;\n }\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n reachConfig,\n );\n\n const hitPosition: [number, number, number] = [\n dummyPosition[0],\n 1.5,\n dummyPosition[2],\n ];\n\n if (accuracy > 0.5) {\n const points = Math.round(accuracy * 100);\n const damage = Math.round(accuracy * 15); // 0-15 damage based on accuracy\n const isPerfect = accuracy > 0.9;\n\n if (state.isTraining) {\n actions.registerHit(points, damage, isPerfect);\n }\n\n if (playBoneImpactSound) {\n void playBoneImpactSound({\n damage,\n remainingHealth: 100, // Dummy has 100 health\n vitalPoint: false,\n hitPosition: { x: hitPosition[0], y: hitPosition[1], z: hitPosition[2] },\n });\n }\n\n let effectType: \"success\" | \"perfect\";\n if (isPerfect) {\n actions.setFeedback(\"완벽한 타격! | Perfect Strike!\");\n audio.playSFX(\"ki_release\");\n effectType = \"perfect\";\n } else if (accuracy > 0.7) {\n actions.setFeedback(\"좋은 타격! | Good Strike!\");\n audio.playSFX(\"ki_charge\");\n effectType = \"success\";\n } else {\n actions.setFeedback(\"타격 성공 | Strike Success\");\n audio.playSFX(\"menu_click\");\n effectType = \"success\";\n }\n\n actions.addHitEffect({\n position: hitPosition,\n type: effectType,\n visible: true,\n damage,\n });\n\n return true;\n } else {\n if (state.isTraining) {\n actions.registerMiss();\n }\n actions.setFeedback(\"빗나감 | Miss - Out of reach!\");\n audio.playSFX(\"menu_navigate\");\n\n actions.addHitEffect({\n position: hitPosition,\n type: \"miss\",\n visible: true,\n });\n\n return false;\n }\n },\n [\n state.isTraining,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n actions,\n audio,\n playBoneImpactSound,\n selectedTechniqueId,\n ],\n );\n\n const handleStanceChange = useCallback(\n (stanceIndex: number) => {\n actions.setStanceIndex(stanceIndex);\n const stance = TRIGRAM_STANCES_ORDER[stanceIndex];\n if (stance) {\n playerAnimation.transitionToStanceGuard(stance);\n onPlayerUpdate({ currentStance: stance });\n audio.playSFX(\"stance_change\");\n }\n },\n [actions, onPlayerUpdate, audio, playerAnimation],\n );\n\n const handleAttack = useCallback(() => {\n let techniqueToUse: KoreanTechnique | undefined;\n let techniqueId = selectedTechniqueId;\n\n if (!techniqueId) {\n const defaultTechnique = getDefaultTechniqueForArchetype(\n playerArchetype,\n playerStance,\n );\n if (defaultTechnique) {\n techniqueToUse = defaultTechnique;\n techniqueId = defaultTechnique.id;\n }\n }\n\n let animationType = currentTechniqueAnimationTypeRef.current;\n if (techniqueToUse?.animationType) {\n animationType = techniqueToUse.animationType;\n currentTechniqueAnimationTypeRef.current = animationType;\n }\n\n const startTime = performance.now() / 1000; // Current time in seconds\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n techniqueToUse?.reachConfig, // Pass technique's reachConfig for accurate reach\n );\n\n pendingAttackRef.current = {\n accuracy,\n vitalPoint: state.selectedVitalPoint ?? \"generic\",\n animationType,\n startTime,\n techniqueId, // Store resolved technique ID for handleDummyHit\n };\n\n if (setAttackAnimation && techniqueId) {\n const animationName = getAnimationForTechnique(techniqueId);\n setAttackAnimation(animationName);\n playerAnimation.transitionToAttack(\n getAnimationDurationOrFallback(animationName),\n );\n } else {\n playerAnimation.transitionToAttack(getAnimationDurationOrFallback());\n }\n\n if (!techniqueToUse && selectedTechniqueId) {\n techniqueToUse = KoreanTechniquesSystem.getTechniqueById(selectedTechniqueId);\n }\n\n if (playAttackSound) {\n const damage = techniqueToUse?.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n void playAttackSound(intensity);\n } else {\n audio.playSFX(\"whoosh\");\n }\n }, [\n state.selectedVitalPoint,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n playerAnimation,\n audio,\n playAttackSound,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n ]);\n\n return {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n };\n}\n\nexport default useTrainingActions;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useTrainingActions.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingActions.ts"],"sourcesContent":["/**\n * useTrainingActions Hook - Training Action Handlers\n *\n * Custom hook for managing training action handlers.\n * Mirrors useCombatActions pattern for consistency.\n *\n * @korean 훈련액션훅 - 훈련 액션 핸들러 관리\n */\n\nimport { useCallback, useRef } from \"react\";\nimport {\n AudioBodyRegion,\n ImpactIntensity,\n} from \"../../../../audio/types\";\nimport type { AttackIntensity } from \"../../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport {\n AnimationState,\n AnimationType,\n getAnimationDurationOrFallback,\n getAnimationForTechnique,\n} from \"../../../../systems/animation\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { getTechniquesByStance } from \"../../../../systems/trigram/techniques\";\nimport { KoreanTechniquesSystem } from \"../../../../systems/trigram/KoreanTechniques\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport type { KoreanTechnique } from \"../../../../systems/vitalpoint/types\";\nimport {\n CombatAttackType,\n DamageType,\n PlayerArchetype,\n Position,\n TrigramStance,\n} from \"../../../../types/common\";\nimport {\n DEFAULT_BODY_RADIUS_METERS,\n METERS_TO_TRAINING_UNITS,\n} from \"../../../../types/physicsConstants\";\nimport { calculateDistance3D } from \"../../../../utils/math\";\nimport { TrainingActions, TrainingScreenState } from \"./useTrainingState\";\n\nexport interface UseTrainingActionsConfig {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n readonly playerPosition: Position;\n readonly player3DPosition: [number, number, number];\n readonly dummyPosition: [number, number, number];\n readonly playerArchetype: import(\"../../../../types/common\").PlayerArchetype;\n readonly playerStance: TrigramStance;\n /** Ref to current selected technique's animation type for distance-based hit detection */\n readonly currentTechniqueAnimationTypeRef: React.MutableRefObject<AnimationType>;\n readonly audio: {\n readonly playSFX: (sound: string, volume?: number) => Promise<void>;\n };\n /** Attack sound function from useCombatAudio for playing attack whoosh sounds */\n readonly playAttackSound?: (intensity: AttackIntensity) => Promise<void>;\n /** Bone impact audio function from useCombatAudio or similar hook */\n readonly playBoneImpactSound?: (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly onPlayerUpdate: (updates: {\n currentStance?: TrigramStance;\n lastActionTime?: number;\n position?: Position;\n }) => void;\n readonly playerAnimation: {\n readonly transitionTo: (state: AnimationState) => boolean;\n readonly transitionToAttack: (durationSeconds: number) => boolean;\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n readonly currentState: string;\n };\n /** External ref to store pending attack data - shared with animation events */\n readonly pendingAttackRef: React.MutableRefObject<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>;\n /** Currently selected technique ID (from technique bar) */\n readonly selectedTechniqueId?: string;\n /** Callback to set the visual attack animation */\n readonly setAttackAnimation?: (animationName: string | undefined) => void;\n}\n\nexport interface UseTrainingActionsReturn {\n readonly handleStartTraining: () => void;\n readonly handleStopTraining: () => void;\n readonly handleDummyHit: (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean;\n readonly handleDummyDefeated: () => void;\n readonly handleStanceChange: (stanceIndex: number) => void;\n readonly handleAttack: () => void;\n}\n\n/**\n * Get the best default technique for an archetype based on current stance.\n *\n * Each archetype has preferred combat styles:\n * - MUSA: Direct strikes, joint techniques (BLUNT/JOINT damage)\n * - AMSALJA: Nerve strikes, pressure points (NERVE/PRESSURE damage)\n * - HACKER: Precise calculated strikes (high accuracy)\n * - JEONGBO_YOWON: Psychological pressure, submissions (PRESSURE/JOINT)\n * - JOJIK_POKRYEOKBAE: High damage brutal strikes\n *\n * @korean 원형별 기본 기술 선택 - 8개 자세에 따른 최적 기술\n */\nfunction getDefaultTechniqueForArchetype(\n archetype: PlayerArchetype,\n stance: TrigramStance,\n): KoreanTechnique | undefined {\n const techniques = getTechniquesByStance(stance);\n if (techniques.length === 0) return undefined;\n\n const scoredTechniques = techniques.map((tech) => {\n let score = 0;\n const damageType = tech.damageType;\n const attackType = tech.type;\n const isAdvanced =\n (tech.kiCost || 0) >= 10 || (tech.staminaCost || 0) >= 15;\n\n switch (archetype) {\n case PlayerArchetype.MUSA:\n if (damageType === DamageType.JOINT) score += 30;\n if (damageType === DamageType.CRUSHING) score += 25;\n if (damageType === DamageType.BLUNT) score += 20;\n if (attackType === CombatAttackType.STRIKE) score += 15;\n if (attackType === CombatAttackType.PUNCH) score += 10;\n if (isAdvanced) score += 10;\n break;\n\n case PlayerArchetype.AMSALJA:\n if (damageType === DamageType.NERVE) score += 35;\n if (damageType === DamageType.PRESSURE) score += 30;\n if (attackType === CombatAttackType.NERVE_STRIKE) score += 25;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 25;\n if (attackType === CombatAttackType.THRUST) score += 15;\n if ((tech.accuracy || 0) >= 0.9) score += 10;\n break;\n\n case PlayerArchetype.HACKER:\n if ((tech.accuracy || 0) >= 0.9) score += 30;\n if ((tech.accuracy || 0) >= 0.8) score += 15;\n if (damageType === DamageType.NERVE) score += 20;\n if (damageType === DamageType.INTERNAL) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n if (damageType === DamageType.PRESSURE) score += 30;\n if (damageType === DamageType.JOINT) score += 25;\n if (attackType === CombatAttackType.GRAPPLE) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n if ((tech.damage || 0) >= 35) score += 35;\n if ((tech.damage || 0) >= 30) score += 20;\n if (damageType === DamageType.SLASHING) score += 20;\n if (damageType === DamageType.PIERCING) score += 15;\n if (damageType === DamageType.CRUSHING) score += 15;\n if (attackType === CombatAttackType.KICK) score += 10;\n break;\n }\n\n return { technique: tech, score };\n });\n\n scoredTechniques.sort((a, b) => b.score - a.score);\n return scoredTechniques[0]?.technique;\n}\n\n/**\n * Calculate hit accuracy based on distance and effective reach.\n * Uses PhysicalReachCalculator for animation-aware reach calculation.\n *\n * Distance logic matches CombatSystem: if out of reach, guaranteed miss (accuracy 0).\n * Training scene coordinates are in meters, using METERS_TO_TRAINING_UNITS = 1.0.\n *\n * **IMPORTANT**: Distance calculation accounts for body radius.\n * Center-to-center distance is adjusted by subtracting target body radius\n * because attacks hit the body surface, not the center point.\n * The target body radius is calculated from physical attributes for archetypes,\n * or uses DEFAULT_BODY_RADIUS_METERS for training dummies.\n *\n * Example: If player center is 1.5m from dummy center, but dummy body\n * extends 0.23m from center, the effective distance to hit is 1.27m.\n *\n * @korean 거리 기반 명중률 계산 - 사정거리 밖이면 빗나감\n */\nfunction calculateHitAccuracy(\n playerPos: [number, number, number],\n dummyPos: [number, number, number],\n archetype: import(\"../../../../types/common\").PlayerArchetype,\n stance: TrigramStance,\n animationType?: AnimationType,\n reachConfig?: import(\"../../../../types/physics\").PhysicalReachConfig,\n): number {\n const centerToCenterDistance = calculateDistance3D(playerPos, dummyPos);\n\n const playerPhysicalAttributes = getArchetypePhysicalAttributes(archetype);\n\n const targetBodyRadius = DEFAULT_BODY_RADIUS_METERS;\n\n const effectiveDistance = Math.max(\n 0,\n centerToCenterDistance - targetBodyRadius,\n );\n\n if (animationType !== undefined) {\n const maxReachMeters = physicalReachCalculator.calculateMaxReach(\n playerPhysicalAttributes,\n animationType,\n stance,\n reachConfig, // Use technique's designed reach if provided\n );\n\n const reachInUnits = maxReachMeters * METERS_TO_TRAINING_UNITS;\n\n if (effectiveDistance > reachInUnits) {\n return 0;\n }\n\n return Math.max(0.7, 1.0 - (effectiveDistance / reachInUnits) * 0.3);\n }\n\n const defaultReach = 0.7 * METERS_TO_TRAINING_UNITS;\n if (effectiveDistance > defaultReach) {\n return 0; // Out of reach = miss\n }\n return Math.max(0.5, 1.0 - (effectiveDistance / defaultReach) * 0.5);\n}\n\n/**\n * useTrainingActions hook\n * Provides training action handlers with proper memoization\n */\nexport function useTrainingActions(\n config: UseTrainingActionsConfig,\n): UseTrainingActionsReturn {\n const {\n state,\n actions,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n audio,\n playBoneImpactSound,\n playAttackSound,\n onPlayerUpdate,\n playerAnimation,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n } = config;\n\n const dummyResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleStartTraining = useCallback(() => {\n actions.startTraining();\n audio.playSFX(\"menu_select\");\n }, [actions, audio]);\n\n const handleStopTraining = useCallback(() => {\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n dummyResetTimeoutRef.current = null;\n }\n actions.stopTraining();\n audio.playSFX(\"menu_back\");\n }, [actions, audio]);\n\n const handleDummyDefeated = useCallback(() => {\n actions.setFeedback(\"훈련 더미 무력화! | Dummy Defeated!\");\n audio.playSFX(\"ki_release\");\n\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n }\n\n dummyResetTimeoutRef.current = setTimeout(() => {\n actions.resetDummy();\n }, 2000);\n }, [actions, audio]);\n\n const handleDummyHit = useCallback(\n (\n _vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ): boolean => {\n const animationType = attackContext?.animationType;\n\n let reachConfig: import(\"../../../../types/physics\").PhysicalReachConfig | undefined;\n const resolvedTechniqueId =\n attackContext?.techniqueId ?? selectedTechniqueId;\n if (resolvedTechniqueId) {\n const technique = KoreanTechniquesSystem.getTechniqueById(resolvedTechniqueId);\n reachConfig = technique?.reachConfig;\n }\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n reachConfig,\n );\n\n const hitPosition: [number, number, number] = [\n dummyPosition[0],\n 1.5,\n dummyPosition[2],\n ];\n\n if (accuracy > 0.5) {\n const points = Math.round(accuracy * 100);\n const damage = Math.round(accuracy * 15); // 0-15 damage based on accuracy\n const isPerfect = accuracy > 0.9;\n\n if (state.isTraining) {\n actions.registerHit(points, damage, isPerfect);\n }\n\n if (playBoneImpactSound) {\n void playBoneImpactSound({\n damage,\n remainingHealth: 100, // Dummy has 100 health\n vitalPoint: false,\n hitPosition: { x: hitPosition[0], y: hitPosition[1], z: hitPosition[2] },\n });\n }\n\n let effectType: \"success\" | \"perfect\";\n if (isPerfect) {\n actions.setFeedback(\"완벽한 타격! | Perfect Strike!\");\n audio.playSFX(\"ki_release\");\n effectType = \"perfect\";\n } else if (accuracy > 0.7) {\n actions.setFeedback(\"좋은 타격! | Good Strike!\");\n audio.playSFX(\"ki_charge\");\n effectType = \"success\";\n } else {\n actions.setFeedback(\"타격 성공 | Strike Success\");\n audio.playSFX(\"menu_click\");\n effectType = \"success\";\n }\n\n actions.addHitEffect({\n position: hitPosition,\n type: effectType,\n visible: true,\n damage,\n });\n\n return true;\n } else {\n if (state.isTraining) {\n actions.registerMiss();\n }\n actions.setFeedback(\"빗나감 | Miss - Out of reach!\");\n audio.playSFX(\"menu_navigate\");\n\n actions.addHitEffect({\n position: hitPosition,\n type: \"miss\",\n visible: true,\n });\n\n return false;\n }\n },\n [\n state.isTraining,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n actions,\n audio,\n playBoneImpactSound,\n selectedTechniqueId,\n ],\n );\n\n const handleStanceChange = useCallback(\n (stanceIndex: number) => {\n actions.setStanceIndex(stanceIndex);\n const stance = TRIGRAM_STANCES_ORDER[stanceIndex];\n if (stance) {\n playerAnimation.transitionToStanceGuard(stance);\n onPlayerUpdate({ currentStance: stance });\n audio.playSFX(\"stance_change\");\n }\n },\n [actions, onPlayerUpdate, audio, playerAnimation],\n );\n\n const handleAttack = useCallback(() => {\n let techniqueToUse: KoreanTechnique | undefined;\n let techniqueId = selectedTechniqueId;\n\n if (!techniqueId) {\n const defaultTechnique = getDefaultTechniqueForArchetype(\n playerArchetype,\n playerStance,\n );\n if (defaultTechnique) {\n techniqueToUse = defaultTechnique;\n techniqueId = defaultTechnique.id;\n }\n }\n\n let animationType = currentTechniqueAnimationTypeRef.current;\n if (techniqueToUse?.animationType) {\n animationType = techniqueToUse.animationType;\n currentTechniqueAnimationTypeRef.current = animationType;\n }\n\n const startTime = performance.now() / 1000; // Current time in seconds\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n techniqueToUse?.reachConfig, // Pass technique's reachConfig for accurate reach\n );\n\n pendingAttackRef.current = {\n accuracy,\n vitalPoint: state.selectedVitalPoint ?? \"generic\",\n animationType,\n startTime,\n techniqueId, // Store resolved technique ID for handleDummyHit\n };\n\n if (setAttackAnimation && techniqueId) {\n const animationName = getAnimationForTechnique(techniqueId);\n setAttackAnimation(animationName);\n playerAnimation.transitionToAttack(\n getAnimationDurationOrFallback(animationName),\n );\n } else {\n playerAnimation.transitionToAttack(getAnimationDurationOrFallback());\n }\n\n if (!techniqueToUse && selectedTechniqueId) {\n techniqueToUse = KoreanTechniquesSystem.getTechniqueById(selectedTechniqueId);\n }\n\n if (playAttackSound) {\n const damage = techniqueToUse?.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n void playAttackSound(intensity);\n } else {\n audio.playSFX(\"whoosh\");\n }\n }, [\n state.selectedVitalPoint,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n playerAnimation,\n audio,\n playAttackSound,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n ]);\n\n return {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n };\n}\n\nexport default useTrainingActions;\n"],"mappings":"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"}
@@ -65,7 +65,8 @@ var SingleFeedback = ({ feedback, isMobile, arenaBounds, animationDuration }) =>
65
65
  ];
66
66
  useFrame(() => {
67
67
  const elapsed = Date.now() - startTimeRef.current;
68
- setProgress(Math.min(elapsed / animationDuration, 1));
68
+ const newProgress = Math.min(elapsed / animationDuration, 1);
69
+ setProgress(newProgress);
69
70
  });
70
71
  if (progress >= 1) return null;
71
72
  const opacity = 1 - progress;
@@ -1 +1 @@
1
- {"version":3,"file":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n setOpacity(1);\n setScale(1);\n } else {\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n setOpacity(1);\n setScale(1);\n } else {\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}