blacktrigram 0.7.67 → 0.7.69

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 (219) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/assets/index.css +987 -987
  3. package/lib/audio/AudioAssetLoader.js.map +1 -1
  4. package/lib/audio/AudioAssetRegistry.js +2 -2
  5. package/lib/audio/AudioAssetRegistry.js.map +1 -1
  6. package/lib/audio/AudioCache.js.map +1 -1
  7. package/lib/audio/AudioManager.js +1 -1
  8. package/lib/audio/AudioManager.js.map +1 -1
  9. package/lib/audio/AudioMonitor.js.map +1 -1
  10. package/lib/audio/AudioPool.js.map +1 -1
  11. package/lib/audio/AudioProvider.js.map +1 -1
  12. package/lib/audio/AudioUtils.js.map +1 -1
  13. package/lib/audio/BoneImpactAudioMap.js.map +1 -1
  14. package/lib/components/screens/combat/CombatScreen3D.js +3 -3
  15. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  18. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  19. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  20. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  25. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  26. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  27. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  28. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  29. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  30. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  31. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  32. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  33. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  34. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  35. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  36. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  37. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  38. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  39. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  40. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  41. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  42. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  43. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  44. package/lib/components/screens/training/TrainingScreen3D.js +1 -1
  45. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  46. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  47. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  48. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  49. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  50. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  51. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  52. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  53. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  54. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  55. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  56. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  57. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  58. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  59. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  60. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  61. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  62. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  63. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  64. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  65. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  66. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  67. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  68. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  69. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  70. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  71. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  72. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  73. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  74. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  75. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  76. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  77. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  78. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  79. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  80. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  81. package/lib/components/shared/ui/SplashScreen.js +2 -2
  82. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  83. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  84. package/lib/data/techniques.js.map +1 -1
  85. package/lib/hooks/useCombatTimer.js.map +1 -1
  86. package/lib/hooks/useKeyboardControls.js.map +1 -1
  87. package/lib/hooks/useSkeletalAnimation.js +2 -2
  88. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  89. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  90. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  91. package/lib/index.js +2 -2
  92. package/lib/systems/CombatSystem.js.map +1 -1
  93. package/lib/systems/EffectCalculator.js.map +1 -1
  94. package/lib/systems/ResponsiveScaling.js.map +1 -1
  95. package/lib/systems/TrigramSystem.js.map +1 -1
  96. package/lib/systems/ai/AIPersonality.js.map +1 -1
  97. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  98. package/lib/systems/ai/DecisionTree.js.map +1 -1
  99. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  100. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  101. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  102. package/lib/systems/animation/builders/SkeletonRig.js +1 -1
  103. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  104. package/lib/systems/animation/catalogs/AttackAnimations.js +1 -1
  105. package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
  106. package/lib/systems/animation/catalogs/BasicAnimations.js +1 -1
  107. package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
  108. package/lib/systems/animation/catalogs/ComboAnimations.js +1 -1
  109. package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
  110. package/lib/systems/animation/catalogs/DarkOpsAnimations.js +1 -1
  111. package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
  112. package/lib/systems/animation/catalogs/DefensiveAnimations.js +2 -11
  113. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  114. package/lib/systems/animation/catalogs/ElbowKneeAnimations.js +1 -1
  115. package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
  116. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js +1 -1
  117. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  118. package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
  119. package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -1
  120. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
  121. package/lib/systems/animation/catalogs/GanStanceAnimations.js +1 -1
  122. package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
  123. package/lib/systems/animation/catalogs/GanTechniqueAnimations.js +1 -1
  124. package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
  125. package/lib/systems/animation/catalogs/GeonStanceAnimations.js +1 -1
  126. package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
  127. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +1 -1
  128. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -1
  129. package/lib/systems/animation/catalogs/GrapplingAnimations.js +1 -1
  130. package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
  131. package/lib/systems/animation/catalogs/JinStanceAnimations.js +1 -1
  132. package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
  133. package/lib/systems/animation/catalogs/JinTechniqueAnimations.js +1 -1
  134. package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
  135. package/lib/systems/animation/catalogs/KickAnimations.js +1 -1
  136. package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
  137. package/lib/systems/animation/catalogs/LiStanceAnimations.js +1 -1
  138. package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
  139. package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
  140. package/lib/systems/animation/catalogs/MovementAnimations.js +1 -1
  141. package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
  142. package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
  143. package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
  144. package/lib/systems/animation/catalogs/SonStanceAnimations.js +1 -1
  145. package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
  146. package/lib/systems/animation/catalogs/SonTechniqueAnimations.js +1 -1
  147. package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
  148. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js +1 -1
  149. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
  150. package/lib/systems/animation/catalogs/StanceAnimations.js +1 -1
  151. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  152. package/lib/systems/animation/catalogs/StanceAttackAnimations.js +215 -272
  153. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  154. package/lib/systems/animation/catalogs/StanceIdleAnimations.js +2 -11
  155. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  156. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js +2 -2
  157. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  158. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js +1 -1
  159. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  160. package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
  161. package/lib/systems/animation/catalogs/TaeStanceAnimations.js +1 -1
  162. package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
  163. package/lib/systems/animation/core/AnimationOptimizations.js +1 -1
  164. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  165. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  166. package/lib/systems/animation/core/AnimationRegistry.js +11 -209
  167. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  168. package/lib/systems/animation/core/AnimationStateMachine.js +2 -2
  169. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  170. package/lib/systems/animation/core/TechniqueAnimationMapper.js +1 -1
  171. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  172. package/lib/systems/animation/core/TechniqueAnimationMapping.js +1 -1
  173. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  174. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  175. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  176. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  177. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  178. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  179. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  180. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  181. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  182. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  183. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  184. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  185. package/lib/systems/combat/typeGuards.js.map +1 -1
  186. package/lib/systems/index.js +2 -2
  187. package/lib/systems/movement/integration.js.map +1 -1
  188. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  189. package/lib/systems/physics/CollisionDetection.js +1 -1
  190. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  191. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  192. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  193. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  194. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  195. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  196. package/lib/systems/trigram/StanceManager.js.map +1 -1
  197. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  198. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  199. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  200. package/lib/systems/trigram/techniques/index.js.map +1 -1
  201. package/lib/systems/trigram/types.js.map +1 -1
  202. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  203. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  204. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  205. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  206. package/lib/types/constants/designSystem.js.map +1 -1
  207. package/lib/utils/colorHelpers.js.map +1 -1
  208. package/lib/utils/colorUtils.js.map +1 -1
  209. package/lib/utils/controlMapping.js.map +1 -1
  210. package/lib/utils/deviceDetection.js.map +1 -1
  211. package/lib/utils/effectUtils.js.map +1 -1
  212. package/lib/utils/fabricTextures.js.map +1 -1
  213. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  214. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  215. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  216. package/lib/utils/safeAreaUtils.js.map +1 -1
  217. package/lib/utils/stanceHelpers.js.map +1 -1
  218. package/lib/utils/visualEffects.js.map +1 -1
  219. package/package.json +9 -9
@@ -1 +1 @@
1
- {"version":3,"file":"VitalPointMarker3D.js","names":[],"sources":["../../../../../src/components/screens/training/components/VitalPointMarker3D.tsx"],"sourcesContent":["/**\n * VitalPointMarker3D - Individual vital point marker with hover labels\n *\n * Provides interactive 3D markers for vital points with Korean-English bilingual labels\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport * as THREE from \"three\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport {\n FONT_FAMILY,\n KOREAN_COLORS,\n UI_DIMENSIONS,\n} from \"../../../../types/constants\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../../utils/htmlOverlayHelpers\";\n\n/**\n * Props for VitalPointMarker3D component\n */\nexport interface VitalPointMarker3DProps {\n /** The vital point data to visualize */\n readonly vitalPoint: VitalPoint;\n /** Whether this vital point is currently selected */\n readonly isSelected: boolean;\n /** Whether training mode is active */\n readonly isTraining: boolean;\n /** Whether on mobile device (larger hit targets) */\n readonly isMobile?: boolean;\n /** Callback when vital point is clicked/hit */\n readonly onHit?: (vitalPointId: string) => void;\n /** Base size multiplier (for difficulty modes) */\n readonly sizeMultiplier?: number;\n /** Pulse frequency in Hz (default: 6Hz for selected, 4Hz for training mode) */\n readonly pulseFrequency?: number;\n /** Pulse amplitude (default: 0.25 for selected, 0.15 for training mode) */\n readonly pulseAmplitude?: number;\n /** Maximum emissive intensity for selected/hovered state (default: 3.5) */\n readonly maxEmissiveIntensity?: number;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.POSITIVE_GREEN;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.WARNING_YELLOW;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.NEGATIVE_RED; // Most severe - red for lethal vital points\n default:\n return KOREAN_COLORS.TEXT_SECONDARY;\n }\n};\n\n/**\n * Base scale offset for pulsing animation\n * This value (1.15) ensures the marker pulses around a slightly enlarged baseline,\n * making the pulsing effect more visible without excessive size variation.\n * Used in useFrame hook for scale calculation (see line ~180).\n */\nconst PULSE_BASE_SCALE = 1.15;\n\n/**\n * VitalPointMarker3D Component\n * Individual 3D marker with hover tooltip\n */\nexport const VitalPointMarker3D: React.FC<VitalPointMarker3DProps> = ({\n vitalPoint,\n isSelected,\n isTraining,\n isMobile = false,\n onHit,\n sizeMultiplier = 1.0,\n pulseFrequency,\n pulseAmplitude,\n maxEmissiveIntensity = 3.5,\n}) => {\n const defaultPulseFrequency = isTraining && !isSelected ? 4 : 6;\n const defaultPulseAmplitude = isTraining && !isSelected ? 0.15 : 0.25;\n \n const activePulseFrequency = pulseFrequency ?? defaultPulseFrequency;\n const activePulseAmplitude = pulseAmplitude ?? defaultPulseAmplitude;\n const meshRef = useRef<THREE.Mesh>(null);\n const [hovered, setHovered] = useState(false);\n\n const baseSize = isMobile ? 0.15 : 0.1;\n const markerSize = baseSize * sizeMultiplier;\n\n const targetScale = useMemo(() => new THREE.Vector3(1, 1, 1), []);\n\n useFrame((state) => {\n if (!meshRef.current) return;\n\n if (isSelected || hovered) {\n const pulse = Math.sin(state.clock.elapsedTime * activePulseFrequency) * activePulseAmplitude + PULSE_BASE_SCALE;\n meshRef.current.scale.setScalar(pulse);\n } else {\n targetScale.set(1, 1, 1);\n meshRef.current.scale.lerp(targetScale, 0.1);\n }\n });\n\n const color = useMemo(\n () => getSeverityColor(vitalPoint.severity),\n [vitalPoint.severity]\n );\n\n const markerMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissive: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissiveIntensity: isSelected || hovered ? maxEmissiveIntensity : 2.0,\n metalness: 0.9, // Increased metalness for more reflective appearance (was 0.8)\n roughness: 0.1, // Reduced roughness for stronger reflections (was 0.2)\n clearcoat: 1.0,\n clearcoatRoughness: 0.05, // Reduced for sharper clearcoat (was 0.1)\n transparent: true,\n opacity: isTraining ? 0.9 : 0.6,\n transmission: 0.1, // Slight transmission for glass-like effect\n thickness: 0.2,\n ior: 2.4, // High IOR for gem-like appearance\n sheen: 0.3, // Increased sheen\n sheenRoughness: 0.1,\n }),\n [isSelected, hovered, color, maxEmissiveIntensity, isTraining]\n );\n\n useEffect(() => {\n return () => {\n markerMaterial.dispose();\n };\n }, [markerMaterial]);\n\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n const tooltipDistanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"text\", isMobile);\n }, [screenWidth, isMobile]);\n\n const tooltipOverlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(\n \"tooltip\",\n false,\n tooltipDistanceFactor,\n true,\n false\n );\n }, [tooltipDistanceFactor]);\n\n const handleClick = useCallback(() => {\n if (isTraining && onHit) {\n onHit(vitalPoint.id);\n }\n }, [isTraining, onHit, vitalPoint.id]);\n\n return (\n <group>\n {/* Hit target sphere\n Note: Three.js 3D objects lack standard DOM accessibility (aria-label, role, etc.).\n For accessible alternatives, see keyboard shortcuts documented in the UI and\n consider future enhancements for screen reader support via Html overlays. */}\n <mesh\n ref={meshRef}\n onClick={handleClick}\n onPointerOver={() => setHovered(true)}\n onPointerOut={() => setHovered(false)}\n name={`vital-point-marker-${vitalPoint.id}`}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <primitive object={markerMaterial} attach=\"material\" />\n </mesh>\n\n {/* Ring indicator for selected marker */}\n {isSelected && (\n <mesh rotation={[Math.PI / 2, 0, 0]}>\n <ringGeometry args={[markerSize * 1.2, markerSize * 1.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Hover tooltip with Korean-English labels */}\n {hovered && (\n <Html\n position={[0, markerSize + 0.2, 0]}\n center={tooltipOverlayStyle.center}\n distanceFactor={tooltipOverlayStyle.distanceFactor}\n occlude={tooltipOverlayStyle.occlude}\n style={{ pointerEvents: tooltipOverlayStyle.pointerEvents }}\n >\n <div\n style={{\n background: \"rgba(0, 0, 0, 0.9)\",\n border: `2px solid ${isSelected ? \"#ffd700\" : \"#00ffff\"}`,\n borderRadius: \"8px\",\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n boxShadow: \"0 0 15px rgba(0, 255, 255, 0.5)\",\n transform: tooltipOverlayStyle.transform,\n zIndex: tooltipOverlayStyle.zIndex,\n }}\n data-testid={`vital-point-tooltip-${vitalPoint.id}`}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n color: \"#ffd700\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: \"#00ffff\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.english}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: \"#999999\",\n fontStyle: \"italic\",\n }}\n >\n {vitalPoint.names.romanized}\n </div>\n\n {/* Severity indicator */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: `#${new THREE.Color(color).getHexString()}`,\n marginTop: \"6px\",\n borderTop: \"1px solid rgba(255, 255, 255, 0.2)\",\n paddingTop: \"4px\",\n }}\n >\n 심각도 | {vitalPoint.severity}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\nexport default VitalPointMarker3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"VitalPointMarker3D.js","names":[],"sources":["../../../../../src/components/screens/training/components/VitalPointMarker3D.tsx"],"sourcesContent":["/**\n * VitalPointMarker3D - Individual vital point marker with hover labels\n *\n * Provides interactive 3D markers for vital points with Korean-English bilingual labels\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport * as THREE from \"three\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport {\n FONT_FAMILY,\n KOREAN_COLORS,\n UI_DIMENSIONS,\n} from \"../../../../types/constants\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../../utils/htmlOverlayHelpers\";\n\n/**\n * Props for VitalPointMarker3D component\n */\nexport interface VitalPointMarker3DProps {\n /** The vital point data to visualize */\n readonly vitalPoint: VitalPoint;\n /** Whether this vital point is currently selected */\n readonly isSelected: boolean;\n /** Whether training mode is active */\n readonly isTraining: boolean;\n /** Whether on mobile device (larger hit targets) */\n readonly isMobile?: boolean;\n /** Callback when vital point is clicked/hit */\n readonly onHit?: (vitalPointId: string) => void;\n /** Base size multiplier (for difficulty modes) */\n readonly sizeMultiplier?: number;\n /** Pulse frequency in Hz (default: 6Hz for selected, 4Hz for training mode) */\n readonly pulseFrequency?: number;\n /** Pulse amplitude (default: 0.25 for selected, 0.15 for training mode) */\n readonly pulseAmplitude?: number;\n /** Maximum emissive intensity for selected/hovered state (default: 3.5) */\n readonly maxEmissiveIntensity?: number;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.POSITIVE_GREEN;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.WARNING_YELLOW;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.NEGATIVE_RED; // Most severe - red for lethal vital points\n default:\n return KOREAN_COLORS.TEXT_SECONDARY;\n }\n};\n\n/**\n * Base scale offset for pulsing animation\n * This value (1.15) ensures the marker pulses around a slightly enlarged baseline,\n * making the pulsing effect more visible without excessive size variation.\n * Used in useFrame hook for scale calculation (see line ~180).\n */\nconst PULSE_BASE_SCALE = 1.15;\n\n/**\n * VitalPointMarker3D Component\n * Individual 3D marker with hover tooltip\n */\nexport const VitalPointMarker3D: React.FC<VitalPointMarker3DProps> = ({\n vitalPoint,\n isSelected,\n isTraining,\n isMobile = false,\n onHit,\n sizeMultiplier = 1.0,\n pulseFrequency,\n pulseAmplitude,\n maxEmissiveIntensity = 3.5,\n}) => {\n const defaultPulseFrequency = isTraining && !isSelected ? 4 : 6;\n const defaultPulseAmplitude = isTraining && !isSelected ? 0.15 : 0.25;\n \n const activePulseFrequency = pulseFrequency ?? defaultPulseFrequency;\n const activePulseAmplitude = pulseAmplitude ?? defaultPulseAmplitude;\n const meshRef = useRef<THREE.Mesh>(null);\n const [hovered, setHovered] = useState(false);\n\n const baseSize = isMobile ? 0.15 : 0.1;\n const markerSize = baseSize * sizeMultiplier;\n\n const targetScale = useMemo(() => new THREE.Vector3(1, 1, 1), []);\n\n useFrame((state) => {\n if (!meshRef.current) return;\n\n if (isSelected || hovered) {\n const pulse = Math.sin(state.clock.elapsedTime * activePulseFrequency) * activePulseAmplitude + PULSE_BASE_SCALE;\n meshRef.current.scale.setScalar(pulse);\n } else {\n targetScale.set(1, 1, 1);\n meshRef.current.scale.lerp(targetScale, 0.1);\n }\n });\n\n const color = useMemo(\n () => getSeverityColor(vitalPoint.severity),\n [vitalPoint.severity]\n );\n\n const markerMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissive: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissiveIntensity: isSelected || hovered ? maxEmissiveIntensity : 2.0,\n metalness: 0.9, // Increased metalness for more reflective appearance (was 0.8)\n roughness: 0.1, // Reduced roughness for stronger reflections (was 0.2)\n clearcoat: 1.0,\n clearcoatRoughness: 0.05, // Reduced for sharper clearcoat (was 0.1)\n transparent: true,\n opacity: isTraining ? 0.9 : 0.6,\n transmission: 0.1, // Slight transmission for glass-like effect\n thickness: 0.2,\n ior: 2.4, // High IOR for gem-like appearance\n sheen: 0.3, // Increased sheen\n sheenRoughness: 0.1,\n }),\n [isSelected, hovered, color, maxEmissiveIntensity, isTraining]\n );\n\n useEffect(() => {\n return () => {\n markerMaterial.dispose();\n };\n }, [markerMaterial]);\n\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n const tooltipDistanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"text\", isMobile);\n }, [screenWidth, isMobile]);\n\n const tooltipOverlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(\n \"tooltip\",\n false,\n tooltipDistanceFactor,\n true,\n false\n );\n }, [tooltipDistanceFactor]);\n\n const handleClick = useCallback(() => {\n if (isTraining && onHit) {\n onHit(vitalPoint.id);\n }\n }, [isTraining, onHit, vitalPoint.id]);\n\n return (\n <group>\n {/* Hit target sphere\n Note: Three.js 3D objects lack standard DOM accessibility (aria-label, role, etc.).\n For accessible alternatives, see keyboard shortcuts documented in the UI and\n consider future enhancements for screen reader support via Html overlays. */}\n <mesh\n ref={meshRef}\n onClick={handleClick}\n onPointerOver={() => setHovered(true)}\n onPointerOut={() => setHovered(false)}\n name={`vital-point-marker-${vitalPoint.id}`}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <primitive object={markerMaterial} attach=\"material\" />\n </mesh>\n\n {/* Ring indicator for selected marker */}\n {isSelected && (\n <mesh rotation={[Math.PI / 2, 0, 0]}>\n <ringGeometry args={[markerSize * 1.2, markerSize * 1.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Hover tooltip with Korean-English labels */}\n {hovered && (\n <Html\n position={[0, markerSize + 0.2, 0]}\n center={tooltipOverlayStyle.center}\n distanceFactor={tooltipOverlayStyle.distanceFactor}\n occlude={tooltipOverlayStyle.occlude}\n style={{ pointerEvents: tooltipOverlayStyle.pointerEvents }}\n >\n <div\n style={{\n background: \"rgba(0, 0, 0, 0.9)\",\n border: `2px solid ${isSelected ? \"#ffd700\" : \"#00ffff\"}`,\n borderRadius: \"8px\",\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n boxShadow: \"0 0 15px rgba(0, 255, 255, 0.5)\",\n transform: tooltipOverlayStyle.transform,\n zIndex: tooltipOverlayStyle.zIndex,\n }}\n data-testid={`vital-point-tooltip-${vitalPoint.id}`}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n color: \"#ffd700\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: \"#00ffff\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.english}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: \"#999999\",\n fontStyle: \"italic\",\n }}\n >\n {vitalPoint.names.romanized}\n </div>\n\n {/* Severity indicator */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: `#${new THREE.Color(color).getHexString()}`,\n marginTop: \"6px\",\n borderTop: \"1px solid rgba(255, 255, 255, 0.2)\",\n paddingTop: \"4px\",\n }}\n >\n 심각도 | {vitalPoint.severity}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\nexport default VitalPointMarker3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuDA,IAAM,oBAAoB,aAAyC;CACjE,QAAQ,UAAR;EACE,KAAK,mBAAmB,OACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,UACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,OACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,UACtB,OAAO,cAAc;EACvB,KAAK,mBAAmB,QACtB,OAAO,cAAc;EACvB,SACE,OAAO,cAAc;CACzB;AACF;;;;;;;AAQA,IAAM,mBAAmB;;;;;AAMzB,IAAa,sBAAyD,EACpE,YACA,YACA,YACA,WAAW,OACX,OACA,iBAAiB,GACjB,gBACA,gBACA,uBAAuB,UACnB;CACJ,MAAM,wBAAwB,cAAc,CAAC,aAAa,IAAI;CAC9D,MAAM,wBAAwB,cAAc,CAAC,aAAa,MAAO;CAEjE,MAAM,uBAAuB,kBAAkB;CAC/C,MAAM,uBAAuB,kBAAkB;CAC/C,MAAM,UAAU,OAAmB,IAAI;CACvC,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAG5C,MAAM,cADW,WAAW,MAAO,MACL;CAE9B,MAAM,cAAc,cAAc,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;CAEhE,UAAU,UAAU;EAClB,IAAI,CAAC,QAAQ,SAAS;EAEtB,IAAI,cAAc,SAAS;GACzB,MAAM,QAAQ,KAAK,IAAI,MAAM,MAAM,cAAc,oBAAoB,IAAI,uBAAuB;GAChG,QAAQ,QAAQ,MAAM,UAAU,KAAK;EACvC,OAAO;GACL,YAAY,IAAI,GAAG,GAAG,CAAC;GACvB,QAAQ,QAAQ,MAAM,KAAK,aAAa,EAAG;EAC7C;CACF,CAAC;CAED,MAAM,QAAQ,cACN,iBAAiB,WAAW,QAAQ,GAC1C,CAAC,WAAW,QAAQ,CACtB;CAEA,MAAM,iBAAiB,cAEnB,IAAI,MAAM,qBAAqB;EAC7B,OAAO,aAAa,cAAc,cAAc;EAChD,UAAU,aAAa,cAAc,cAAc;EACnD,mBAAmB,cAAc,UAAU,uBAAuB;EAClE,WAAW;EACX,WAAW;EACX,WAAW;EACX,oBAAoB;EACpB,aAAa;EACb,SAAS,aAAa,KAAM;EAC5B,cAAc;EACd,WAAW;EACX,KAAK;EACL,OAAO;EACP,gBAAgB;CAClB,CAAC,GACH;EAAC;EAAY;EAAS;EAAO;EAAsB;CAAU,CAC/D;CAEA,gBAAgB;EACd,aAAa;GACX,eAAe,QAAQ;EACzB;CACF,GAAG,CAAC,cAAc,CAAC;CAEnB,MAAM,CAAC,aAAa,kBAAkB,eACpC,OAAO,WAAW,cACd,OAAO,aACP,cAAc,oBACpB;CAEA,gBAAgB;EACd,IAAI,OAAO,WAAW,aAAa;EAEnC,MAAM,qBAAqB;GACzB,eAAe,OAAO,UAAU;EAClC;EAEA,OAAO,iBAAiB,UAAU,YAAY;EAC9C,aAAa,OAAO,oBAAoB,UAAU,YAAY;CAChE,GAAG,CAAC,CAAC;CAEL,MAAM,wBAAwB,cAAc;EAC1C,OAAO,wBAAwB,aAAa,QAAQ,QAAQ;CAC9D,GAAG,CAAC,aAAa,QAAQ,CAAC;CAE1B,MAAM,sBAAsB,cAAc;EACxC,OAAO,uBACL,WACA,OACA,uBACA,MACA,KACF;CACF,GAAG,CAAC,qBAAqB,CAAC;CAQ1B,OACE,qBAAC,SAAD,EAAA,UAAA;EAKE,qBAAC,QAAD;GACE,KAAK;GACL,SAdc,kBAAkB;IACpC,IAAI,cAAc,OAChB,MAAM,WAAW,EAAE;GAEvB,GAAG;IAAC;IAAY;IAAO,WAAW;GAAE,CAUrB;GACT,qBAAqB,WAAW,IAAI;GACpC,oBAAoB,WAAW,KAAK;GACpC,MAAM,sBAAsB,WAAW;aALzC,CAOE,oBAAC,kBAAD,EAAgB,MAAM;IAAC;IAAY;IAAI;GAAE,EAAI,CAAA,GAC7C,oBAAC,aAAD;IAAW,QAAQ;IAAgB,QAAO;GAAY,CAAA,CAClD;;EAGL,cACC,qBAAC,QAAD;GAAM,UAAU;IAAC,KAAK,KAAK;IAAG;IAAG;GAAC;aAAlC,CACE,oBAAC,gBAAD,EAAc,MAAM;IAAC,aAAa;IAAK,aAAa;IAAK;GAAE,EAAI,CAAA,GAC/D,oBAAC,qBAAD;IACE,OAAO,cAAc;IACrB,aAAA;IACA,SAAS;IACT,MAAM,MAAM;GACb,CAAA,CACG;;EAIP,WACC,oBAAC,MAAD;GACE,UAAU;IAAC;IAAG,aAAa;IAAK;GAAC;GACjC,QAAQ,oBAAoB;GAC5B,gBAAgB,oBAAoB;GACpC,SAAS,oBAAoB;GAC7B,OAAO,EAAE,eAAe,oBAAoB,cAAc;aAE1D,qBAAC,OAAD;IACE,OAAO;KACL,YAAY;KACZ,QAAQ,aAAa,aAAa,YAAY;KAC9C,cAAc;KACd,SAAS,WAAW,aAAa;KACjC,YAAY,YAAY;KACxB,YAAY;KACZ,WAAW;KACX,WAAW,oBAAoB;KAC/B,QAAQ,oBAAoB;IAC9B;IACA,eAAa,uBAAuB,WAAW;cAZjD;KAeE,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,WAAW,SAAS;OAC9B,YAAY;OACZ,OAAO;OACP,cAAc;MAChB;gBAEC,WAAW,MAAM;KACf,CAAA;KAGL,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,WAAW,SAAS;OAC9B,OAAO;OACP,cAAc;MAChB;gBAEC,WAAW,MAAM;KACf,CAAA;KAGL,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,WAAW,QAAQ;OAC7B,OAAO;OACP,WAAW;MACb;gBAEC,WAAW,MAAM;KACf,CAAA;KAGL,qBAAC,OAAD;MACE,OAAO;OACL,UAAU,WAAW,QAAQ;OAC7B,OAAO,IAAI,IAAI,MAAM,MAAM,KAAK,CAAC,CAAC,aAAa;OAC/C,WAAW;OACX,WAAW;OACX,YAAY;MACd;gBAPF,CAQC,UACQ,WAAW,QACf;;IACF;;EACD,CAAA;CAEH,EAAA,CAAA;AAEX"}
@@ -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,EAAE,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;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 +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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ActionButtons.js","names":[],"sources":["../../../../src/components/shared/mobile/ActionButtons.tsx"],"sourcesContent":["/**\n * ActionButtons Component\n *\n * Touch-optimized action buttons for combat (Attack and Block)\n * Provides tactile combat controls with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for screen readers\n * - Keyboard navigation (Enter, Space)\n * - Visible focus indicators (2px cyan border)\n * - 80x80px and 70x70px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/ActionButtons\n * @category Mobile Controls\n * @korean 액션 버튼\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, {\n useCallback,\n useState,\n useMemo,\n useRef,\n useEffect,\n} from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerOptimizedHaptic } from \"./HapticController\";\nimport {\n applyOptimizedUpdate,\n createTransformStyle,\n createFilterStyle,\n} from \"./TouchOptimizer\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * Event type for button interactions\n */\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for ActionButtons component\n */\nexport interface ActionButtonsProps {\n /** Callback when attack button is pressed */\n readonly onAttack: () => void;\n /** Callback when block button is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether buttons are disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from right in pixels (default: 20) */\n readonly right?: number;\n /** Opacity of buttons (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * ActionButtons Component\n *\n * Provides two primary combat action buttons:\n * - Attack Button (⚡): Primary combat action, 80x80px\n * - Block Button (🛡️): Defensive action, 70x70px\n *\n * Features:\n * - Touch-optimized with minimum 44x44px targets\n * - Attack button: 80x80px for primary action\n * - Block button: 70x70px for secondary action\n * - Visual feedback on press\n * - Haptic feedback for tactile response\n * - Korean cyberpunk theming\n * - Hold-to-block support\n *\n * Usage in Combat:\n * - Attack: Executes current stance technique\n * - Block: Activates defensive guard (hold for sustained block)\n *\n * @example\n * ```tsx\n * <ActionButtons\n * onAttack={() => executeTechnique()}\n * onBlock={(eventType) => {\n * if (eventType === 'start') {\n * activateBlock();\n * } else {\n * deactivateBlock();\n * }\n * }}\n * disabled={isPaused}\n * />\n * ```\n *\n * @korean 액션버튼\n */\nconst ActionButtonsComponent: React.FC<ActionButtonsProps> = ({\n onAttack,\n onBlock,\n disabled = false,\n bottom = 34,\n right = 20,\n opacity = 0.8,\n}) => {\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n const [attackFocused, setAttackFocused] = useState(false);\n const [blockFocused, setBlockFocused] = useState(false);\n\n const attackButtonRef = useRef<HTMLButtonElement>(null);\n const blockButtonRef = useRef<HTMLButtonElement>(null);\n\n const throttledOnAttack = useThrottle(onAttack, 16);\n const throttledOnBlock = useThrottle(onBlock, 16);\n\n /**\n * Handle attack button press with optimized latency (<16ms)\n * Uses direct DOM manipulation for immediate visual feedback\n */\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n setAttackPressed(true);\n throttledOnAttack();\n triggerOptimizedHaptic(\"medium\");\n },\n );\n },\n [disabled, throttledOnAttack],\n );\n\n /**\n * Handle attack button release with optimized latency\n */\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setAttackPressed(false);\n },\n );\n },\n [disabled],\n );\n\n /**\n * Handle block button press with optimized latency (<16ms)\n */\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n setBlockPressed(true);\n throttledOnBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Handle block button release with optimized latency\n */\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setBlockPressed(false);\n throttledOnBlock(\"end\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Cleanup on unmount - reset any pending visual states.\n * Note: Captures button refs at effect creation time to avoid stale closures.\n * If the buttons unmount before cleanup runs, the captured variables will still\n * reference the original DOM elements (now potentially detached), so style changes\n * are harmless but may not be visible. The null checks primarily guard against\n * refs that were never set in the first place.\n */\n useEffect(() => {\n const attackButton = attackButtonRef.current;\n const blockButton = blockButtonRef.current;\n\n return () => {\n if (attackButton) {\n attackButton.style.transform = createTransformStyle(false);\n attackButton.style.filter = createFilterStyle(false);\n }\n if (blockButton) {\n blockButton.style.transform = createTransformStyle(false);\n blockButton.style.filter = createFilterStyle(false);\n }\n };\n }, []);\n\n /**\n * Handle keyboard navigation for attack button\n */\n const handleAttackKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setAttackPressed(true);\n onAttack();\n triggerOptimizedHaptic(\"medium\");\n setTimeout(() => setAttackPressed(false), 150);\n },\n });\n },\n [disabled, onAttack],\n );\n\n /**\n * Handle keyboard navigation for block button\n */\n const handleBlockKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setBlockPressed(true);\n onBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n setTimeout(() => {\n setBlockPressed(false);\n onBlock(\"end\");\n }, 150);\n },\n });\n },\n [disabled, onBlock],\n );\n\n const colors = useMemo(\n () => ({\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n blue: getColorRGB(KOREAN_COLORS.ACCENT_BLUE),\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n right: `${right}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"10px\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"action-buttons\"\n >\n {/* Primary Attack Button */}\n <button\n ref={attackButtonRef}\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n onKeyDown={handleAttackKeyDown}\n onFocus={() => setAttackFocused(true)}\n onBlur={() => setAttackFocused(false)}\n style={{\n width: \"80px\",\n height: \"80px\",\n borderRadius: \"50%\",\n background: attackPressed\n ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`\n : `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`,\n border: \"3px solid #fff\",\n fontSize: \"28px\",\n color: \"#000\",\n fontWeight: \"bold\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n transform: createTransformStyle(attackPressed, 0.95),\n filter: createFilterStyle(attackPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: attackPressed\n ? `0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 12px rgba(0, 0, 0, 0.5), 0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.6)`,\n ...getFocusStyle(attackFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"공격\", \"Attack\").label}\n aria-pressed={attackPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button */}\n <button\n ref={blockButtonRef}\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n onKeyDown={handleBlockKeyDown}\n onFocus={() => setBlockFocused(true)}\n onBlur={() => setBlockFocused(false)}\n style={{\n width: \"70px\",\n height: \"70px\",\n borderRadius: \"50%\",\n background: blockPressed\n ? `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`\n : `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.9)`,\n border: \"2px solid #fff\",\n fontSize: \"24px\",\n color: \"#fff\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n transform: createTransformStyle(blockPressed, 0.95),\n filter: createFilterStyle(blockPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: blockPressed\n ? `0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 10px rgba(0, 0, 0, 0.5), 0 0 12px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.6)`,\n ...getFocusStyle(blockFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"방어\", \"Block\").label}\n aria-pressed={blockPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"block-button\"\n >\n 🛡️\n </button>\n\n {/* Button Labels (Korean + English) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n alignItems: \"center\",\n fontSize: \"10px\",\n color: `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n marginTop: \"4px\",\n }}\n >\n <span>공격 | Attack</span>\n <span style={{ fontSize: \"9px\" }}>방어 | Block</span>\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized ActionButtons with custom comparison\n * Only re-renders when props change\n */\nexport const ActionButtons = React.memo(\n ActionButtonsComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.right === nextProps.right &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onAttack === nextProps.onAttack &&\n prevProps.onBlock === nextProps.onBlock\n );\n },\n);\n\nActionButtons.displayName = \"ActionButtons\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"ActionButtons.js","names":[],"sources":["../../../../src/components/shared/mobile/ActionButtons.tsx"],"sourcesContent":["/**\n * ActionButtons Component\n *\n * Touch-optimized action buttons for combat (Attack and Block)\n * Provides tactile combat controls with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for screen readers\n * - Keyboard navigation (Enter, Space)\n * - Visible focus indicators (2px cyan border)\n * - 80x80px and 70x70px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/ActionButtons\n * @category Mobile Controls\n * @korean 액션 버튼\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, {\n useCallback,\n useState,\n useMemo,\n useRef,\n useEffect,\n} from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerOptimizedHaptic } from \"./HapticController\";\nimport {\n applyOptimizedUpdate,\n createTransformStyle,\n createFilterStyle,\n} from \"./TouchOptimizer\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * Event type for button interactions\n */\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for ActionButtons component\n */\nexport interface ActionButtonsProps {\n /** Callback when attack button is pressed */\n readonly onAttack: () => void;\n /** Callback when block button is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether buttons are disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from right in pixels (default: 20) */\n readonly right?: number;\n /** Opacity of buttons (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * ActionButtons Component\n *\n * Provides two primary combat action buttons:\n * - Attack Button (⚡): Primary combat action, 80x80px\n * - Block Button (🛡️): Defensive action, 70x70px\n *\n * Features:\n * - Touch-optimized with minimum 44x44px targets\n * - Attack button: 80x80px for primary action\n * - Block button: 70x70px for secondary action\n * - Visual feedback on press\n * - Haptic feedback for tactile response\n * - Korean cyberpunk theming\n * - Hold-to-block support\n *\n * Usage in Combat:\n * - Attack: Executes current stance technique\n * - Block: Activates defensive guard (hold for sustained block)\n *\n * @example\n * ```tsx\n * <ActionButtons\n * onAttack={() => executeTechnique()}\n * onBlock={(eventType) => {\n * if (eventType === 'start') {\n * activateBlock();\n * } else {\n * deactivateBlock();\n * }\n * }}\n * disabled={isPaused}\n * />\n * ```\n *\n * @korean 액션버튼\n */\nconst ActionButtonsComponent: React.FC<ActionButtonsProps> = ({\n onAttack,\n onBlock,\n disabled = false,\n bottom = 34,\n right = 20,\n opacity = 0.8,\n}) => {\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n const [attackFocused, setAttackFocused] = useState(false);\n const [blockFocused, setBlockFocused] = useState(false);\n\n const attackButtonRef = useRef<HTMLButtonElement>(null);\n const blockButtonRef = useRef<HTMLButtonElement>(null);\n\n const throttledOnAttack = useThrottle(onAttack, 16);\n const throttledOnBlock = useThrottle(onBlock, 16);\n\n /**\n * Handle attack button press with optimized latency (<16ms)\n * Uses direct DOM manipulation for immediate visual feedback\n */\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n setAttackPressed(true);\n throttledOnAttack();\n triggerOptimizedHaptic(\"medium\");\n },\n );\n },\n [disabled, throttledOnAttack],\n );\n\n /**\n * Handle attack button release with optimized latency\n */\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n attackButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setAttackPressed(false);\n },\n );\n },\n [disabled],\n );\n\n /**\n * Handle block button press with optimized latency (<16ms)\n */\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(true, 0.95);\n element.style.filter = createFilterStyle(true, 1.2);\n },\n () => {\n setBlockPressed(true);\n throttledOnBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Handle block button release with optimized latency\n */\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n applyOptimizedUpdate(\n blockButtonRef.current,\n (element) => {\n element.style.transform = createTransformStyle(false);\n element.style.filter = createFilterStyle(false);\n },\n () => {\n setBlockPressed(false);\n throttledOnBlock(\"end\");\n },\n );\n },\n [disabled, throttledOnBlock],\n );\n\n /**\n * Cleanup on unmount - reset any pending visual states.\n * Note: Captures button refs at effect creation time to avoid stale closures.\n * If the buttons unmount before cleanup runs, the captured variables will still\n * reference the original DOM elements (now potentially detached), so style changes\n * are harmless but may not be visible. The null checks primarily guard against\n * refs that were never set in the first place.\n */\n useEffect(() => {\n const attackButton = attackButtonRef.current;\n const blockButton = blockButtonRef.current;\n\n return () => {\n if (attackButton) {\n attackButton.style.transform = createTransformStyle(false);\n attackButton.style.filter = createFilterStyle(false);\n }\n if (blockButton) {\n blockButton.style.transform = createTransformStyle(false);\n blockButton.style.filter = createFilterStyle(false);\n }\n };\n }, []);\n\n /**\n * Handle keyboard navigation for attack button\n */\n const handleAttackKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setAttackPressed(true);\n onAttack();\n triggerOptimizedHaptic(\"medium\");\n setTimeout(() => setAttackPressed(false), 150);\n },\n });\n },\n [disabled, onAttack],\n );\n\n /**\n * Handle keyboard navigation for block button\n */\n const handleBlockKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setBlockPressed(true);\n onBlock(\"start\");\n triggerOptimizedHaptic(\"light\");\n setTimeout(() => {\n setBlockPressed(false);\n onBlock(\"end\");\n }, 150);\n },\n });\n },\n [disabled, onBlock],\n );\n\n const colors = useMemo(\n () => ({\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n blue: getColorRGB(KOREAN_COLORS.ACCENT_BLUE),\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n right: `${right}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"10px\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"action-buttons\"\n >\n {/* Primary Attack Button */}\n <button\n ref={attackButtonRef}\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n onKeyDown={handleAttackKeyDown}\n onFocus={() => setAttackFocused(true)}\n onBlur={() => setAttackFocused(false)}\n style={{\n width: \"80px\",\n height: \"80px\",\n borderRadius: \"50%\",\n background: attackPressed\n ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`\n : `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`,\n border: \"3px solid #fff\",\n fontSize: \"28px\",\n color: \"#000\",\n fontWeight: \"bold\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n transform: createTransformStyle(attackPressed, 0.95),\n filter: createFilterStyle(attackPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: attackPressed\n ? `0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 12px rgba(0, 0, 0, 0.5), 0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.6)`,\n ...getFocusStyle(attackFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 25px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"공격\", \"Attack\").label}\n aria-pressed={attackPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button */}\n <button\n ref={blockButtonRef}\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n onKeyDown={handleBlockKeyDown}\n onFocus={() => setBlockFocused(true)}\n onBlur={() => setBlockFocused(false)}\n style={{\n width: \"70px\",\n height: \"70px\",\n borderRadius: \"50%\",\n background: blockPressed\n ? `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`\n : `rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.9)`,\n border: \"2px solid #fff\",\n fontSize: \"24px\",\n color: \"#fff\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"transform 0.1s ease-out, filter 0.1s ease-out\",\n transform: createTransformStyle(blockPressed, 0.95),\n filter: createFilterStyle(blockPressed, 1.2),\n willChange: \"transform, filter\", // GPU hint\n boxShadow: blockPressed\n ? `0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1), inset 0 4px 8px rgba(0, 0, 0, 0.3)`\n : `0 4px 10px rgba(0, 0, 0, 0.5), 0 0 12px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 0.6)`,\n ...getFocusStyle(blockFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.5), 0 0 20px rgba(${colors.blue.r}, ${colors.blue.g}, ${colors.blue.b}, 1)`,\n }),\n }}\n disabled={disabled}\n aria-label={createBilingualLabel(\"방어\", \"Block\").label}\n aria-pressed={blockPressed}\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"block-button\"\n >\n 🛡️\n </button>\n\n {/* Button Labels (Korean + English) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n alignItems: \"center\",\n fontSize: \"10px\",\n color: `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n marginTop: \"4px\",\n }}\n >\n <span>공격 | Attack</span>\n <span style={{ fontSize: \"9px\" }}>방어 | Block</span>\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized ActionButtons with custom comparison\n * Only re-renders when props change\n */\nexport const ActionButtons = React.memo(\n ActionButtonsComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.right === nextProps.right &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onAttack === nextProps.onAttack &&\n prevProps.onBlock === nextProps.onBlock\n );\n },\n);\n\nActionButtons.displayName = \"ActionButtons\";\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"GestureRecognizerPure.js","names":[],"sources":["../../../../src/components/shared/mobile/GestureRecognizerPure.tsx"],"sourcesContent":["/**\n * GestureRecognizerPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Visual overlay for gesture detection feedback\n * Displays swipe trails and multi-touch indicators\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * @module components/mobile/GestureRecognizerPure\n * @category Mobile Controls\n * @korean 제스처 인식기 (순수 DOM)\n */\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport {\n GestureEvent,\n useTouchControls,\n} from \"../../../hooks/useTouchControls\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\n\n/**\n * Props for GestureRecognizerPure component\n */\nexport interface GestureRecognizerPureProps {\n /** Callback when gesture is detected */\n readonly onGesture: (gesture: GestureEvent) => void;\n /** Whether gesture recognition is enabled */\n readonly enabled?: boolean;\n /** Whether to show visual feedback */\n readonly showFeedback?: boolean;\n /** Minimum swipe distance in pixels (default: 50) */\n readonly minSwipeDistance?: number;\n}\n\n/**\n * Visual feedback state for gestures\n */\ninterface GestureFeedback {\n readonly id: number;\n readonly type: string;\n readonly timestamp: number;\n readonly x: number;\n readonly y: number;\n readonly age?: number; // Cached age to avoid impure function calls during render\n}\n\n/**\n * GestureRecognizerPure Component\n *\n * Pure DOM gesture detection and visual feedback for mobile controls\n * Features:\n * - Swipe detection (4 directions)\n * - Two-finger tap detection\n * - Visual trail feedback\n * - Gesture type indicators\n * - Auto-fading feedback\n *\n * Gesture Mappings:\n * - Swipe Right: Advance toward opponent\n * - Swipe Left: Retreat from opponent\n * - Swipe Up: High attack mode\n * - Swipe Down: Low attack mode\n * - Two-Finger Tap: Vital point targeting mode\n *\n * @example\n * ```tsx\n * <GestureRecognizerPure\n * onGesture={(gesture) => {\n * console.log('Detected:', gesture.type);\n * handleGesture(gesture);\n * }}\n * enabled={!isPaused}\n * showFeedback={true}\n * />\n * ```\n *\n * @korean 제스처인식기순수\n */\nexport const GestureRecognizerPure: React.FC<GestureRecognizerPureProps> = ({\n onGesture,\n enabled = true,\n showFeedback = true,\n minSwipeDistance = 50,\n}) => {\n const [feedbacks, setFeedbacks] = useState<GestureFeedback[]>([]);\n const [nextId, setNextId] = useState(0);\n\n /**\n * Handle detected gesture\n */\n const handleGesture = useCallback(\n (gesture: GestureEvent) => {\n onGesture(gesture);\n\n if (\n showFeedback &&\n gesture.endX !== undefined &&\n gesture.endY !== undefined\n ) {\n const feedback: GestureFeedback = {\n id: nextId,\n type: gesture.type,\n timestamp: Date.now(),\n x: gesture.endX,\n y: gesture.endY,\n };\n\n setFeedbacks((prev) => [...prev, feedback]);\n setNextId((prev) => prev + 1);\n }\n },\n [onGesture, showFeedback, nextId],\n );\n\n /**\n * Use touch controls hook for gesture detection\n */\n useTouchControls({\n onGesture: handleGesture,\n enabled,\n minSwipeDistance,\n });\n\n /**\n * Clean up old feedback indicators and update ages\n */\n useEffect(() => {\n if (!showFeedback) return;\n\n const interval = setInterval(() => {\n const now = Date.now();\n setFeedbacks((prev) =>\n prev\n .filter((fb) => now - fb.timestamp < 1000)\n .map((fb) => ({ ...fb, age: now - fb.timestamp })),\n );\n }, 100);\n\n return () => clearInterval(interval);\n }, [showFeedback]);\n\n if (!showFeedback) {\n return null;\n }\n\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n\n /**\n * Get display info for gesture type\n */\n const getGestureDisplay = (\n type: string,\n ): { korean: string; english: string; icon: string } => {\n const displays: Record<\n string,\n { korean: string; english: string; icon: string }\n > = {\n \"swipe-right\": { korean: \"전진\", english: \"Advance\", icon: \"→\" },\n \"swipe-left\": { korean: \"후퇴\", english: \"Retreat\", icon: \"←\" },\n \"swipe-up\": { korean: \"상단\", english: \"High\", icon: \"↑\" },\n \"swipe-down\": { korean: \"하단\", english: \"Low\", icon: \"↓\" },\n \"two-finger-tap\": { korean: \"급소\", english: \"Vital Point\", icon: \"🎯\" },\n tap: { korean: \"터치\", english: \"Tap\", icon: \"👆\" },\n };\n return (\n displays[type] ?? { korean: \"제스처\", english: \"Gesture\", icon: \"✋\" }\n );\n };\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n data-testid=\"gesture-recognizer-pure\"\n >\n {/* Gesture feedback indicators */}\n {feedbacks.map((feedback) => {\n const age = feedback.age ?? 0;\n const opacity = Math.max(0, 1 - age / 1000);\n const scale = 1 + age / 500;\n const display = getGestureDisplay(feedback.type);\n\n return (\n <div\n key={feedback.id}\n style={{\n position: \"absolute\",\n left: `${feedback.x}px`,\n top: `${feedback.y}px`,\n transform: `translate(-50%, -50%) scale(${scale})`,\n opacity,\n transition: \"all 0.1s ease-out\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n }}\n data-testid={`gesture-feedback-pure-${feedback.id}`}\n >\n {/* Icon */}\n <div\n style={{\n fontSize: \"32px\",\n color: `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity})`,\n textShadow: `0 0 10px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity * 0.8})`,\n }}\n >\n {display.icon}\n </div>\n\n {/* Label */}\n <div\n style={{\n background: `rgba(0, 0, 0, ${opacity * 0.8})`,\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n borderRadius: \"8px\",\n padding: \"4px 8px\",\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n textAlign: \"center\",\n whiteSpace: \"nowrap\",\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {display.korean} | {display.english}\n </div>\n </div>\n );\n })}\n\n {/* Gesture instructions overlay (optional) */}\n {enabled && (\n <div\n style={{\n position: \"absolute\",\n top: \"10px\",\n right: \"10px\",\n background: \"rgba(0, 0, 0, 0.7)\",\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.6)`,\n borderRadius: \"8px\",\n padding: \"8px 12px\",\n fontSize: \"10px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n maxWidth: \"150px\",\n opacity: 0.7,\n }}\n data-testid=\"gesture-instructions-pure\"\n >\n <div style={{ fontWeight: \"bold\", marginBottom: \"4px\" }}>\n 제스처 | Gestures\n </div>\n <div>← → 이동 | Move</div>\n <div>↑ ↓ 공격 | Attack</div>\n <div>🤞 급소 | Vital</div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"GestureRecognizerPure.js","names":[],"sources":["../../../../src/components/shared/mobile/GestureRecognizerPure.tsx"],"sourcesContent":["/**\n * GestureRecognizerPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Visual overlay for gesture detection feedback\n * Displays swipe trails and multi-touch indicators\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * @module components/mobile/GestureRecognizerPure\n * @category Mobile Controls\n * @korean 제스처 인식기 (순수 DOM)\n */\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport {\n GestureEvent,\n useTouchControls,\n} from \"../../../hooks/useTouchControls\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\n\n/**\n * Props for GestureRecognizerPure component\n */\nexport interface GestureRecognizerPureProps {\n /** Callback when gesture is detected */\n readonly onGesture: (gesture: GestureEvent) => void;\n /** Whether gesture recognition is enabled */\n readonly enabled?: boolean;\n /** Whether to show visual feedback */\n readonly showFeedback?: boolean;\n /** Minimum swipe distance in pixels (default: 50) */\n readonly minSwipeDistance?: number;\n}\n\n/**\n * Visual feedback state for gestures\n */\ninterface GestureFeedback {\n readonly id: number;\n readonly type: string;\n readonly timestamp: number;\n readonly x: number;\n readonly y: number;\n readonly age?: number; // Cached age to avoid impure function calls during render\n}\n\n/**\n * GestureRecognizerPure Component\n *\n * Pure DOM gesture detection and visual feedback for mobile controls\n * Features:\n * - Swipe detection (4 directions)\n * - Two-finger tap detection\n * - Visual trail feedback\n * - Gesture type indicators\n * - Auto-fading feedback\n *\n * Gesture Mappings:\n * - Swipe Right: Advance toward opponent\n * - Swipe Left: Retreat from opponent\n * - Swipe Up: High attack mode\n * - Swipe Down: Low attack mode\n * - Two-Finger Tap: Vital point targeting mode\n *\n * @example\n * ```tsx\n * <GestureRecognizerPure\n * onGesture={(gesture) => {\n * console.log('Detected:', gesture.type);\n * handleGesture(gesture);\n * }}\n * enabled={!isPaused}\n * showFeedback={true}\n * />\n * ```\n *\n * @korean 제스처인식기순수\n */\nexport const GestureRecognizerPure: React.FC<GestureRecognizerPureProps> = ({\n onGesture,\n enabled = true,\n showFeedback = true,\n minSwipeDistance = 50,\n}) => {\n const [feedbacks, setFeedbacks] = useState<GestureFeedback[]>([]);\n const [nextId, setNextId] = useState(0);\n\n /**\n * Handle detected gesture\n */\n const handleGesture = useCallback(\n (gesture: GestureEvent) => {\n onGesture(gesture);\n\n if (\n showFeedback &&\n gesture.endX !== undefined &&\n gesture.endY !== undefined\n ) {\n const feedback: GestureFeedback = {\n id: nextId,\n type: gesture.type,\n timestamp: Date.now(),\n x: gesture.endX,\n y: gesture.endY,\n };\n\n setFeedbacks((prev) => [...prev, feedback]);\n setNextId((prev) => prev + 1);\n }\n },\n [onGesture, showFeedback, nextId],\n );\n\n /**\n * Use touch controls hook for gesture detection\n */\n useTouchControls({\n onGesture: handleGesture,\n enabled,\n minSwipeDistance,\n });\n\n /**\n * Clean up old feedback indicators and update ages\n */\n useEffect(() => {\n if (!showFeedback) return;\n\n const interval = setInterval(() => {\n const now = Date.now();\n setFeedbacks((prev) =>\n prev\n .filter((fb) => now - fb.timestamp < 1000)\n .map((fb) => ({ ...fb, age: now - fb.timestamp })),\n );\n }, 100);\n\n return () => clearInterval(interval);\n }, [showFeedback]);\n\n if (!showFeedback) {\n return null;\n }\n\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n\n /**\n * Get display info for gesture type\n */\n const getGestureDisplay = (\n type: string,\n ): { korean: string; english: string; icon: string } => {\n const displays: Record<\n string,\n { korean: string; english: string; icon: string }\n > = {\n \"swipe-right\": { korean: \"전진\", english: \"Advance\", icon: \"→\" },\n \"swipe-left\": { korean: \"후퇴\", english: \"Retreat\", icon: \"←\" },\n \"swipe-up\": { korean: \"상단\", english: \"High\", icon: \"↑\" },\n \"swipe-down\": { korean: \"하단\", english: \"Low\", icon: \"↓\" },\n \"two-finger-tap\": { korean: \"급소\", english: \"Vital Point\", icon: \"🎯\" },\n tap: { korean: \"터치\", english: \"Tap\", icon: \"👆\" },\n };\n return (\n displays[type] ?? { korean: \"제스처\", english: \"Gesture\", icon: \"✋\" }\n );\n };\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n data-testid=\"gesture-recognizer-pure\"\n >\n {/* Gesture feedback indicators */}\n {feedbacks.map((feedback) => {\n const age = feedback.age ?? 0;\n const opacity = Math.max(0, 1 - age / 1000);\n const scale = 1 + age / 500;\n const display = getGestureDisplay(feedback.type);\n\n return (\n <div\n key={feedback.id}\n style={{\n position: \"absolute\",\n left: `${feedback.x}px`,\n top: `${feedback.y}px`,\n transform: `translate(-50%, -50%) scale(${scale})`,\n opacity,\n transition: \"all 0.1s ease-out\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n }}\n data-testid={`gesture-feedback-pure-${feedback.id}`}\n >\n {/* Icon */}\n <div\n style={{\n fontSize: \"32px\",\n color: `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity})`,\n textShadow: `0 0 10px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, ${opacity * 0.8})`,\n }}\n >\n {display.icon}\n </div>\n\n {/* Label */}\n <div\n style={{\n background: `rgba(0, 0, 0, ${opacity * 0.8})`,\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n borderRadius: \"8px\",\n padding: \"4px 8px\",\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${opacity})`,\n textAlign: \"center\",\n whiteSpace: \"nowrap\",\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {display.korean} | {display.english}\n </div>\n </div>\n );\n })}\n\n {/* Gesture instructions overlay (optional) */}\n {enabled && (\n <div\n style={{\n position: \"absolute\",\n top: \"10px\",\n right: \"10px\",\n background: \"rgba(0, 0, 0, 0.7)\",\n border: `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.6)`,\n borderRadius: \"8px\",\n padding: \"8px 12px\",\n fontSize: \"10px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n maxWidth: \"150px\",\n opacity: 0.7,\n }}\n data-testid=\"gesture-instructions-pure\"\n >\n <div style={{ fontWeight: \"bold\", marginBottom: \"4px\" }}>\n 제스처 | Gestures\n </div>\n <div>← → 이동 | Move</div>\n <div>↑ ↓ 공격 | Attack</div>\n <div>🤞 급소 | Vital</div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"HapticController.js","names":[],"sources":["../../../../src/components/shared/mobile/HapticController.ts"],"sourcesContent":["/**\n * HapticController\n * \n * Optimized haptic feedback system with device capability detection\n * Provides differentiated haptic patterns without causing frame drops\n * \n * Key Features:\n * - Device capability detection (auto-disable on low-end devices)\n * - Differentiated feedback intensities (light, medium, strong)\n * - Frame-drop prevention (<2ms overhead)\n * - Adaptive patterns based on device performance\n * - Throttling to prevent excessive haptic triggers\n * \n * Browser Compatibility Note:\n * navigator.vibrate() returns boolean in some browsers (Chrome, Edge) and void in others (Firefox).\n * This implementation normalizes return values to boolean for consistency.\n * \n * @module components/mobile/HapticController\n * @category Mobile Controls\n * @korean 햅틱 컨트롤러\n */\n\n/**\n * Haptic intensity levels\n */\nexport type HapticIntensity = 'light' | 'medium' | 'strong' | 'disabled';\n\n/**\n * Device performance tier\n */\nexport type DevicePerformanceTier = 'high' | 'medium' | 'low';\n\n/**\n * Haptic pattern configuration\n */\ninterface HapticPattern {\n readonly vibration: number | number[];\n readonly maxFrameTime: number; // Maximum frame time impact in ms\n}\n\n/**\n * Optimized haptic patterns for different intensities\n * Designed to provide clear feedback without frame drops\n * \n * @korean 햅틱 패턴\n */\nconst HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [20], // Short, crisp tap\n maxFrameTime: 0.5, // Minimal impact\n },\n medium: {\n vibration: [40], // Medium pulse\n maxFrameTime: 1.0, // Small impact\n },\n strong: {\n vibration: [60], // Strong pulse\n maxFrameTime: 2.0, // Noticeable impact\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * Adaptive haptic patterns for low-end devices\n * Reduced intensities to prevent frame drops\n * \n * @korean 적응형 햅틱 패턴\n */\nconst ADAPTIVE_HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [10], // Minimal vibration\n maxFrameTime: 0.2,\n },\n medium: {\n vibration: [20], // Reduced medium\n maxFrameTime: 0.5,\n },\n strong: {\n vibration: [30], // Reduced strong\n maxFrameTime: 1.0,\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * HapticController class\n * Manages haptic feedback with device-aware optimization\n * \n * @korean 햅틱컨트롤러\n */\nexport class HapticController {\n private static instance: HapticController | null = null;\n private isSupported: boolean = false;\n private isEnabled: boolean = true;\n private performanceTier: DevicePerformanceTier = 'high';\n /**\n * Last trigger timestamp for throttling.\n * Initialized to -Infinity to ensure the first trigger always succeeds.\n * Using -Infinity instead of 0 prevents throttling when performance.now() returns 0,\n * which can occur in test environments or immediately after page load.\n */\n private lastTriggerTime: number = -Infinity;\n private minTriggerInterval: number = 50; // Minimum 50ms between haptics\n\n /**\n * Private constructor for singleton pattern\n */\n private constructor() {\n this.isSupported = this.detectHapticSupport();\n this.performanceTier = this.detectPerformanceTier();\n \n if (this.performanceTier === 'low') {\n this.isEnabled = false;\n }\n }\n\n /**\n * Get singleton instance\n * \n * @returns HapticController instance\n * @korean 인스턴스가져오기\n */\n public static getInstance(): HapticController {\n this.instance ??= new HapticController();\n return this.instance;\n }\n\n /**\n * Detect if haptic feedback is supported\n * \n * @returns True if Vibration API is available\n * @korean 햅틱지원감지\n */\n private detectHapticSupport(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n return 'vibrate' in navigator;\n }\n\n /**\n * Detect device performance tier\n * Uses multiple heuristics to determine device capability\n * \n * @returns Performance tier (high, medium, low)\n * @korean 성능등급감지\n */\n private detectPerformanceTier(): DevicePerformanceTier {\n if (typeof navigator === 'undefined') {\n return 'medium';\n }\n\n const cores = navigator.hardwareConcurrency ?? 4;\n \n const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 4;\n \n const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);\n \n let score = 0;\n \n if (cores >= 8) score += 2;\n else if (cores >= 4) score += 1;\n \n if (memory >= 8) score += 2;\n else if (memory >= 4) score += 1;\n \n if (!isMobile) score += 1;\n \n if (score >= 4) return 'high';\n if (score >= 2) return 'medium';\n return 'low';\n }\n\n /**\n * Trigger haptic feedback with optimized patterns\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Light haptic for button tap\n * haptic.trigger('light');\n * \n * // Strong haptic for critical hit\n * haptic.trigger('strong');\n * ```\n * \n */\n public trigger(intensity: HapticIntensity): boolean {\n if (!this.isSupported || !this.isEnabled || intensity === 'disabled') {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n const patterns = this.performanceTier === 'low' \n ? ADAPTIVE_HAPTIC_PATTERNS \n : HAPTIC_PATTERNS;\n \n const pattern = patterns[intensity];\n\n try {\n const result = navigator.vibrate(pattern.vibration);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Custom combo pattern\n * haptic.triggerCustom([30, 20, 30, 20, 50]);\n * ```\n * \n */\n public triggerCustom(pattern: number | number[]): boolean {\n if (!this.isSupported || !this.isEnabled) {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n try {\n let adaptedPattern = pattern;\n if (this.performanceTier === 'low') {\n if (Array.isArray(pattern)) {\n adaptedPattern = pattern.map(duration => Math.floor(duration * 0.5));\n } else {\n adaptedPattern = Math.floor(pattern * 0.5);\n }\n }\n\n const result = navigator.vibrate(adaptedPattern);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Custom haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Stop any ongoing haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * \n */\n public stop(): boolean {\n if (!this.isSupported) {\n return false;\n }\n\n try {\n const result = navigator.vibrate(0);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Failed to stop haptic feedback:', error);\n return false;\n }\n }\n\n /**\n * Enable haptic feedback\n * \n * @korean 햅틱활성화\n */\n public enable(): void {\n this.isEnabled = true;\n }\n\n /**\n * Disable haptic feedback\n * \n * @korean 햅틱비활성화\n */\n public disable(): void {\n this.isEnabled = false;\n this.stop();\n }\n\n /**\n * Check if haptic is currently enabled\n * \n * @returns True if haptic is enabled\n * @korean 햅틱활성화상태\n */\n public isHapticEnabled(): boolean {\n return this.isSupported && this.isEnabled;\n }\n\n /**\n * Get current device performance tier\n * \n * @returns Performance tier\n * @korean 성능등급가져오기\n */\n public getPerformanceTier(): DevicePerformanceTier {\n return this.performanceTier;\n }\n\n /**\n * Set minimum interval between haptic triggers\n * \n * @param intervalMs - Minimum interval in milliseconds\n * @korean 최소간격설정\n */\n public setMinTriggerInterval(intervalMs: number): void {\n this.minTriggerInterval = Math.max(0, intervalMs);\n }\n}\n\n/**\n * Convenience function to trigger haptic feedback\n * Uses singleton HapticController instance\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * // Light haptic for UI interaction\n * triggerOptimizedHaptic('light');\n * \n * // Medium haptic for combat action\n * triggerOptimizedHaptic('medium');\n * \n * // Strong haptic for critical hit\n * triggerOptimizedHaptic('strong');\n * ```\n * \n */\nexport function triggerOptimizedHaptic(intensity: HapticIntensity): boolean {\n return HapticController.getInstance().trigger(intensity);\n}\n\n/**\n * Convenience function to trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n */\nexport function triggerCustomOptimizedHaptic(pattern: number | number[]): boolean {\n return HapticController.getInstance().triggerCustom(pattern);\n}\n\n/**\n * Convenience function to stop haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n */\nexport function stopOptimizedHaptic(): boolean {\n return HapticController.getInstance().stop();\n}\n\n/**\n * Combat-specific optimized haptic patterns\n * Pre-configured for common combat scenarios\n * \n * @korean 전투 햅틱 패턴\n */\nexport const OptimizedCombatHaptics = {\n /**\n * Standard attack hit feedback\n * @korean 일반 공격\n */\n attack: () => triggerOptimizedHaptic('medium'),\n\n /**\n * Block successful feedback\n * @korean 방어 성공\n */\n block: () => triggerOptimizedHaptic('light'),\n\n /**\n * Critical hit feedback with double pulse\n * @korean 크리티컬 히트\n */\n criticalHit: () => triggerCustomOptimizedHaptic([40, 20, 60]),\n\n /**\n * Vital point strike feedback\n * @korean 급소 타격\n */\n vitalPointStrike: () => triggerOptimizedHaptic('strong'),\n\n /**\n * Stance change feedback\n * @korean 자세 변경\n */\n stanceChange: () => triggerOptimizedHaptic('light'),\n\n /**\n * Combo counter increment\n * @korean 콤보 카운터\n */\n comboIncrement: () => triggerOptimizedHaptic('light'),\n\n /**\n * Player KO feedback with extended pattern\n * @korean 플레이어 KO\n */\n knockout: () => triggerCustomOptimizedHaptic([60, 30, 60, 30, 100]),\n\n /**\n * Error or invalid action feedback\n * @korean 오류 피드백\n */\n error: () => triggerCustomOptimizedHaptic([15, 10, 15]),\n} as const;\n"],"mappings":"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"}
1
+ {"version":3,"file":"HapticController.js","names":[],"sources":["../../../../src/components/shared/mobile/HapticController.ts"],"sourcesContent":["/**\n * HapticController\n * \n * Optimized haptic feedback system with device capability detection\n * Provides differentiated haptic patterns without causing frame drops\n * \n * Key Features:\n * - Device capability detection (auto-disable on low-end devices)\n * - Differentiated feedback intensities (light, medium, strong)\n * - Frame-drop prevention (<2ms overhead)\n * - Adaptive patterns based on device performance\n * - Throttling to prevent excessive haptic triggers\n * \n * Browser Compatibility Note:\n * navigator.vibrate() returns boolean in some browsers (Chrome, Edge) and void in others (Firefox).\n * This implementation normalizes return values to boolean for consistency.\n * \n * @module components/mobile/HapticController\n * @category Mobile Controls\n * @korean 햅틱 컨트롤러\n */\n\n/**\n * Haptic intensity levels\n */\nexport type HapticIntensity = 'light' | 'medium' | 'strong' | 'disabled';\n\n/**\n * Device performance tier\n */\nexport type DevicePerformanceTier = 'high' | 'medium' | 'low';\n\n/**\n * Haptic pattern configuration\n */\ninterface HapticPattern {\n readonly vibration: number | number[];\n readonly maxFrameTime: number; // Maximum frame time impact in ms\n}\n\n/**\n * Optimized haptic patterns for different intensities\n * Designed to provide clear feedback without frame drops\n * \n * @korean 햅틱 패턴\n */\nconst HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [20], // Short, crisp tap\n maxFrameTime: 0.5, // Minimal impact\n },\n medium: {\n vibration: [40], // Medium pulse\n maxFrameTime: 1.0, // Small impact\n },\n strong: {\n vibration: [60], // Strong pulse\n maxFrameTime: 2.0, // Noticeable impact\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * Adaptive haptic patterns for low-end devices\n * Reduced intensities to prevent frame drops\n * \n * @korean 적응형 햅틱 패턴\n */\nconst ADAPTIVE_HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [10], // Minimal vibration\n maxFrameTime: 0.2,\n },\n medium: {\n vibration: [20], // Reduced medium\n maxFrameTime: 0.5,\n },\n strong: {\n vibration: [30], // Reduced strong\n maxFrameTime: 1.0,\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * HapticController class\n * Manages haptic feedback with device-aware optimization\n * \n * @korean 햅틱컨트롤러\n */\nexport class HapticController {\n private static instance: HapticController | null = null;\n private isSupported: boolean = false;\n private isEnabled: boolean = true;\n private performanceTier: DevicePerformanceTier = 'high';\n /**\n * Last trigger timestamp for throttling.\n * Initialized to -Infinity to ensure the first trigger always succeeds.\n * Using -Infinity instead of 0 prevents throttling when performance.now() returns 0,\n * which can occur in test environments or immediately after page load.\n */\n private lastTriggerTime: number = -Infinity;\n private minTriggerInterval: number = 50; // Minimum 50ms between haptics\n\n /**\n * Private constructor for singleton pattern\n */\n private constructor() {\n this.isSupported = this.detectHapticSupport();\n this.performanceTier = this.detectPerformanceTier();\n \n if (this.performanceTier === 'low') {\n this.isEnabled = false;\n }\n }\n\n /**\n * Get singleton instance\n * \n * @returns HapticController instance\n * @korean 인스턴스가져오기\n */\n public static getInstance(): HapticController {\n this.instance ??= new HapticController();\n return this.instance;\n }\n\n /**\n * Detect if haptic feedback is supported\n * \n * @returns True if Vibration API is available\n * @korean 햅틱지원감지\n */\n private detectHapticSupport(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n return 'vibrate' in navigator;\n }\n\n /**\n * Detect device performance tier\n * Uses multiple heuristics to determine device capability\n * \n * @returns Performance tier (high, medium, low)\n * @korean 성능등급감지\n */\n private detectPerformanceTier(): DevicePerformanceTier {\n if (typeof navigator === 'undefined') {\n return 'medium';\n }\n\n const cores = navigator.hardwareConcurrency ?? 4;\n \n const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 4;\n \n const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);\n \n let score = 0;\n \n if (cores >= 8) score += 2;\n else if (cores >= 4) score += 1;\n \n if (memory >= 8) score += 2;\n else if (memory >= 4) score += 1;\n \n if (!isMobile) score += 1;\n \n if (score >= 4) return 'high';\n if (score >= 2) return 'medium';\n return 'low';\n }\n\n /**\n * Trigger haptic feedback with optimized patterns\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Light haptic for button tap\n * haptic.trigger('light');\n * \n * // Strong haptic for critical hit\n * haptic.trigger('strong');\n * ```\n * \n */\n public trigger(intensity: HapticIntensity): boolean {\n if (!this.isSupported || !this.isEnabled || intensity === 'disabled') {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n const patterns = this.performanceTier === 'low' \n ? ADAPTIVE_HAPTIC_PATTERNS \n : HAPTIC_PATTERNS;\n \n const pattern = patterns[intensity];\n\n try {\n const result = navigator.vibrate(pattern.vibration);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Custom combo pattern\n * haptic.triggerCustom([30, 20, 30, 20, 50]);\n * ```\n * \n */\n public triggerCustom(pattern: number | number[]): boolean {\n if (!this.isSupported || !this.isEnabled) {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n try {\n let adaptedPattern = pattern;\n if (this.performanceTier === 'low') {\n if (Array.isArray(pattern)) {\n adaptedPattern = pattern.map(duration => Math.floor(duration * 0.5));\n } else {\n adaptedPattern = Math.floor(pattern * 0.5);\n }\n }\n\n const result = navigator.vibrate(adaptedPattern);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Custom haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Stop any ongoing haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * \n */\n public stop(): boolean {\n if (!this.isSupported) {\n return false;\n }\n\n try {\n const result = navigator.vibrate(0);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Failed to stop haptic feedback:', error);\n return false;\n }\n }\n\n /**\n * Enable haptic feedback\n * \n * @korean 햅틱활성화\n */\n public enable(): void {\n this.isEnabled = true;\n }\n\n /**\n * Disable haptic feedback\n * \n * @korean 햅틱비활성화\n */\n public disable(): void {\n this.isEnabled = false;\n this.stop();\n }\n\n /**\n * Check if haptic is currently enabled\n * \n * @returns True if haptic is enabled\n * @korean 햅틱활성화상태\n */\n public isHapticEnabled(): boolean {\n return this.isSupported && this.isEnabled;\n }\n\n /**\n * Get current device performance tier\n * \n * @returns Performance tier\n * @korean 성능등급가져오기\n */\n public getPerformanceTier(): DevicePerformanceTier {\n return this.performanceTier;\n }\n\n /**\n * Set minimum interval between haptic triggers\n * \n * @param intervalMs - Minimum interval in milliseconds\n * @korean 최소간격설정\n */\n public setMinTriggerInterval(intervalMs: number): void {\n this.minTriggerInterval = Math.max(0, intervalMs);\n }\n}\n\n/**\n * Convenience function to trigger haptic feedback\n * Uses singleton HapticController instance\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * // Light haptic for UI interaction\n * triggerOptimizedHaptic('light');\n * \n * // Medium haptic for combat action\n * triggerOptimizedHaptic('medium');\n * \n * // Strong haptic for critical hit\n * triggerOptimizedHaptic('strong');\n * ```\n * \n */\nexport function triggerOptimizedHaptic(intensity: HapticIntensity): boolean {\n return HapticController.getInstance().trigger(intensity);\n}\n\n/**\n * Convenience function to trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n */\nexport function triggerCustomOptimizedHaptic(pattern: number | number[]): boolean {\n return HapticController.getInstance().triggerCustom(pattern);\n}\n\n/**\n * Convenience function to stop haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n */\nexport function stopOptimizedHaptic(): boolean {\n return HapticController.getInstance().stop();\n}\n\n/**\n * Combat-specific optimized haptic patterns\n * Pre-configured for common combat scenarios\n * \n * @korean 전투 햅틱 패턴\n */\nexport const OptimizedCombatHaptics = {\n /**\n * Standard attack hit feedback\n * @korean 일반 공격\n */\n attack: () => triggerOptimizedHaptic('medium'),\n\n /**\n * Block successful feedback\n * @korean 방어 성공\n */\n block: () => triggerOptimizedHaptic('light'),\n\n /**\n * Critical hit feedback with double pulse\n * @korean 크리티컬 히트\n */\n criticalHit: () => triggerCustomOptimizedHaptic([40, 20, 60]),\n\n /**\n * Vital point strike feedback\n * @korean 급소 타격\n */\n vitalPointStrike: () => triggerOptimizedHaptic('strong'),\n\n /**\n * Stance change feedback\n * @korean 자세 변경\n */\n stanceChange: () => triggerOptimizedHaptic('light'),\n\n /**\n * Combo counter increment\n * @korean 콤보 카운터\n */\n comboIncrement: () => triggerOptimizedHaptic('light'),\n\n /**\n * Player KO feedback with extended pattern\n * @korean 플레이어 KO\n */\n knockout: () => triggerCustomOptimizedHaptic([60, 30, 60, 30, 100]),\n\n /**\n * Error or invalid action feedback\n * @korean 오류 피드백\n */\n error: () => triggerCustomOptimizedHaptic([15, 10, 15]),\n} as const;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StanceWheelPure.js","names":[],"sources":["../../../../src/components/shared/mobile/StanceWheelPure.tsx"],"sourcesContent":["/**\n * StanceWheelPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Circular 8-segment stance selector for mobile touch controls\n * Provides visual and tactile stance switching interface\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 stance buttons\n * - Keyboard navigation (Tab, Enter, Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - aria-expanded state for wheel toggle\n * - role=\"radiogroup\" for stance selection\n * - 50x50px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/StanceWheelPure\n * @category Mobile Controls\n * @korean 자세 휠 (순수 DOM)\n */\n\nimport React, { useCallback, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../types/common\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport {\n handleKeyboardNav,\n getFocusStyle,\n announceToScreenReader,\n} from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\n\n/**\n * Props for StanceWheelPure component\n */\nexport interface StanceWheelPureProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Callback when stance changes */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Whether wheel is expanded */\n readonly expanded: boolean;\n /** Callback to toggle expansion */\n readonly onToggle: () => void;\n /** Whether wheel is disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 when collapsed, 100 when expanded for safe area) */\n readonly bottom?: number;\n /** Opacity of wheel (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Trigram symbols for each stance\n */\nconst TRIGRAM_SYMBOLS = [\n \"☰\",\n \"☱\",\n \"☲\",\n \"☳\",\n \"☴\",\n \"☵\",\n \"☶\",\n \"☷\",\n] as const;\n\n/**\n * Korean names for each stance\n */\nconst STANCE_KOREAN_NAMES = [\n \"건\", // Geon - Heaven\n \"태\", // Tae - Lake\n \"리\", // Li - Fire\n \"진\", // Jin - Thunder\n \"손\", // Son - Wind\n \"감\", // Gam - Water\n \"간\", // Gan - Mountain\n \"곤\", // Gon - Earth\n] as const;\n\n/**\n * Get color for a specific stance\n */\nconst getStanceColor = (stance: TrigramStance): number => {\n const stanceColors: Record<TrigramStance, number> = {\n geon: 0xffd700, // Gold - Heaven\n tae: 0x00ffff, // Cyan - Lake\n li: 0xff4444, // Red - Fire\n jin: 0xffaa00, // Orange - Thunder\n son: 0x88ff88, // Light Green - Wind\n gam: 0x0088ff, // Blue - Water\n gan: 0x8844ff, // Purple - Mountain\n gon: 0xaa6644, // Brown - Earth\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * StanceWheelPure Component\n *\n * Pure DOM circular stance selector with 8 segments for trigram stances\n * Features:\n * - Expandable/collapsible interface\n * - Visual stance indicator when collapsed (60x60px)\n * - 8 touch-optimized stance buttons when expanded (50x50px each)\n * - 200px wheel diameter with safe positioning\n * - Korean trigram symbols and names\n * - Color-coded by stance element\n * - Haptic feedback on selection\n * - 50x50px minimum touch targets\n *\n * Usage in Combat:\n * - Tap collapsed indicator to expand wheel\n * - Select from 8 trigram stances\n * - Current stance highlighted with gold accent\n * - Tap current stance to collapse wheel\n *\n * @example\n * ```tsx\n * <StanceWheelPure\n * currentStance={player.stance}\n * onStanceChange={(index) => handleStanceChange(index)}\n * expanded={wheelExpanded}\n * onToggle={() => setWheelExpanded(!wheelExpanded)}\n * disabled={isPaused}\n * />\n * ```\n *\n * @korean 자세휠순수\n */\nexport const StanceWheelPure: React.FC<StanceWheelPureProps> = ({\n currentStance,\n onStanceChange,\n expanded,\n onToggle,\n disabled = false,\n bottom,\n opacity = 0.8,\n}) => {\n const [hoveredStance, setHoveredStance] = useState<number | null>(null);\n const [focusedStance, setFocusedStance] = useState<number | null>(null);\n\n /**\n * Handle stance selection (touch or mouse)\n */\n const handleStanceSelect = useCallback(\n (e: React.TouchEvent | React.MouseEvent, stanceIndex: number) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n if (stanceIndex === currentStance) {\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\n const stanceName = STANCE_KOREAN_NAMES[stanceIndex];\n const stanceSymbol = TRIGRAM_SYMBOLS[stanceIndex];\n announceToScreenReader({\n message: createBilingualLabel(\n `자세 변경: ${stanceName} ${stanceSymbol}`,\n `Stance changed to ${TRIGRAM_STANCES_ORDER[stanceIndex]}`,\n ).label,\n politeness: \"polite\",\n });\n\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle wheel toggle (touch or mouse)\n */\n const handleToggle = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n onToggle();\n triggerHaptic(\"light\");\n\n const nextExpanded = !expanded;\n announceToScreenReader({\n message: createBilingualLabel(\n nextExpanded ? \"자세 휠 열림\" : \"자세 휠 닫힘\",\n nextExpanded ? \"Stance wheel opened\" : \"Stance wheel closed\",\n ).label,\n politeness: \"polite\",\n });\n },\n [disabled, onToggle, expanded],\n );\n\n /**\n * Handle keyboard navigation for stance buttons\n */\n const handleStanceKeyDown = useCallback(\n (stanceIndex: number) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n if (stanceIndex === currentStance) {\n onToggle();\n } else {\n onStanceChange(stanceIndex);\n }\n triggerHaptic(\"medium\");\n },\n onCancel: () => {\n onToggle();\n },\n onNavigate: (direction) => {\n let newIndex = stanceIndex;\n if (direction === \"left\" || direction === \"up\") {\n newIndex = (stanceIndex - 1 + 8) % 8;\n } else if (direction === \"right\" || direction === \"down\") {\n newIndex = (stanceIndex + 1) % 8;\n }\n setFocusedStance(newIndex);\n requestAnimationFrame(() => {\n const button = document.querySelector(\n `[data-testid=\"stance-button-pure-${newIndex}\"]`,\n ) as HTMLElement | null;\n button?.focus();\n });\n },\n });\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle keyboard navigation for toggle button\n */\n const handleToggleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n onToggle();\n triggerHaptic(\"light\");\n },\n });\n },\n [disabled, onToggle],\n );\n\n const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\n const currentStanceColor = getColorRGB(\n getStanceColor(TRIGRAM_STANCES_ORDER[currentStance]),\n );\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n if (expanded) {\n const wheelSize = 200;\n const segmentAngle = 360 / 8;\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-expanded\"\n role=\"radiogroup\"\n aria-label={\n createBilingualLabel(\n \"팔괘 자세 선택\",\n \"Eight Trigram Stance Selection\",\n ).label\n }\n aria-activedescendant={`stance-button-pure-${currentStance}`}\n >\n <div\n style={{\n width: `${wheelSize}px`,\n height: `${wheelSize}px`,\n position: \"relative\",\n }}\n >\n {/* Stance buttons arranged in circle */}\n {TRIGRAM_STANCES_ORDER.map((stance, index) => {\n const angle = index * segmentAngle;\n const radian = (angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * 80 + 100;\n const y = Math.sin(radian) * 80 + 100;\n const isActive = index === currentStance;\n const isHovered = index === hoveredStance;\n const stanceColor = getColorRGB(getStanceColor(stance));\n\n return (\n <button\n key={stance}\n onTouchStart={(e) => handleStanceSelect(e, index)}\n onTouchEnd={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setHoveredStance(null);\n }}\n onMouseDown={(e) => handleStanceSelect(e, index)}\n onMouseEnter={() => setHoveredStance(index)}\n onMouseLeave={() => setHoveredStance(null)}\n onKeyDown={handleStanceKeyDown(index)}\n onFocus={() => setFocusedStance(index)}\n onBlur={() => setFocusedStance(null)}\n style={{\n position: \"absolute\",\n left: `${x - 25}px`,\n top: `${y - 25}px`,\n width: \"50px\",\n height: \"50px\",\n borderRadius: \"50%\",\n background: isActive\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.95)`\n : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`,\n border: `3px solid ${isActive ? \"#fff\" : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 1)`}`,\n fontSize: \"24px\",\n color: isActive ? \"#000\" : \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n transform: isActive || isHovered ? \"scale(1.15)\" : \"scale(1)\",\n boxShadow: isActive\n ? `0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : isHovered\n ? `0 0 15px rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`\n : `0 4px 10px rgba(0, 0, 0, 0.5)`,\n ...getFocusStyle(focusedStance === index, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n aria-label={\n createBilingualLabel(\n `${STANCE_KOREAN_NAMES[index]} ${TRIGRAM_SYMBOLS[index]}`,\n `${stance} stance`,\n ).label\n }\n aria-checked={isActive}\n role=\"radio\"\n tabIndex={0}\n disabled={disabled}\n id={`stance-button-pure-${index}`}\n data-testid={`stance-button-pure-${index}`}\n >\n <div style={{ fontSize: \"20px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[index]}\n </div>\n <div style={{ fontSize: \"8px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[index]}\n </div>\n </button>\n );\n })}\n\n {/* Center label */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontSize: \"12px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n textAlign: \"center\",\n pointerEvents: \"none\",\n }}\n >\n 자세 선택\n <br />\n <span style={{ fontSize: \"10px\" }}>Stance</span>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-collapsed\"\n >\n <button\n onTouchStart={handleToggle}\n onMouseDown={handleToggle}\n onKeyDown={handleToggleKeyDown}\n onFocus={() => setFocusedStance(currentStance)}\n onBlur={() => setFocusedStance(null)}\n style={{\n width: \"60px\",\n height: \"60px\",\n borderRadius: \"50%\",\n background: `rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.9)`,\n border: `3px solid rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n fontSize: \"28px\",\n color: \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n ...getFocusStyle(focusedStance === currentStance, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n disabled={disabled}\n aria-label={\n createBilingualLabel(\n `현재 자세: ${STANCE_KOREAN_NAMES[currentStance]} ${TRIGRAM_SYMBOLS[currentStance]}. 자세 휠 열기`,\n `Current stance: ${TRIGRAM_STANCES_ORDER[currentStance]}. Open stance wheel`,\n ).label\n }\n aria-expanded={expanded}\n aria-haspopup=\"menu\"\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"stance-wheel-pure-toggle\"\n >\n <div style={{ fontSize: \"24px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[currentStance]}\n </div>\n <div style={{ fontSize: \"10px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[currentStance]}\n </div>\n </button>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"StanceWheelPure.js","names":[],"sources":["../../../../src/components/shared/mobile/StanceWheelPure.tsx"],"sourcesContent":["/**\n * StanceWheelPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Circular 8-segment stance selector for mobile touch controls\n * Provides visual and tactile stance switching interface\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 stance buttons\n * - Keyboard navigation (Tab, Enter, Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - aria-expanded state for wheel toggle\n * - role=\"radiogroup\" for stance selection\n * - 50x50px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/StanceWheelPure\n * @category Mobile Controls\n * @korean 자세 휠 (순수 DOM)\n */\n\nimport React, { useCallback, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../types/common\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport {\n handleKeyboardNav,\n getFocusStyle,\n announceToScreenReader,\n} from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\n\n/**\n * Props for StanceWheelPure component\n */\nexport interface StanceWheelPureProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Callback when stance changes */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Whether wheel is expanded */\n readonly expanded: boolean;\n /** Callback to toggle expansion */\n readonly onToggle: () => void;\n /** Whether wheel is disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 when collapsed, 100 when expanded for safe area) */\n readonly bottom?: number;\n /** Opacity of wheel (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Trigram symbols for each stance\n */\nconst TRIGRAM_SYMBOLS = [\n \"☰\",\n \"☱\",\n \"☲\",\n \"☳\",\n \"☴\",\n \"☵\",\n \"☶\",\n \"☷\",\n] as const;\n\n/**\n * Korean names for each stance\n */\nconst STANCE_KOREAN_NAMES = [\n \"건\", // Geon - Heaven\n \"태\", // Tae - Lake\n \"리\", // Li - Fire\n \"진\", // Jin - Thunder\n \"손\", // Son - Wind\n \"감\", // Gam - Water\n \"간\", // Gan - Mountain\n \"곤\", // Gon - Earth\n] as const;\n\n/**\n * Get color for a specific stance\n */\nconst getStanceColor = (stance: TrigramStance): number => {\n const stanceColors: Record<TrigramStance, number> = {\n geon: 0xffd700, // Gold - Heaven\n tae: 0x00ffff, // Cyan - Lake\n li: 0xff4444, // Red - Fire\n jin: 0xffaa00, // Orange - Thunder\n son: 0x88ff88, // Light Green - Wind\n gam: 0x0088ff, // Blue - Water\n gan: 0x8844ff, // Purple - Mountain\n gon: 0xaa6644, // Brown - Earth\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * StanceWheelPure Component\n *\n * Pure DOM circular stance selector with 8 segments for trigram stances\n * Features:\n * - Expandable/collapsible interface\n * - Visual stance indicator when collapsed (60x60px)\n * - 8 touch-optimized stance buttons when expanded (50x50px each)\n * - 200px wheel diameter with safe positioning\n * - Korean trigram symbols and names\n * - Color-coded by stance element\n * - Haptic feedback on selection\n * - 50x50px minimum touch targets\n *\n * Usage in Combat:\n * - Tap collapsed indicator to expand wheel\n * - Select from 8 trigram stances\n * - Current stance highlighted with gold accent\n * - Tap current stance to collapse wheel\n *\n * @example\n * ```tsx\n * <StanceWheelPure\n * currentStance={player.stance}\n * onStanceChange={(index) => handleStanceChange(index)}\n * expanded={wheelExpanded}\n * onToggle={() => setWheelExpanded(!wheelExpanded)}\n * disabled={isPaused}\n * />\n * ```\n *\n * @korean 자세휠순수\n */\nexport const StanceWheelPure: React.FC<StanceWheelPureProps> = ({\n currentStance,\n onStanceChange,\n expanded,\n onToggle,\n disabled = false,\n bottom,\n opacity = 0.8,\n}) => {\n const [hoveredStance, setHoveredStance] = useState<number | null>(null);\n const [focusedStance, setFocusedStance] = useState<number | null>(null);\n\n /**\n * Handle stance selection (touch or mouse)\n */\n const handleStanceSelect = useCallback(\n (e: React.TouchEvent | React.MouseEvent, stanceIndex: number) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n if (stanceIndex === currentStance) {\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\n const stanceName = STANCE_KOREAN_NAMES[stanceIndex];\n const stanceSymbol = TRIGRAM_SYMBOLS[stanceIndex];\n announceToScreenReader({\n message: createBilingualLabel(\n `자세 변경: ${stanceName} ${stanceSymbol}`,\n `Stance changed to ${TRIGRAM_STANCES_ORDER[stanceIndex]}`,\n ).label,\n politeness: \"polite\",\n });\n\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle wheel toggle (touch or mouse)\n */\n const handleToggle = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n onToggle();\n triggerHaptic(\"light\");\n\n const nextExpanded = !expanded;\n announceToScreenReader({\n message: createBilingualLabel(\n nextExpanded ? \"자세 휠 열림\" : \"자세 휠 닫힘\",\n nextExpanded ? \"Stance wheel opened\" : \"Stance wheel closed\",\n ).label,\n politeness: \"polite\",\n });\n },\n [disabled, onToggle, expanded],\n );\n\n /**\n * Handle keyboard navigation for stance buttons\n */\n const handleStanceKeyDown = useCallback(\n (stanceIndex: number) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n if (stanceIndex === currentStance) {\n onToggle();\n } else {\n onStanceChange(stanceIndex);\n }\n triggerHaptic(\"medium\");\n },\n onCancel: () => {\n onToggle();\n },\n onNavigate: (direction) => {\n let newIndex = stanceIndex;\n if (direction === \"left\" || direction === \"up\") {\n newIndex = (stanceIndex - 1 + 8) % 8;\n } else if (direction === \"right\" || direction === \"down\") {\n newIndex = (stanceIndex + 1) % 8;\n }\n setFocusedStance(newIndex);\n requestAnimationFrame(() => {\n const button = document.querySelector(\n `[data-testid=\"stance-button-pure-${newIndex}\"]`,\n ) as HTMLElement | null;\n button?.focus();\n });\n },\n });\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle keyboard navigation for toggle button\n */\n const handleToggleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n onToggle();\n triggerHaptic(\"light\");\n },\n });\n },\n [disabled, onToggle],\n );\n\n const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\n const currentStanceColor = getColorRGB(\n getStanceColor(TRIGRAM_STANCES_ORDER[currentStance]),\n );\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n if (expanded) {\n const wheelSize = 200;\n const segmentAngle = 360 / 8;\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-expanded\"\n role=\"radiogroup\"\n aria-label={\n createBilingualLabel(\n \"팔괘 자세 선택\",\n \"Eight Trigram Stance Selection\",\n ).label\n }\n aria-activedescendant={`stance-button-pure-${currentStance}`}\n >\n <div\n style={{\n width: `${wheelSize}px`,\n height: `${wheelSize}px`,\n position: \"relative\",\n }}\n >\n {/* Stance buttons arranged in circle */}\n {TRIGRAM_STANCES_ORDER.map((stance, index) => {\n const angle = index * segmentAngle;\n const radian = (angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * 80 + 100;\n const y = Math.sin(radian) * 80 + 100;\n const isActive = index === currentStance;\n const isHovered = index === hoveredStance;\n const stanceColor = getColorRGB(getStanceColor(stance));\n\n return (\n <button\n key={stance}\n onTouchStart={(e) => handleStanceSelect(e, index)}\n onTouchEnd={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setHoveredStance(null);\n }}\n onMouseDown={(e) => handleStanceSelect(e, index)}\n onMouseEnter={() => setHoveredStance(index)}\n onMouseLeave={() => setHoveredStance(null)}\n onKeyDown={handleStanceKeyDown(index)}\n onFocus={() => setFocusedStance(index)}\n onBlur={() => setFocusedStance(null)}\n style={{\n position: \"absolute\",\n left: `${x - 25}px`,\n top: `${y - 25}px`,\n width: \"50px\",\n height: \"50px\",\n borderRadius: \"50%\",\n background: isActive\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.95)`\n : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`,\n border: `3px solid ${isActive ? \"#fff\" : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 1)`}`,\n fontSize: \"24px\",\n color: isActive ? \"#000\" : \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n transform: isActive || isHovered ? \"scale(1.15)\" : \"scale(1)\",\n boxShadow: isActive\n ? `0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : isHovered\n ? `0 0 15px rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`\n : `0 4px 10px rgba(0, 0, 0, 0.5)`,\n ...getFocusStyle(focusedStance === index, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n aria-label={\n createBilingualLabel(\n `${STANCE_KOREAN_NAMES[index]} ${TRIGRAM_SYMBOLS[index]}`,\n `${stance} stance`,\n ).label\n }\n aria-checked={isActive}\n role=\"radio\"\n tabIndex={0}\n disabled={disabled}\n id={`stance-button-pure-${index}`}\n data-testid={`stance-button-pure-${index}`}\n >\n <div style={{ fontSize: \"20px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[index]}\n </div>\n <div style={{ fontSize: \"8px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[index]}\n </div>\n </button>\n );\n })}\n\n {/* Center label */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontSize: \"12px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n textAlign: \"center\",\n pointerEvents: \"none\",\n }}\n >\n 자세 선택\n <br />\n <span style={{ fontSize: \"10px\" }}>Stance</span>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-collapsed\"\n >\n <button\n onTouchStart={handleToggle}\n onMouseDown={handleToggle}\n onKeyDown={handleToggleKeyDown}\n onFocus={() => setFocusedStance(currentStance)}\n onBlur={() => setFocusedStance(null)}\n style={{\n width: \"60px\",\n height: \"60px\",\n borderRadius: \"50%\",\n background: `rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.9)`,\n border: `3px solid rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n fontSize: \"28px\",\n color: \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n ...getFocusStyle(focusedStance === currentStance, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n disabled={disabled}\n aria-label={\n createBilingualLabel(\n `현재 자세: ${STANCE_KOREAN_NAMES[currentStance]} ${TRIGRAM_SYMBOLS[currentStance]}. 자세 휠 열기`,\n `Current stance: ${TRIGRAM_STANCES_ORDER[currentStance]}. Open stance wheel`,\n ).label\n }\n aria-expanded={expanded}\n aria-haspopup=\"menu\"\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"stance-wheel-pure-toggle\"\n >\n <div style={{ fontSize: \"24px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[currentStance]}\n </div>\n <div style={{ fontSize: \"10px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[currentStance]}\n </div>\n </button>\n </div>\n );\n};\n"],"mappings":"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"}