blacktrigram 0.7.47 → 0.7.49

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 (471) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioAssetLoader.js.map +1 -1
  3. package/lib/audio/AudioAssetRegistry.js.map +1 -1
  4. package/lib/audio/AudioCache.js.map +1 -1
  5. package/lib/audio/AudioManager.js.map +1 -1
  6. package/lib/audio/AudioMonitor.js.map +1 -1
  7. package/lib/audio/AudioPool.js.map +1 -1
  8. package/lib/audio/AudioProvider.js.map +1 -1
  9. package/lib/audio/AudioUtils.js.map +1 -1
  10. package/lib/audio/BoneImpactAudioMap.js.map +1 -1
  11. package/lib/audio/VariantSelector.js.map +1 -1
  12. package/lib/audio/types.js.map +1 -1
  13. package/lib/components/screens/combat/CombatScreen3D.d.ts.map +1 -1
  14. package/lib/components/screens/combat/CombatScreen3D.js +29 -25
  15. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  18. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  19. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  20. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  21. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  22. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  25. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  26. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  27. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  28. package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
  29. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  30. package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
  31. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  32. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  33. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  34. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  36. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  37. package/lib/components/screens/combat/components/hud/CombatBottomHUD.d.ts.map +1 -1
  38. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js +2 -2
  39. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  40. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  41. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js +1 -1
  42. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  43. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  44. package/lib/components/screens/combat/components/hud/CombatTopHUD.d.ts.map +1 -1
  45. package/lib/components/screens/combat/components/hud/CombatTopHUD.js +2 -1
  46. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  47. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  48. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  49. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  50. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  51. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  52. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  53. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  54. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  55. package/lib/components/screens/combat/helpers/AnimationUpdater.d.ts.map +1 -1
  56. package/lib/components/screens/combat/helpers/AnimationUpdater.js +4 -2
  57. package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
  58. package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
  59. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  60. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  61. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  62. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  63. package/lib/components/screens/combat/hooks/useCombatLayout.d.ts.map +1 -1
  64. package/lib/components/screens/combat/hooks/useCombatLayout.js +11 -5
  65. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  66. package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
  67. package/lib/components/screens/controls/ControlsScreen3D.js +1 -1
  68. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  69. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  72. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  74. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  75. package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
  76. package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
  77. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  78. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  79. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  80. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  81. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  83. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  84. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  85. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  86. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  87. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  88. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  90. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  91. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  92. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  93. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  94. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  95. package/lib/components/screens/philosophy/PhilosophyScreen3D.js +1 -1
  96. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  97. package/lib/components/screens/training/TrainingScreen3D.d.ts.map +1 -1
  98. package/lib/components/screens/training/TrainingScreen3D.js +3 -11
  99. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  100. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  101. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  102. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  103. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  104. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  105. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  106. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  107. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  108. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  109. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  110. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  111. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  112. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  113. package/lib/components/screens/training/components/hud/TrainingBottomHUD.d.ts.map +1 -1
  114. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js +2 -2
  115. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  116. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  117. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  118. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  119. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  120. package/lib/components/screens/training/hooks/useTrainingActions.d.ts +1 -0
  121. package/lib/components/screens/training/hooks/useTrainingActions.d.ts.map +1 -1
  122. package/lib/components/screens/training/hooks/useTrainingActions.js +6 -4
  123. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  124. package/lib/components/screens/training/hooks/useTrainingLayout.d.ts.map +1 -1
  125. package/lib/components/screens/training/hooks/useTrainingLayout.js +11 -5
  126. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  127. package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
  128. package/lib/components/shared/base/BaseButton.js.map +1 -1
  129. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  130. package/lib/components/shared/base/BasePanel.js.map +1 -1
  131. package/lib/components/shared/base/BaseText.js.map +1 -1
  132. package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
  133. package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
  134. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  135. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  136. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  137. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  138. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  139. package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
  140. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  141. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  142. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  143. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  144. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  145. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  146. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  147. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  148. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  149. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  150. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  151. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  152. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  153. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  154. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  155. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  156. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  157. package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
  158. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  159. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  160. package/lib/components/shared/three/models/SkeletalPlayer3D.d.ts.map +1 -1
  161. package/lib/components/shared/three/models/SkeletalPlayer3D.js +7 -5
  162. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  163. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  164. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  165. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  166. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  167. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  168. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  169. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  170. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  171. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  172. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  173. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  174. package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
  175. package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
  176. package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
  177. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  178. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  179. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  180. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  181. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  182. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  183. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  184. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  185. package/lib/components/shared/ui/BackButton.js.map +1 -1
  186. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  187. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  188. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  189. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  190. package/lib/components/shared/ui/SplashScreen.js +2 -2
  191. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  192. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  193. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  194. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  195. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  196. package/lib/constants/bodyDimensions.js.map +1 -1
  197. package/lib/constants/bodyRenderingConstants.js.map +1 -1
  198. package/lib/data/archetypeClothing.js.map +1 -1
  199. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  200. package/lib/data/techniqueMappings.js.map +1 -1
  201. package/lib/data/techniques.js.map +1 -1
  202. package/lib/hooks/useActionFeedback.js.map +1 -1
  203. package/lib/hooks/useBalanceAnimations.js.map +1 -1
  204. package/lib/hooks/useCombatTimer.js.map +1 -1
  205. package/lib/hooks/useDebounce.js.map +1 -1
  206. package/lib/hooks/useHUDLayout.d.ts.map +1 -1
  207. package/lib/hooks/useHUDLayout.js +3 -2
  208. package/lib/hooks/useHUDLayout.js.map +1 -1
  209. package/lib/hooks/useHandPoseTransitions.js.map +1 -1
  210. package/lib/hooks/useKeyboardControls.js.map +1 -1
  211. package/lib/hooks/useMatchCountdown.js.map +1 -1
  212. package/lib/hooks/useMuscleActivation.js.map +1 -1
  213. package/lib/hooks/usePauseMenu.js.map +1 -1
  214. package/lib/hooks/usePlayerAnimation.js.map +1 -1
  215. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  216. package/lib/hooks/useRoundTransition.js.map +1 -1
  217. package/lib/hooks/useSkeletalAnimation.d.ts.map +1 -1
  218. package/lib/hooks/useSkeletalAnimation.js +1 -1
  219. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  220. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  221. package/lib/hooks/useThrottle.js.map +1 -1
  222. package/lib/hooks/useTouchControls.js.map +1 -1
  223. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  224. package/lib/hooks/useWindowSize.js.map +1 -1
  225. package/lib/systems/CombatSystem.js.map +1 -1
  226. package/lib/systems/EffectCalculator.js.map +1 -1
  227. package/lib/systems/LayoutSystem.js.map +1 -1
  228. package/lib/systems/PlayerEffectManager.js.map +1 -1
  229. package/lib/systems/ResponsiveScaling.js.map +1 -1
  230. package/lib/systems/TrigramSystem.js.map +1 -1
  231. package/lib/systems/VitalPointSystem.js.map +1 -1
  232. package/lib/systems/ai/AIPersonality.js.map +1 -1
  233. package/lib/systems/ai/AdaptiveDifficulty.js +16 -16
  234. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  235. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  236. package/lib/systems/ai/ComboSystem.js.map +1 -1
  237. package/lib/systems/ai/DecisionTree.js.map +1 -1
  238. package/lib/systems/ai/TrainingAI.js.map +1 -1
  239. package/lib/systems/ai/types.js.map +1 -1
  240. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  241. package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
  242. package/lib/systems/animation/builders/HandPoses.js.map +1 -1
  243. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  244. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  245. package/lib/systems/animation/builders/KickPhaseApplicator.d.ts +6 -0
  246. package/lib/systems/animation/builders/KickPhaseApplicator.d.ts.map +1 -1
  247. package/lib/systems/animation/builders/KickPhaseApplicator.js +16 -9
  248. package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
  249. package/lib/systems/animation/builders/KoreanGuardPositions.d.ts +4 -4
  250. package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
  251. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts +1 -1
  252. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts.map +1 -1
  253. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js +5 -5
  254. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  255. package/lib/systems/animation/builders/MartialArtsConstants.d.ts +112 -71
  256. package/lib/systems/animation/builders/MartialArtsConstants.d.ts.map +1 -1
  257. package/lib/systems/animation/builders/MartialArtsConstants.js +113 -72
  258. package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
  259. package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
  260. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  261. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  262. package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
  263. package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
  264. package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
  265. package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
  266. package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
  267. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  268. package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
  269. package/lib/systems/animation/catalogs/EnhancedAttackAnimations.js.map +1 -1
  270. package/lib/systems/animation/catalogs/EnhancedElbowKneeAnimations.js.map +1 -1
  271. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  272. package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
  273. package/lib/systems/animation/catalogs/GamStanceAnimations.js +21 -0
  274. package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -0
  275. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js +34 -2
  276. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
  277. package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
  278. package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
  279. package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
  280. package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts +9 -0
  281. package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts.map +1 -1
  282. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +288 -0
  283. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -0
  284. package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
  285. package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
  286. package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
  287. package/lib/systems/animation/catalogs/KickAnimations.d.ts +2 -2
  288. package/lib/systems/animation/catalogs/KickAnimations.js +2 -2
  289. package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
  290. package/lib/systems/animation/catalogs/LiStanceAnimations.js +14 -1
  291. package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
  292. package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
  293. package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
  294. package/lib/systems/animation/catalogs/PunchAnimations.d.ts +1 -1
  295. package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
  296. package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
  297. package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
  298. package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
  299. package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
  300. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
  301. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  302. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  303. package/lib/systems/animation/catalogs/StanceGuardPoses.d.ts +6 -6
  304. package/lib/systems/animation/catalogs/StanceGuardPoses.js +36 -36
  305. package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
  306. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  307. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  308. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  309. package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
  310. package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
  311. package/lib/systems/animation/constants/AnatomicalLimits.js.map +1 -1
  312. package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
  313. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  314. package/lib/systems/animation/core/AnimationPriority.js +15 -15
  315. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  316. package/lib/systems/animation/core/AnimationRegistry.d.ts +30 -0
  317. package/lib/systems/animation/core/AnimationRegistry.d.ts.map +1 -1
  318. package/lib/systems/animation/core/AnimationRegistry.js +74 -12
  319. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  320. package/lib/systems/animation/core/AnimationStateMachine.js +16 -16
  321. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  322. package/lib/systems/animation/core/AnimationTransitions.d.ts.map +1 -1
  323. package/lib/systems/animation/core/AnimationTransitions.js +34 -0
  324. package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
  325. package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
  326. package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
  327. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  328. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  329. package/lib/systems/animation/core/index.d.ts +1 -1
  330. package/lib/systems/animation/core/index.d.ts.map +1 -1
  331. package/lib/systems/animation/core/types.d.ts +24 -0
  332. package/lib/systems/animation/core/types.d.ts.map +1 -1
  333. package/lib/systems/animation/core/types.js +27 -11
  334. package/lib/systems/animation/core/types.js.map +1 -1
  335. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  336. package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
  337. package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
  338. package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
  339. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  340. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  341. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  342. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  343. package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
  344. package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
  345. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  346. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  347. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  348. package/lib/systems/bodypart/types.js.map +1 -1
  349. package/lib/systems/breathing/BreathingDisruptionSystem.js +19 -19
  350. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  351. package/lib/systems/breathing/feedback.js.map +1 -1
  352. package/lib/systems/breathing/integration.js.map +1 -1
  353. package/lib/systems/combat/BalanceSystem.js +19 -19
  354. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  355. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  356. package/lib/systems/combat/CombatStateSystem.js +17 -17
  357. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  358. package/lib/systems/combat/ConsciousnessSystem.js +24 -24
  359. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  360. package/lib/systems/combat/FallIntegration.js.map +1 -1
  361. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  362. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  363. package/lib/systems/combat/PainResponseSystem.js +21 -21
  364. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  365. package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
  366. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  367. package/lib/systems/combat/typeGuards.js.map +1 -1
  368. package/lib/systems/effects.js.map +1 -1
  369. package/lib/systems/game.js.map +1 -1
  370. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  371. package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
  372. package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
  373. package/lib/systems/movement/integration.js.map +1 -1
  374. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  375. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  376. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  377. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  378. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  379. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  380. package/lib/systems/physics/SpeedModifierSystem.js +6 -6
  381. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  382. package/lib/systems/trigram/KoreanCulture.js.map +1 -1
  383. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  384. package/lib/systems/trigram/StanceManager.js.map +1 -1
  385. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  386. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  387. package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
  388. package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
  389. package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
  390. package/lib/systems/trigram/techniques/GeonTechniques.js.map +1 -1
  391. package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
  392. package/lib/systems/trigram/techniques/JinTechniques.js.map +1 -1
  393. package/lib/systems/trigram/techniques/LiTechniques.js.map +1 -1
  394. package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
  395. package/lib/systems/trigram/techniques/TaeTechniques.js.map +1 -1
  396. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  397. package/lib/systems/trigram/techniques/index.js.map +1 -1
  398. package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
  399. package/lib/systems/trigram/types.js.map +1 -1
  400. package/lib/systems/types.js.map +1 -1
  401. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  402. package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
  403. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  404. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  405. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  406. package/lib/systems/vitalpoint/VitalPointsData.js.map +1 -1
  407. package/lib/types/AccessibilityTypes.js.map +1 -1
  408. package/lib/types/LayoutTypes.js.map +1 -1
  409. package/lib/types/PhysicsTypes.js.map +1 -1
  410. package/lib/types/common.js.map +1 -1
  411. package/lib/types/constants/animations.js.map +1 -1
  412. package/lib/types/constants/colors.js.map +1 -1
  413. package/lib/types/constants/designSystem.js.map +1 -1
  414. package/lib/types/constants/index.js.map +1 -1
  415. package/lib/types/constants/layout.d.ts +21 -0
  416. package/lib/types/constants/layout.d.ts.map +1 -1
  417. package/lib/types/constants/layout.js +22 -1
  418. package/lib/types/constants/layout.js.map +1 -1
  419. package/lib/types/constants/performance.js.map +1 -1
  420. package/lib/types/constants/typography.js.map +1 -1
  421. package/lib/types/constants/ui.js.map +1 -1
  422. package/lib/types/facial.js +19 -19
  423. package/lib/types/facial.js.map +1 -1
  424. package/lib/types/hand-animation.js.map +1 -1
  425. package/lib/types/injury.js.map +1 -1
  426. package/lib/types/muscle.js.map +1 -1
  427. package/lib/types/physics.js.map +1 -1
  428. package/lib/types/physicsConstants.js.map +1 -1
  429. package/lib/types/player-visual.d.ts +1 -1
  430. package/lib/types/player-visual.d.ts.map +1 -1
  431. package/lib/types/skeletal.js.map +1 -1
  432. package/lib/types/techniqueId.js.map +1 -1
  433. package/lib/utils/accessibility.js.map +1 -1
  434. package/lib/utils/arenaWorldDimensions.js.map +1 -1
  435. package/lib/utils/assetConfig.js.map +1 -1
  436. package/lib/utils/characterScaling.js.map +1 -1
  437. package/lib/utils/colorHelpers.js.map +1 -1
  438. package/lib/utils/colorUtils.js.map +1 -1
  439. package/lib/utils/combatReadiness.js.map +1 -1
  440. package/lib/utils/controlMapping.js.map +1 -1
  441. package/lib/utils/deviceDetection.js +6 -7
  442. package/lib/utils/deviceDetection.js.map +1 -1
  443. package/lib/utils/effectUtils.js.map +1 -1
  444. package/lib/utils/fabricTextures.js.map +1 -1
  445. package/lib/utils/hapticFeedback.js.map +1 -1
  446. package/lib/utils/haptics.js.map +1 -1
  447. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  448. package/lib/utils/inputSystem.js.map +1 -1
  449. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  450. package/lib/utils/math.js.map +1 -1
  451. package/lib/utils/mobileLayoutHelpers.js.map +1 -1
  452. package/lib/utils/mobileUIUtils.js.map +1 -1
  453. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  454. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  455. package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
  456. package/lib/utils/performanceOptimization.js.map +1 -1
  457. package/lib/utils/player3DHelpers.js.map +1 -1
  458. package/lib/utils/playerUtils.js.map +1 -1
  459. package/lib/utils/responsiveLayout.js.map +1 -1
  460. package/lib/utils/responsiveLayoutHelpers.d.ts +7 -0
  461. package/lib/utils/responsiveLayoutHelpers.d.ts.map +1 -1
  462. package/lib/utils/responsiveLayoutHelpers.js +16 -2
  463. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  464. package/lib/utils/responsiveOrientationConstants.js.map +1 -1
  465. package/lib/utils/safeAreaUtils.js.map +1 -1
  466. package/lib/utils/sharedPhysicsConfig.js.map +1 -1
  467. package/lib/utils/skeletonScaling.js.map +1 -1
  468. package/lib/utils/stanceHelpers.js.map +1 -1
  469. package/lib/utils/threeObjectPool.js.map +1 -1
  470. package/lib/utils/visualEffects.js.map +1 -1
  471. package/package.json +7 -7
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiFA,IAAa,yBAA+D,EAC1E,WACA,UAAU,MACV,eAAe,MACf,mBAAmB,SACf;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAA4B,EAAE,CAAC;CACjE,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;;;;CAgCvC,iBAAiB;EACf,WA5BoB,aACnB,YAA0B;GACzB,UAAU,QAAQ;GAElB,IACE,gBACA,QAAQ,SAAS,KAAA,KACjB,QAAQ,SAAS,KAAA,GACjB;IACA,MAAM,WAA4B;KAChC,IAAI;KACJ,MAAM,QAAQ;KACd,WAAW,KAAK,KAAK;KACrB,GAAG,QAAQ;KACX,GAAG,QAAQ;KACZ;IAED,cAAc,SAAS,CAAC,GAAG,MAAM,SAAS,CAAC;IAC3C,WAAW,SAAS,OAAO,EAAE;;KAGjC;GAAC;GAAW;GAAc;GAAO,CAOtB;EACX;EACA;EACD,CAAC;;;;CAKF,gBAAgB;EACd,IAAI,CAAC,cAAc;EAEnB,MAAM,WAAW,kBAAkB;GACjC,MAAM,MAAM,KAAK,KAAK;GACtB,cAAc,SACZ,KACG,QAAQ,OAAO,MAAM,GAAG,YAAY,IAAK,CACzC,KAAK,QAAQ;IAAE,GAAG;IAAI,KAAK,MAAM,GAAG;IAAW,EAAE,CACrD;KACA,IAAI;EAEP,aAAa,cAAc,SAAS;IACnC,CAAC,aAAa,CAAC;CAElB,IAAI,CAAC,cACH,OAAO;CAGT,MAAM,eAAe,YAAY,cAAc,aAAa;CAC5D,MAAM,YAAY,YAAY,cAAc,YAAY;;;;CAKxD,MAAM,qBACJ,SACsD;EAYtD,OACE;GARA,eAAe;IAAE,QAAQ;IAAM,SAAS;IAAW,MAAM;IAAK;GAC9D,cAAc;IAAE,QAAQ;IAAM,SAAS;IAAW,MAAM;IAAK;GAC7D,YAAY;IAAE,QAAQ;IAAM,SAAS;IAAQ,MAAM;IAAK;GACxD,cAAc;IAAE,QAAQ;IAAM,SAAS;IAAO,MAAM;IAAK;GACzD,kBAAkB;IAAE,QAAQ;IAAM,SAAS;IAAe,MAAM;IAAM;GACtE,KAAK;IAAE,QAAQ;IAAM,SAAS;IAAO,MAAM;IAAM;GAGjD,CAAS,SAAS;GAAE,QAAQ;GAAO,SAAS;GAAW,MAAM;GAAK;;CAItE,OACE,qBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,eAAe;GACf,QAAQ;GACT;EACD,eAAY;YAVd,CAaG,UAAU,KAAK,aAAa;GAC3B,MAAM,MAAM,SAAS,OAAO;GAC5B,MAAM,UAAU,KAAK,IAAI,GAAG,IAAI,MAAM,IAAK;GAC3C,MAAM,QAAQ,IAAI,MAAM;GACxB,MAAM,UAAU,kBAAkB,SAAS,KAAK;GAEhD,OACE,qBAAC,OAAD;IAEE,OAAO;KACL,UAAU;KACV,MAAM,GAAG,SAAS,EAAE;KACpB,KAAK,GAAG,SAAS,EAAE;KACnB,WAAW,+BAA+B,MAAM;KAChD;KACA,YAAY;KACZ,SAAS;KACT,eAAe;KACf,YAAY;KACZ,KAAK;KACN;IACD,eAAa,yBAAyB,SAAS;cAdjD,CAiBE,oBAAC,OAAD;KACE,OAAO;MACL,UAAU;MACV,OAAO,QAAQ,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,QAAQ;MACvE,YAAY,iBAAiB,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU,GAAI;MAC5F;eAEA,QAAQ;KACL,CAAA,EAGN,qBAAC,OAAD;KACE,OAAO;MACL,YAAY,iBAAiB,UAAU,GAAI;MAC3C,QAAQ,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,QAAQ;MAC3F,cAAc;MACd,SAAS;MACT,UAAU;MACV,YAAY;MACZ,OAAO,QAAQ,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,QAAQ;MAChF,WAAW;MACX,YAAY;MACZ,YAAY;MACb;eAZH;MAcG,QAAQ;MAAO;MAAI,QAAQ;MACxB;OACF;MA3CC,SAAS,GA2CV;IAER,EAGD,WACC,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,KAAK;IACL,OAAO;IACP,YAAY;IACZ,QAAQ,kBAAkB,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE;IAC/E,cAAc;IACd,SAAS;IACT,UAAU;IACV,OAAO,QAAQ,aAAa,EAAE,IAAI,aAAa,EAAE,IAAI,aAAa,EAAE;IACpE,UAAU;IACV,SAAS;IACV;GACD,eAAY;aAdd;IAgBE,oBAAC,OAAD;KAAK,OAAO;MAAE,YAAY;MAAQ,cAAc;MAAO;eAAE;KAEnD,CAAA;IACN,oBAAC,OAAD,EAAA,UAAK,iBAAmB,CAAA;IACxB,oBAAC,OAAD,EAAA,UAAK,mBAAqB,CAAA;IAC1B,oBAAC,OAAD,EAAA,UAAK,iBAAmB,CAAA;IACpB;KAEJ"}
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":"MobileControlsPure.js","names":[],"sources":["../../../../src/components/shared/mobile/MobileControlsPure.tsx"],"sourcesContent":["/**\n * MobileControlsPure - Pure DOM mobile controls (no Three.js/drei dependency)\n *\n * These controls render OUTSIDE the Three.js Canvas for reliable touch event handling.\n * The key difference from VirtualDPad/ActionButtons is that these don't use drei's Html\n * component, which can intercept touch events on mobile devices.\n *\n * Visual style matches the existing Korean cyberpunk aesthetic.\n *\n * @module components/mobile/MobileControlsPure\n * @category Mobile Controls\n * @korean 순수 DOM 모바일 컨트롤\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\n\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\nexport type DPadEventType = \"start\" | \"end\";\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for the combined mobile controls overlay\n */\nexport interface MobileControlsOverlayProps {\n /** Callback when D-Pad direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Callback when attack is pressed */\n readonly onAttack: () => void;\n /** Callback when block is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether controls are disabled */\n readonly disabled?: boolean;\n /** Bottom offset in pixels (default: 160 to clear BottomHUD) */\n readonly bottom?: number;\n /** Opacity (default: 0.85) */\n readonly opacity?: number;\n /** Viewport width for responsive control sizing */\n readonly viewportWidth?: number;\n /** Viewport height for responsive control sizing */\n readonly viewportHeight?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number;\n readonly symbol: string;\n readonly keys: string[]; // Keys to dispatch\n}\n\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, symbol: \"▲\", keys: [\"w\"] },\n { direction: \"up-right\", angle: 45, symbol: \"◥\", keys: [\"w\", \"d\"] },\n { direction: \"right\", angle: 90, symbol: \"▶\", keys: [\"d\"] },\n { direction: \"down-right\", angle: 135, symbol: \"◢\", keys: [\"s\", \"d\"] },\n { direction: \"down\", angle: 180, symbol: \"▼\", keys: [\"s\"] },\n { direction: \"down-left\", angle: 225, symbol: \"◣\", keys: [\"s\", \"a\"] },\n { direction: \"left\", angle: 270, symbol: \"◀\", keys: [\"a\"] },\n { direction: \"up-left\", angle: 315, symbol: \"◤\", keys: [\"w\", \"a\"] },\n] as const;\n\n/**\n * Fallback CSS-pixel viewport used only when a parent does not provide live\n * dimensions. 390×844 matches the common iPhone 13/14/15 CSS viewport class\n * and approximates many mid-size Android portrait viewports, avoiding oversized\n * controls on compact devices.\n */\nconst DEFAULT_MOBILE_VIEWPORT = {\n width: 390,\n height: 844,\n} as const;\n\n/**\n * D-Pad diameter target as a ratio of the shortest viewport side. 34% keeps the\n * full radial control reachable by thumb while each directional button remains\n * at or above the 44px WCAG touch target minimum after clamping.\n */\nconst DPAD_SHORTEST_SIDE_RATIO = 0.34;\n\n/**\n * MobileControlsOverlay - Floating mobile controls rendered outside Canvas\n *\n * Positions D-Pad on left, Action buttons on right, floating above BottomHUD.\n * Uses pure DOM events for reliable mobile touch handling.\n */\nexport const MobileControlsOverlay: React.FC<MobileControlsOverlayProps> =\n React.memo(\n ({\n onMove,\n onAttack,\n onBlock,\n disabled = false,\n bottom = 160,\n opacity = 0.85,\n viewportWidth = DEFAULT_MOBILE_VIEWPORT.width,\n viewportHeight = DEFAULT_MOBILE_VIEWPORT.height,\n }) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n\n const controlLayout = useMemo(() => {\n const shortestSide = Math.min(viewportWidth, viewportHeight);\n const dpadSize = Math.round(\n Math.max(\n 112,\n Math.min(140, shortestSide * DPAD_SHORTEST_SIDE_RATIO),\n ),\n );\n const buttonSize = Math.max(44, Math.round(dpadSize * 0.34));\n const buttonPlacementRadius = dpadSize * 0.32;\n const attackSize = Math.round(\n Math.max(64, Math.min(80, dpadSize * 0.58)),\n );\n const blockSize = Math.round(\n Math.max(54, Math.min(65, dpadSize * 0.47)),\n );\n const sidePadding = Math.round(\n Math.max(12, Math.min(20, viewportWidth * 0.04)),\n );\n\n return {\n dpadSize,\n buttonSize,\n buttonPlacementRadius,\n attackSize,\n blockSize,\n sidePadding,\n overlayHeight: dpadSize + 32,\n actionGap: Math.max(8, Math.round(dpadSize * 0.08)),\n };\n }, [viewportHeight, viewportWidth]);\n\n const handleDPadStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(direction);\n triggerHaptic(\"light\");\n onMove(direction, \"start\");\n },\n [disabled, onMove],\n );\n\n const handleDPadEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(null);\n onMove(null, \"end\");\n },\n [disabled, onMove],\n );\n\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(true);\n triggerHaptic(\"medium\");\n onAttack();\n },\n [disabled, onAttack],\n );\n\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(false);\n },\n [],\n );\n\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(true);\n triggerHaptic(\"light\");\n onBlock(\"start\");\n },\n [disabled, onBlock],\n );\n\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(false);\n onBlock(\"end\");\n },\n [onBlock],\n );\n\n const glowStyle = useMemo(\n () => ({\n boxShadow: `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}, inset 0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }),\n [],\n );\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${bottom}px`,\n left: 0,\n right: 0,\n height: `${controlLayout.overlayHeight}px`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"flex-end\",\n padding: `0 ${controlLayout.sidePadding}px`,\n pointerEvents: \"none\",\n zIndex: 1000,\n opacity: disabled ? 0.4 : opacity,\n }}\n data-testid=\"mobile-controls-overlay\"\n >\n {/* D-Pad (Left Side) */}\n <div\n style={{\n position: \"relative\",\n width: `${controlLayout.dpadSize}px`,\n height: `${controlLayout.dpadSize}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-dpad\"\n >\n {/* D-Pad Background Circle */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: `${controlLayout.dpadSize * 0.9}px`,\n height: `${controlLayout.dpadSize * 0.9}px`,\n borderRadius: \"50%\",\n background: `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95)} 100%)`,\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.5)}`,\n ...glowStyle,\n }}\n />\n\n {/* Direction Buttons */}\n {DIRECTIONS.map((config) => {\n const radian = (config.angle - 90) * (Math.PI / 180);\n const x = Math.cos(radian) * controlLayout.buttonPlacementRadius;\n const y = Math.sin(radian) * controlLayout.buttonPlacementRadius;\n const isActive = activeDirection === config.direction;\n\n return (\n <button\n key={config.direction}\n onTouchStart={(e) => handleDPadStart(e, config.direction)}\n onTouchEnd={handleDPadEnd}\n onTouchCancel={handleDPadEnd}\n onMouseDown={(e) => handleDPadStart(e, config.direction)}\n onMouseUp={handleDPadEnd}\n onMouseLeave={handleDPadEnd}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${controlLayout.buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${controlLayout.buttonSize / 2}px)`,\n width: `${controlLayout.buttonSize}px`,\n height: `${controlLayout.buttonSize}px`,\n borderRadius: \"50%\",\n background: isActive\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 100%)`,\n border: `2px solid ${hexToRgbaString(isActive ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.PRIMARY_CYAN, isActive ? 1 : 0.7)}`,\n fontSize: \"14px\",\n color: isActive\n ? hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 1)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: isActive ? \"scale(1.15)\" : \"scale(1)\",\n transition: \"transform 0.1s ease, background 0.1s ease\",\n boxShadow: isActive\n ? `0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8)}`\n : \"none\",\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n }}\n aria-label={`이동 ${config.direction} | Move ${config.direction}`}\n data-testid={`mobile-dpad-${config.direction}`}\n >\n {config.symbol}\n </button>\n );\n })}\n\n {/* Center Indicator */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"20px\",\n height: \"20px\",\n borderRadius: \"50%\",\n background: activeDirection\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)\n : hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.8)}`,\n transition: \"background 0.15s ease\",\n }}\n />\n </div>\n\n {/* Action Buttons (Right Side) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${controlLayout.actionGap}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-action-buttons\"\n >\n {/* Attack Button - Large Red */}\n <button\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onTouchCancel={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n style={{\n width: `${controlLayout.attackSize}px`,\n height: `${controlLayout.attackSize}px`,\n borderRadius: \"50%\",\n background: attackPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.NEGATIVE_RED_DARK, 0.9)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)}`,\n fontSize: \"28px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: attackPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: attackPressed\n ? `0 0 25px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)}`\n : `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.5)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"공격 | Attack\"\n data-testid=\"mobile-attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button - Smaller Cyan */}\n <button\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onTouchCancel={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n style={{\n width: `${controlLayout.blockSize}px`,\n height: `${controlLayout.blockSize}px`,\n borderRadius: \"50%\",\n marginLeft: \"auto\",\n background: blockPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.KI_LOW, 0.8)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n fontSize: \"22px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: blockPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: blockPressed\n ? `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9)}`\n : `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"방어 | Block\"\n data-testid=\"mobile-block-button\"\n >\n 🛡️\n </button>\n </div>\n </div>\n );\n },\n );\n\nMobileControlsOverlay.displayName = \"MobileControlsOverlay\";\n\nexport default MobileControlsOverlay;\n"],"mappings":"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"}
1
+ {"version":3,"file":"MobileControlsPure.js","names":[],"sources":["../../../../src/components/shared/mobile/MobileControlsPure.tsx"],"sourcesContent":["/**\n * MobileControlsPure - Pure DOM mobile controls (no Three.js/drei dependency)\n *\n * These controls render OUTSIDE the Three.js Canvas for reliable touch event handling.\n * The key difference from VirtualDPad/ActionButtons is that these don't use drei's Html\n * component, which can intercept touch events on mobile devices.\n *\n * Visual style matches the existing Korean cyberpunk aesthetic.\n *\n * @module components/mobile/MobileControlsPure\n * @category Mobile Controls\n * @korean 순수 DOM 모바일 컨트롤\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\n\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\nexport type DPadEventType = \"start\" | \"end\";\nexport type ButtonEventType = \"start\" | \"end\";\n\n/**\n * Props for the combined mobile controls overlay\n */\nexport interface MobileControlsOverlayProps {\n /** Callback when D-Pad direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Callback when attack is pressed */\n readonly onAttack: () => void;\n /** Callback when block is pressed/released */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Whether controls are disabled */\n readonly disabled?: boolean;\n /** Bottom offset in pixels (default: 160 to clear BottomHUD) */\n readonly bottom?: number;\n /** Opacity (default: 0.85) */\n readonly opacity?: number;\n /** Viewport width for responsive control sizing */\n readonly viewportWidth?: number;\n /** Viewport height for responsive control sizing */\n readonly viewportHeight?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number;\n readonly symbol: string;\n readonly keys: string[]; // Keys to dispatch\n}\n\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, symbol: \"▲\", keys: [\"w\"] },\n { direction: \"up-right\", angle: 45, symbol: \"◥\", keys: [\"w\", \"d\"] },\n { direction: \"right\", angle: 90, symbol: \"▶\", keys: [\"d\"] },\n { direction: \"down-right\", angle: 135, symbol: \"◢\", keys: [\"s\", \"d\"] },\n { direction: \"down\", angle: 180, symbol: \"▼\", keys: [\"s\"] },\n { direction: \"down-left\", angle: 225, symbol: \"◣\", keys: [\"s\", \"a\"] },\n { direction: \"left\", angle: 270, symbol: \"◀\", keys: [\"a\"] },\n { direction: \"up-left\", angle: 315, symbol: \"◤\", keys: [\"w\", \"a\"] },\n] as const;\n\n/**\n * Fallback CSS-pixel viewport used only when a parent does not provide live\n * dimensions. 390×844 matches the common iPhone 13/14/15 CSS viewport class\n * and approximates many mid-size Android portrait viewports, avoiding oversized\n * controls on compact devices.\n */\nconst DEFAULT_MOBILE_VIEWPORT = {\n width: 390,\n height: 844,\n} as const;\n\n/**\n * D-Pad diameter target as a ratio of the shortest viewport side. 34% keeps the\n * full radial control reachable by thumb while each directional button remains\n * at or above the 44px WCAG touch target minimum after clamping.\n */\nconst DPAD_SHORTEST_SIDE_RATIO = 0.34;\n\n/**\n * MobileControlsOverlay - Floating mobile controls rendered outside Canvas\n *\n * Positions D-Pad on left, Action buttons on right, floating above BottomHUD.\n * Uses pure DOM events for reliable mobile touch handling.\n */\nexport const MobileControlsOverlay: React.FC<MobileControlsOverlayProps> =\n React.memo(\n ({\n onMove,\n onAttack,\n onBlock,\n disabled = false,\n bottom = 160,\n opacity = 0.85,\n viewportWidth = DEFAULT_MOBILE_VIEWPORT.width,\n viewportHeight = DEFAULT_MOBILE_VIEWPORT.height,\n }) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [attackPressed, setAttackPressed] = useState(false);\n const [blockPressed, setBlockPressed] = useState(false);\n\n const controlLayout = useMemo(() => {\n const shortestSide = Math.min(viewportWidth, viewportHeight);\n const dpadSize = Math.round(\n Math.max(\n 112,\n Math.min(140, shortestSide * DPAD_SHORTEST_SIDE_RATIO),\n ),\n );\n const buttonSize = Math.max(44, Math.round(dpadSize * 0.34));\n const buttonPlacementRadius = dpadSize * 0.32;\n const attackSize = Math.round(\n Math.max(64, Math.min(80, dpadSize * 0.58)),\n );\n const blockSize = Math.round(\n Math.max(54, Math.min(65, dpadSize * 0.47)),\n );\n const sidePadding = Math.round(\n Math.max(12, Math.min(20, viewportWidth * 0.04)),\n );\n\n return {\n dpadSize,\n buttonSize,\n buttonPlacementRadius,\n attackSize,\n blockSize,\n sidePadding,\n overlayHeight: dpadSize + 32,\n actionGap: Math.max(8, Math.round(dpadSize * 0.08)),\n };\n }, [viewportHeight, viewportWidth]);\n\n const handleDPadStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(direction);\n triggerHaptic(\"light\");\n onMove(direction, \"start\");\n },\n [disabled, onMove],\n );\n\n const handleDPadEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setActiveDirection(null);\n onMove(null, \"end\");\n },\n [disabled, onMove],\n );\n\n const handleAttackStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(true);\n triggerHaptic(\"medium\");\n onAttack();\n },\n [disabled, onAttack],\n );\n\n const handleAttackEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setAttackPressed(false);\n },\n [],\n );\n\n const handleBlockStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(true);\n triggerHaptic(\"light\");\n onBlock(\"start\");\n },\n [disabled, onBlock],\n );\n\n const handleBlockEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setBlockPressed(false);\n onBlock(\"end\");\n },\n [onBlock],\n );\n\n const glowStyle = useMemo(\n () => ({\n boxShadow: `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}, inset 0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }),\n [],\n );\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${bottom}px`,\n left: 0,\n right: 0,\n height: `${controlLayout.overlayHeight}px`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"flex-end\",\n padding: `0 ${controlLayout.sidePadding}px`,\n pointerEvents: \"none\",\n zIndex: 1000,\n opacity: disabled ? 0.4 : opacity,\n }}\n data-testid=\"mobile-controls-overlay\"\n >\n {/* D-Pad (Left Side) */}\n <div\n style={{\n position: \"relative\",\n width: `${controlLayout.dpadSize}px`,\n height: `${controlLayout.dpadSize}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-dpad\"\n >\n {/* D-Pad Background Circle */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: `${controlLayout.dpadSize * 0.9}px`,\n height: `${controlLayout.dpadSize * 0.9}px`,\n borderRadius: \"50%\",\n background: `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95)} 100%)`,\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.5)}`,\n ...glowStyle,\n }}\n />\n\n {/* Direction Buttons */}\n {DIRECTIONS.map((config) => {\n const radian = (config.angle - 90) * (Math.PI / 180);\n const x = Math.cos(radian) * controlLayout.buttonPlacementRadius;\n const y = Math.sin(radian) * controlLayout.buttonPlacementRadius;\n const isActive = activeDirection === config.direction;\n\n return (\n <button\n key={config.direction}\n onTouchStart={(e) => handleDPadStart(e, config.direction)}\n onTouchEnd={handleDPadEnd}\n onTouchCancel={handleDPadEnd}\n onMouseDown={(e) => handleDPadStart(e, config.direction)}\n onMouseUp={handleDPadEnd}\n onMouseLeave={handleDPadEnd}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${controlLayout.buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${controlLayout.buttonSize / 2}px)`,\n width: `${controlLayout.buttonSize}px`,\n height: `${controlLayout.buttonSize}px`,\n borderRadius: \"50%\",\n background: isActive\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 100%)`,\n border: `2px solid ${hexToRgbaString(isActive ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.PRIMARY_CYAN, isActive ? 1 : 0.7)}`,\n fontSize: \"14px\",\n color: isActive\n ? hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 1)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: isActive ? \"scale(1.15)\" : \"scale(1)\",\n transition: \"transform 0.1s ease, background 0.1s ease\",\n boxShadow: isActive\n ? `0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8)}`\n : \"none\",\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n }}\n aria-label={`이동 ${config.direction} | Move ${config.direction}`}\n data-testid={`mobile-dpad-${config.direction}`}\n >\n {config.symbol}\n </button>\n );\n })}\n\n {/* Center Indicator */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"20px\",\n height: \"20px\",\n borderRadius: \"50%\",\n background: activeDirection\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9)\n : hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.8)}`,\n transition: \"background 0.15s ease\",\n }}\n />\n </div>\n\n {/* Action Buttons (Right Side) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${controlLayout.actionGap}px`,\n pointerEvents: \"auto\",\n touchAction: \"none\",\n }}\n data-testid=\"mobile-action-buttons\"\n >\n {/* Attack Button - Large Red */}\n <button\n onTouchStart={handleAttackStart}\n onTouchEnd={handleAttackEnd}\n onTouchCancel={handleAttackEnd}\n onMouseDown={handleAttackStart}\n onMouseUp={handleAttackEnd}\n onMouseLeave={handleAttackEnd}\n style={{\n width: `${controlLayout.attackSize}px`,\n height: `${controlLayout.attackSize}px`,\n borderRadius: \"50%\",\n background: attackPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.NEGATIVE_RED_DARK, 0.9)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 1)}`,\n fontSize: \"28px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: attackPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: attackPressed\n ? `0 0 25px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.9)}`\n : `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_RED, 0.5)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"공격 | Attack\"\n data-testid=\"mobile-attack-button\"\n >\n ⚡\n </button>\n\n {/* Block Button - Smaller Cyan */}\n <button\n onTouchStart={handleBlockStart}\n onTouchEnd={handleBlockEnd}\n onTouchCancel={handleBlockEnd}\n onMouseDown={handleBlockStart}\n onMouseUp={handleBlockEnd}\n onMouseLeave={handleBlockEnd}\n style={{\n width: `${controlLayout.blockSize}px`,\n height: `${controlLayout.blockSize}px`,\n borderRadius: \"50%\",\n marginLeft: \"auto\",\n background: blockPressed\n ? `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)} 0%, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)} 100%)`\n : `radial-gradient(circle, ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)} 0%, ${hexToRgbaString(KOREAN_COLORS.KI_LOW, 0.8)} 100%)`,\n border: `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n fontSize: \"22px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 1),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n touchAction: \"none\",\n transform: blockPressed ? \"scale(0.92)\" : \"scale(1)\",\n transition: \"transform 0.1s ease\",\n boxShadow: blockPressed\n ? `0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9)}`\n : `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n outline: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n aria-label=\"방어 | Block\"\n data-testid=\"mobile-block-button\"\n >\n 🛡️\n </button>\n </div>\n </div>\n );\n },\n );\n\nMobileControlsOverlay.displayName = \"MobileControlsOverlay\";\n\nexport default MobileControlsOverlay;\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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TouchOptimizer.js","names":[],"sources":["../../../../src/components/shared/mobile/TouchOptimizer.ts"],"sourcesContent":["/**\n * TouchOptimizer\n * \n * High-performance touch event handling with <16ms latency\n * Uses requestAnimationFrame for immediate visual updates and\n * requestIdleCallback for deferred state updates to maintain 60fps\n * \n * Key Features:\n * - RAF-based visual updates (<16ms latency)\n * - Touch event coalescing (60-70% overhead reduction)\n * - Passive event listeners where appropriate\n * - Transform-only CSS animations (GPU-accelerated)\n * \n * @module components/mobile/TouchOptimizer\n * @category Mobile Controls\n * @korean 터치 최적화\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Touch position data\n */\nexport interface TouchPosition {\n readonly x: number;\n readonly y: number;\n readonly timestamp: number;\n}\n\n/**\n * Touch optimization options\n */\nexport interface TouchOptimizerOptions {\n /** Enable touch event coalescing (default: true) */\n readonly enableCoalescing?: boolean;\n /** Use passive listeners where possible (default: true) */\n readonly usePassiveListeners?: boolean;\n /** Enable RAF for visual updates (default: true) */\n readonly useRAF?: boolean;\n /** Coalescing sample rate (default: 3 - use last 3 events) */\n readonly coalescingSampleRate?: number;\n}\n\n/**\n * Touch optimizer return type\n */\nexport interface TouchOptimizerReturn {\n /** Current RAF ID (for debugging) */\n readonly rafId: number | null;\n /** Whether touch is active */\n readonly isTouching: boolean;\n}\n\n/**\n * Custom hook for optimized touch handling with <16ms latency\n * \n * Uses requestAnimationFrame for immediate visual feedback and\n * defers state updates to avoid blocking the main thread\n * \n * @param onTouchStart - Callback for touch start (immediate)\n * @param onTouchMove - Callback for touch move (coalesced)\n * @param onTouchEnd - Callback for touch end (immediate)\n * @param options - Optimization options\n * \n * @example\n * ```tsx\n * const { isTouching } = useTouchOptimizer(\n * (x, y) => {\n * // Immediate visual update (same frame)\n * buttonRef.current.style.transform = 'scale(0.95)';\n * \n * // Defer state update\n * requestIdleCallback(() => {\n * setPressed(true);\n * onAction();\n * });\n * },\n * (x, y) => {\n * // Handle coalesced touch move\n * updatePosition(x, y);\n * },\n * () => {\n * // Immediate visual reset\n * buttonRef.current.style.transform = 'scale(1)';\n * \n * requestIdleCallback(() => {\n * setPressed(false);\n * });\n * }\n * );\n * ```\n * \n * @korean 터치최적화사용\n */\nexport function useTouchOptimizer(\n onTouchStart: (x: number, y: number, timestamp: number) => void,\n onTouchMove: (x: number, y: number, timestamp: number) => void,\n onTouchEnd: (x: number, y: number, timestamp: number) => void,\n options: TouchOptimizerOptions = {}\n): TouchOptimizerReturn {\n const {\n enableCoalescing = true,\n usePassiveListeners = true,\n useRAF = true,\n coalescingSampleRate = 3,\n } = options;\n\n const rafIdRef = useRef<number | null>(null);\n const touchStateRef = useRef<TouchPosition | null>(null);\n const isTouchingRef = useRef<boolean>(false);\n const pendingMoveRef = useRef<TouchPosition | null>(null);\n \n const [rafId, setRafId] = useState<number | null>(null);\n const [isTouching, setIsTouching] = useState<boolean>(false);\n\n /**\n * Cancel pending RAF\n */\n const cancelPendingRAF = useCallback(() => {\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n rafIdRef.current = null;\n setRafId(null);\n }\n }, []);\n\n /**\n * Process touch start with RAF\n */\n const processTouchStart = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n touchStateRef.current = { x, y, timestamp };\n isTouchingRef.current = true;\n setIsTouching(true);\n\n if (useRAF) {\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchStart(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n onTouchStart(x, y, timestamp);\n }\n },\n [onTouchStart, useRAF, cancelPendingRAF]\n );\n\n /**\n * Process coalesced touch move with RAF\n */\n const processTouchMove = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n pendingMoveRef.current = { x, y, timestamp };\n\n if (useRAF && rafIdRef.current === null) {\n rafIdRef.current = requestAnimationFrame(() => {\n if (pendingMoveRef.current) {\n const { x, y, timestamp } = pendingMoveRef.current;\n onTouchMove(x, y, timestamp);\n pendingMoveRef.current = null;\n }\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else if (!useRAF) {\n onTouchMove(x, y, timestamp);\n }\n },\n [onTouchMove, useRAF]\n );\n\n /**\n * Process touch end with RAF\n */\n const processTouchEnd = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n\n if (useRAF) {\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchEnd(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n onTouchEnd(x, y, timestamp);\n }\n },\n [onTouchEnd, useRAF, cancelPendingRAF]\n );\n\n /**\n * Handle touch start event\n */\n const handleTouchStart = useCallback(\n (e: TouchEvent) => {\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.touches[0];\n if (touch) {\n processTouchStart(touch.clientX, touch.clientY);\n }\n },\n [processTouchStart, usePassiveListeners]\n );\n\n /**\n * Handle touch move event with coalescing\n */\n const handleTouchMove = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n let events: readonly Touch[] = [e.touches[0]];\n \n if (enableCoalescing) {\n const eventWithCoalescing = e as TouchEvent & { getCoalescedEvents?: () => TouchEvent[] };\n if (typeof eventWithCoalescing.getCoalescedEvents === 'function') {\n try {\n const coalesced = eventWithCoalescing.getCoalescedEvents();\n if (coalesced && coalesced.length > 0) {\n const recentEvents = coalesced.slice(-coalescingSampleRate);\n events = recentEvents.map((evt: TouchEvent) => evt.touches[0]).filter((touch): touch is Touch => touch !== undefined);\n }\n } catch { /* ignore coalescing errors */ }\n }\n }\n\n const lastTouch = events[events.length - 1];\n if (lastTouch) {\n processTouchMove(lastTouch.clientX, lastTouch.clientY);\n }\n },\n [enableCoalescing, coalescingSampleRate, processTouchMove]\n );\n\n /**\n * Handle touch end event\n */\n const handleTouchEnd = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.changedTouches[0];\n if (touch) {\n processTouchEnd(touch.clientX, touch.clientY);\n }\n },\n [processTouchEnd, usePassiveListeners]\n );\n\n /**\n * Handle touch cancel event\n */\n const handleTouchCancel = useCallback(() => {\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n cancelPendingRAF();\n }, [cancelPendingRAF]);\n\n /**\n * Setup touch event listeners\n * \n * Note: Event listeners are attached to the document for each component instance.\n * This allows independent touch handling per component but may result in multiple\n * document-level listeners if many components use this hook simultaneously.\n * For applications with many touch-optimized components, consider implementing\n * an event delegation pattern or singleton event manager for better efficiency.\n */\n useEffect(() => {\n const options: AddEventListenerOptions = {\n passive: usePassiveListeners,\n };\n\n document.addEventListener('touchstart', handleTouchStart, options);\n document.addEventListener('touchmove', handleTouchMove, options);\n document.addEventListener('touchend', handleTouchEnd, options);\n document.addEventListener('touchcancel', handleTouchCancel, options);\n\n return () => {\n document.removeEventListener('touchstart', handleTouchStart);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n document.removeEventListener('touchcancel', handleTouchCancel);\n cancelPendingRAF();\n };\n }, [\n handleTouchStart,\n handleTouchMove,\n handleTouchEnd,\n handleTouchCancel,\n usePassiveListeners,\n cancelPendingRAF,\n ]);\n\n return {\n rafId,\n isTouching,\n };\n}\n\n/**\n * Helper function to create optimized visual updates\n * Updates DOM directly for immediate feedback, defers state\n * \n * @param element - DOM element to update\n * @param visualUpdate - Function to update visual state (runs in RAF)\n * @param stateUpdate - Function to update React state (runs in idle)\n * \n * @example\n * ```tsx\n * applyOptimizedUpdate(\n * buttonRef.current,\n * (el) => {\n * // Immediate visual feedback (<16ms)\n * el.style.transform = 'scale(0.95)';\n * el.style.filter = 'brightness(1.2)';\n * },\n * () => {\n * // Deferred state update (non-blocking)\n * setPressed(true);\n * onAction();\n * }\n * );\n * ```\n * \n * @korean 최적화된업데이트적용\n */\nexport function applyOptimizedUpdate(\n element: HTMLElement | null,\n visualUpdate: (element: HTMLElement) => void,\n stateUpdate: () => void\n): void {\n if (element) {\n requestAnimationFrame(() => {\n visualUpdate(element);\n });\n }\n\n if (typeof (window as Window & typeof globalThis).requestIdleCallback === 'function') {\n (window as Window & typeof globalThis).requestIdleCallback(() => {\n stateUpdate();\n });\n } else {\n setTimeout(stateUpdate, 0);\n }\n}\n\n/**\n * Create transform-only style for GPU-accelerated animations\n * Avoids layout thrashing by only using transform\n * \n * @param pressed - Whether element is pressed\n * @param scale - Scale value when pressed (default: 0.95)\n * \n * @returns CSS transform string\n * \n * @example\n * ```tsx\n * const style = {\n * transform: createTransformStyle(isPressed, 0.95),\n * transition: 'transform 0.1s ease-out',\n * willChange: 'transform', // Hint to GPU\n * };\n * ```\n * \n * @korean 변환스타일생성\n */\nexport function createTransformStyle(\n pressed: boolean,\n scale: number = 0.95\n): string {\n if (pressed) {\n return `scale(${scale})`;\n }\n return 'scale(1)';\n}\n\n/**\n * Create filter style for visual effects\n * \n * @param pressed - Whether element is pressed\n * @param brightness - Brightness multiplier when pressed (default: 1.2)\n * \n * @returns CSS filter string\n * \n * @korean 필터스타일생성\n */\nexport function createFilterStyle(\n pressed: boolean,\n brightness: number = 1.2\n): string {\n if (pressed) {\n return `brightness(${brightness})`;\n }\n return 'brightness(1)';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4VA,SAAgB,qBACd,SACA,cACA,aACM;CACN,IAAI,SACF,4BAA4B;EAC1B,aAAa,QAAQ;GACrB;CAGJ,IAAI,OAAQ,OAAsC,wBAAwB,YACxE,OAAuC,0BAA0B;EAC/D,aAAa;GACb;MAEF,WAAW,aAAa,EAAE;;;;;;;;;;;;;;;;;;;;;;AAwB9B,SAAgB,qBACd,SACA,QAAgB,KACR;CACR,IAAI,SACF,OAAO,SAAS,MAAM;CAExB,OAAO;;;;;;;;;;;;AAaT,SAAgB,kBACd,SACA,aAAqB,KACb;CACR,IAAI,SACF,OAAO,cAAc,WAAW;CAElC,OAAO"}
1
+ {"version":3,"file":"TouchOptimizer.js","names":[],"sources":["../../../../src/components/shared/mobile/TouchOptimizer.ts"],"sourcesContent":["/**\n * TouchOptimizer\n * \n * High-performance touch event handling with <16ms latency\n * Uses requestAnimationFrame for immediate visual updates and\n * requestIdleCallback for deferred state updates to maintain 60fps\n * \n * Key Features:\n * - RAF-based visual updates (<16ms latency)\n * - Touch event coalescing (60-70% overhead reduction)\n * - Passive event listeners where appropriate\n * - Transform-only CSS animations (GPU-accelerated)\n * \n * @module components/mobile/TouchOptimizer\n * @category Mobile Controls\n * @korean 터치 최적화\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Touch position data\n */\nexport interface TouchPosition {\n readonly x: number;\n readonly y: number;\n readonly timestamp: number;\n}\n\n/**\n * Touch optimization options\n */\nexport interface TouchOptimizerOptions {\n /** Enable touch event coalescing (default: true) */\n readonly enableCoalescing?: boolean;\n /** Use passive listeners where possible (default: true) */\n readonly usePassiveListeners?: boolean;\n /** Enable RAF for visual updates (default: true) */\n readonly useRAF?: boolean;\n /** Coalescing sample rate (default: 3 - use last 3 events) */\n readonly coalescingSampleRate?: number;\n}\n\n/**\n * Touch optimizer return type\n */\nexport interface TouchOptimizerReturn {\n /** Current RAF ID (for debugging) */\n readonly rafId: number | null;\n /** Whether touch is active */\n readonly isTouching: boolean;\n}\n\n/**\n * Custom hook for optimized touch handling with <16ms latency\n * \n * Uses requestAnimationFrame for immediate visual feedback and\n * defers state updates to avoid blocking the main thread\n * \n * @param onTouchStart - Callback for touch start (immediate)\n * @param onTouchMove - Callback for touch move (coalesced)\n * @param onTouchEnd - Callback for touch end (immediate)\n * @param options - Optimization options\n * \n * @example\n * ```tsx\n * const { isTouching } = useTouchOptimizer(\n * (x, y) => {\n * // Immediate visual update (same frame)\n * buttonRef.current.style.transform = 'scale(0.95)';\n * \n * // Defer state update\n * requestIdleCallback(() => {\n * setPressed(true);\n * onAction();\n * });\n * },\n * (x, y) => {\n * // Handle coalesced touch move\n * updatePosition(x, y);\n * },\n * () => {\n * // Immediate visual reset\n * buttonRef.current.style.transform = 'scale(1)';\n * \n * requestIdleCallback(() => {\n * setPressed(false);\n * });\n * }\n * );\n * ```\n * \n * @korean 터치최적화사용\n */\nexport function useTouchOptimizer(\n onTouchStart: (x: number, y: number, timestamp: number) => void,\n onTouchMove: (x: number, y: number, timestamp: number) => void,\n onTouchEnd: (x: number, y: number, timestamp: number) => void,\n options: TouchOptimizerOptions = {}\n): TouchOptimizerReturn {\n const {\n enableCoalescing = true,\n usePassiveListeners = true,\n useRAF = true,\n coalescingSampleRate = 3,\n } = options;\n\n const rafIdRef = useRef<number | null>(null);\n const touchStateRef = useRef<TouchPosition | null>(null);\n const isTouchingRef = useRef<boolean>(false);\n const pendingMoveRef = useRef<TouchPosition | null>(null);\n \n const [rafId, setRafId] = useState<number | null>(null);\n const [isTouching, setIsTouching] = useState<boolean>(false);\n\n /**\n * Cancel pending RAF\n */\n const cancelPendingRAF = useCallback(() => {\n if (rafIdRef.current !== null) {\n cancelAnimationFrame(rafIdRef.current);\n rafIdRef.current = null;\n setRafId(null);\n }\n }, []);\n\n /**\n * Process touch start with RAF\n */\n const processTouchStart = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n touchStateRef.current = { x, y, timestamp };\n isTouchingRef.current = true;\n setIsTouching(true);\n\n if (useRAF) {\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchStart(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n onTouchStart(x, y, timestamp);\n }\n },\n [onTouchStart, useRAF, cancelPendingRAF]\n );\n\n /**\n * Process coalesced touch move with RAF\n */\n const processTouchMove = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n pendingMoveRef.current = { x, y, timestamp };\n\n if (useRAF && rafIdRef.current === null) {\n rafIdRef.current = requestAnimationFrame(() => {\n if (pendingMoveRef.current) {\n const { x, y, timestamp } = pendingMoveRef.current;\n onTouchMove(x, y, timestamp);\n pendingMoveRef.current = null;\n }\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else if (!useRAF) {\n onTouchMove(x, y, timestamp);\n }\n },\n [onTouchMove, useRAF]\n );\n\n /**\n * Process touch end with RAF\n */\n const processTouchEnd = useCallback(\n (x: number, y: number) => {\n const timestamp = performance.now();\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n\n if (useRAF) {\n cancelPendingRAF();\n rafIdRef.current = requestAnimationFrame(() => {\n onTouchEnd(x, y, timestamp);\n rafIdRef.current = null;\n setRafId(null);\n });\n setRafId(rafIdRef.current);\n } else {\n onTouchEnd(x, y, timestamp);\n }\n },\n [onTouchEnd, useRAF, cancelPendingRAF]\n );\n\n /**\n * Handle touch start event\n */\n const handleTouchStart = useCallback(\n (e: TouchEvent) => {\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.touches[0];\n if (touch) {\n processTouchStart(touch.clientX, touch.clientY);\n }\n },\n [processTouchStart, usePassiveListeners]\n );\n\n /**\n * Handle touch move event with coalescing\n */\n const handleTouchMove = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n let events: readonly Touch[] = [e.touches[0]];\n \n if (enableCoalescing) {\n const eventWithCoalescing = e as TouchEvent & { getCoalescedEvents?: () => TouchEvent[] };\n if (typeof eventWithCoalescing.getCoalescedEvents === 'function') {\n try {\n const coalesced = eventWithCoalescing.getCoalescedEvents();\n if (coalesced && coalesced.length > 0) {\n const recentEvents = coalesced.slice(-coalescingSampleRate);\n events = recentEvents.map((evt: TouchEvent) => evt.touches[0]).filter((touch): touch is Touch => touch !== undefined);\n }\n } catch { /* ignore coalescing errors */ }\n }\n }\n\n const lastTouch = events[events.length - 1];\n if (lastTouch) {\n processTouchMove(lastTouch.clientX, lastTouch.clientY);\n }\n },\n [enableCoalescing, coalescingSampleRate, processTouchMove]\n );\n\n /**\n * Handle touch end event\n */\n const handleTouchEnd = useCallback(\n (e: TouchEvent) => {\n if (!isTouchingRef.current) return;\n\n if (!usePassiveListeners) {\n e.preventDefault();\n }\n\n const touch = e.changedTouches[0];\n if (touch) {\n processTouchEnd(touch.clientX, touch.clientY);\n }\n },\n [processTouchEnd, usePassiveListeners]\n );\n\n /**\n * Handle touch cancel event\n */\n const handleTouchCancel = useCallback(() => {\n isTouchingRef.current = false;\n setIsTouching(false);\n touchStateRef.current = null;\n pendingMoveRef.current = null;\n cancelPendingRAF();\n }, [cancelPendingRAF]);\n\n /**\n * Setup touch event listeners\n * \n * Note: Event listeners are attached to the document for each component instance.\n * This allows independent touch handling per component but may result in multiple\n * document-level listeners if many components use this hook simultaneously.\n * For applications with many touch-optimized components, consider implementing\n * an event delegation pattern or singleton event manager for better efficiency.\n */\n useEffect(() => {\n const options: AddEventListenerOptions = {\n passive: usePassiveListeners,\n };\n\n document.addEventListener('touchstart', handleTouchStart, options);\n document.addEventListener('touchmove', handleTouchMove, options);\n document.addEventListener('touchend', handleTouchEnd, options);\n document.addEventListener('touchcancel', handleTouchCancel, options);\n\n return () => {\n document.removeEventListener('touchstart', handleTouchStart);\n document.removeEventListener('touchmove', handleTouchMove);\n document.removeEventListener('touchend', handleTouchEnd);\n document.removeEventListener('touchcancel', handleTouchCancel);\n cancelPendingRAF();\n };\n }, [\n handleTouchStart,\n handleTouchMove,\n handleTouchEnd,\n handleTouchCancel,\n usePassiveListeners,\n cancelPendingRAF,\n ]);\n\n return {\n rafId,\n isTouching,\n };\n}\n\n/**\n * Helper function to create optimized visual updates\n * Updates DOM directly for immediate feedback, defers state\n * \n * @param element - DOM element to update\n * @param visualUpdate - Function to update visual state (runs in RAF)\n * @param stateUpdate - Function to update React state (runs in idle)\n * \n * @example\n * ```tsx\n * applyOptimizedUpdate(\n * buttonRef.current,\n * (el) => {\n * // Immediate visual feedback (<16ms)\n * el.style.transform = 'scale(0.95)';\n * el.style.filter = 'brightness(1.2)';\n * },\n * () => {\n * // Deferred state update (non-blocking)\n * setPressed(true);\n * onAction();\n * }\n * );\n * ```\n * \n * @korean 최적화된업데이트적용\n */\nexport function applyOptimizedUpdate(\n element: HTMLElement | null,\n visualUpdate: (element: HTMLElement) => void,\n stateUpdate: () => void\n): void {\n if (element) {\n requestAnimationFrame(() => {\n visualUpdate(element);\n });\n }\n\n if (typeof (window as Window & typeof globalThis).requestIdleCallback === 'function') {\n (window as Window & typeof globalThis).requestIdleCallback(() => {\n stateUpdate();\n });\n } else {\n setTimeout(stateUpdate, 0);\n }\n}\n\n/**\n * Create transform-only style for GPU-accelerated animations\n * Avoids layout thrashing by only using transform\n * \n * @param pressed - Whether element is pressed\n * @param scale - Scale value when pressed (default: 0.95)\n * \n * @returns CSS transform string\n * \n * @example\n * ```tsx\n * const style = {\n * transform: createTransformStyle(isPressed, 0.95),\n * transition: 'transform 0.1s ease-out',\n * willChange: 'transform', // Hint to GPU\n * };\n * ```\n * \n * @korean 변환스타일생성\n */\nexport function createTransformStyle(\n pressed: boolean,\n scale: number = 0.95\n): string {\n if (pressed) {\n return `scale(${scale})`;\n }\n return 'scale(1)';\n}\n\n/**\n * Create filter style for visual effects\n * \n * @param pressed - Whether element is pressed\n * @param brightness - Brightness multiplier when pressed (default: 1.2)\n * \n * @returns CSS filter string\n * \n * @korean 필터스타일생성\n */\nexport function createFilterStyle(\n pressed: boolean,\n brightness: number = 1.2\n): string {\n if (pressed) {\n return `brightness(${brightness})`;\n }\n return 'brightness(1)';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4VA,SAAgB,qBACd,SACA,cACA,aACM;CACN,IAAI,SACF,4BAA4B;EAC1B,aAAa,OAAO;CACtB,CAAC;CAGH,IAAI,OAAQ,OAAsC,wBAAwB,YACxE,OAAuC,0BAA0B;EAC/D,YAAY;CACd,CAAC;MAED,WAAW,aAAa,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,qBACd,SACA,QAAgB,KACR;CACR,IAAI,SACF,OAAO,SAAS,MAAM;CAExB,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,kBACd,SACA,aAAqB,KACb;CACR,IAAI,SACF,OAAO,cAAc,WAAW;CAElC,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"VirtualDPad.js","names":[],"sources":["../../../../src/components/shared/mobile/VirtualDPad.tsx"],"sourcesContent":["/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for mobile touch controls\n * Provides tactile movement control with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 directions\n * - Keyboard navigation (Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - role=\"group\" with descriptive label\n * - 48x48px minimum touch targets\n *\n * @module components/mobile/VirtualDPad\n * @category Mobile Controls\n * @korean 가상 방향 패드\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useState, useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * 8 directions for movement control\n */\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\n/**\n * Event type for D-Pad interactions\n */\nexport type DPadEventType = \"start\" | \"end\";\n\n/**\n * Props for VirtualDPad component\n */\nexport interface VirtualDPadProps {\n /** Callback when direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Whether D-Pad is disabled */\n readonly disabled?: boolean;\n /** Size of the D-Pad in pixels (default: 140) */\n readonly size?: number;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from left in pixels (default: 20) */\n readonly left?: number;\n /** Opacity of the D-Pad (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number; // Angle in degrees for positioning\n readonly korean: string; // Korean label\n readonly englishLabel: string; // English label for ARIA\n}\n\n/**\n * 8-directional configuration\n * Arranged clockwise starting from up (0°)\n */\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, korean: \"↑\", englishLabel: \"Up\" },\n { direction: \"up-right\", angle: 45, korean: \"↗\", englishLabel: \"Up Right\" },\n { direction: \"right\", angle: 90, korean: \"→\", englishLabel: \"Right\" },\n {\n direction: \"down-right\",\n angle: 135,\n korean: \"↘\",\n englishLabel: \"Down Right\",\n },\n { direction: \"down\", angle: 180, korean: \"↓\", englishLabel: \"Down\" },\n {\n direction: \"down-left\",\n angle: 225,\n korean: \"↙\",\n englishLabel: \"Down Left\",\n },\n { direction: \"left\", angle: 270, korean: \"←\", englishLabel: \"Left\" },\n { direction: \"up-left\", angle: 315, korean: \"↖\", englishLabel: \"Up Left\" },\n] as const;\n\n/**\n * Individual D-Pad button component\n */\ninterface DPadButtonProps {\n readonly config: DirectionConfig;\n readonly active: boolean;\n readonly focused: boolean;\n readonly onStart: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onEnd: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onKeyDown: (e: React.KeyboardEvent) => void;\n readonly onFocus: () => void;\n readonly onBlur: () => void;\n readonly radius: number; // Radius for button positioning\n readonly buttonSize: number;\n}\n\n/**\n * D-Pad button positioned around the center\n * Memoized to prevent unnecessary re-renders\n */\nconst DPadButton = React.memo<DPadButtonProps>(\n ({\n config,\n active,\n focused,\n onStart,\n onEnd,\n onKeyDown,\n onFocus,\n onBlur,\n radius,\n buttonSize,\n }) => {\n const radian = (config.angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * radius;\n const y = Math.sin(radian) * radius;\n\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n const ariaLabel = createBilingualLabel(\n `이동 ${config.korean}`,\n `Move ${config.englishLabel}`,\n ).label;\n\n return (\n <button\n onTouchStart={onStart}\n onTouchEnd={onEnd}\n onMouseDown={onStart}\n onMouseUp={onEnd}\n onMouseLeave={onEnd}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n onBlur={onBlur}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${buttonSize / 2}px)`,\n width: `${buttonSize}px`,\n height: `${buttonSize}px`,\n borderRadius: \"50%\",\n background: active\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : \"rgba(0, 0, 0, 0.5)\",\n border: focused\n ? `3px solid rgb(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b})`\n : `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${active ? 1 : 0.6})`,\n fontSize: \"20px\",\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.2s ease, opacity 0.2s ease\",\n transform: active ? \"scale(1.1)\" : \"scale(1)\",\n boxShadow: focused\n ? `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : active\n ? `0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : \"none\",\n ...getFocusStyle(focused),\n }}\n aria-label={ariaLabel}\n aria-pressed={active}\n role=\"button\"\n tabIndex={0}\n data-testid={`dpad-button-${config.direction}`}\n >\n {config.korean}\n </button>\n );\n },\n (prevProps, nextProps) => {\n return (\n prevProps.active === nextProps.active &&\n prevProps.focused === nextProps.focused\n );\n },\n);\n\nDPadButton.displayName = \"DPadButton\";\n\n/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for touch-based movement control\n * Features:\n * - 8 directional buttons arranged in a circle\n * - Visual feedback on active direction\n * - Haptic feedback on touch\n * - Korean theming with cyberpunk aesthetics\n * - 48x48px minimum touch targets (improved from iOS 44px guideline)\n * - 140x140px default size for better mobile usability\n *\n * Usage in Combat/Training:\n * - Provides tactical positioning and footwork\n * - Alternative to keyboard WASD controls\n * - Essential for mobile gameplay\n *\n * @example\n * ```tsx\n * <VirtualDPad\n * onMove={(direction, eventType) => {\n * if (eventType === 'start' && direction) {\n * handleMovement(direction);\n * } else if (eventType === 'end') {\n * stopMovement();\n * }\n * }}\n * disabled={isPaused}\n * size={140}\n * bottom={34}\n * />\n * ```\n *\n * @korean 가상방향패드\n */\nconst VirtualDPadComponent: React.FC<VirtualDPadProps> = ({\n onMove,\n disabled = false,\n size = 140,\n bottom = 34,\n left = 20,\n opacity = 0.8,\n}) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [focusedDirection, setFocusedDirection] = useState<Direction | null>(\n null,\n );\n\n const throttledOnMove = useThrottle(onMove, 16);\n\n /**\n * Handle touch or mouse start on a direction button\n * Throttled for performance\n */\n const handleStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle touch or mouse end\n * Throttled for performance\n */\n const handleEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle keyboard navigation for D-Pad\n */\n const handleKeyDown = useCallback(\n (direction: Direction) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n setTimeout(() => {\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n }, 150);\n },\n });\n },\n [disabled, throttledOnMove],\n );\n\n const dimensions = useMemo(\n () => ({\n buttonSize: Math.max(48, size * 0.3),\n radius: (size - Math.max(48, size * 0.3)) / 2,\n }),\n [size],\n );\n\n const colors = useMemo(\n () => ({\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n left: `${left}px`,\n width: `${size}px`,\n height: `${size}px`,\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"virtual-dpad\"\n >\n {/* D-Pad Container */}\n <div\n style={{\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n background: \"rgba(0, 0, 0, 0.5)\",\n borderRadius: \"50%\",\n border: `2px solid rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.8)`,\n boxShadow: `0 0 20px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.3)`,\n }}\n role=\"group\"\n aria-label={\n createBilingualLabel(\"방향 패드\", \"Directional Pad\").label\n }\n >\n {/* Directional Buttons */}\n {DIRECTIONS.map((config) => (\n <DPadButton\n key={config.direction}\n config={config}\n active={activeDirection === config.direction}\n focused={focusedDirection === config.direction}\n onStart={(e) => handleStart(e, config.direction)}\n onEnd={handleEnd}\n onKeyDown={handleKeyDown(config.direction)}\n onFocus={() => setFocusedDirection(config.direction)}\n onBlur={() => setFocusedDirection(null)}\n radius={dimensions.radius}\n buttonSize={dimensions.buttonSize}\n />\n ))}\n\n {/* Center Indicator */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"20px\",\n height: \"20px\",\n borderRadius: \"50%\",\n background: activeDirection\n ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`\n : `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.7)`,\n border: \"2px solid #fff\",\n transition: \"transform 0.15s ease, opacity 0.15s ease\",\n boxShadow: activeDirection\n ? `0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.8)`\n : \"none\",\n }}\n data-testid=\"dpad-center\"\n />\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized VirtualDPad with custom comparison\n * Only re-renders when props change\n */\nexport const VirtualDPad = React.memo(\n VirtualDPadComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.size === nextProps.size &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.left === nextProps.left &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onMove === nextProps.onMove\n );\n },\n);\n\nVirtualDPad.displayName = \"VirtualDPad\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"VirtualDPad.js","names":[],"sources":["../../../../src/components/shared/mobile/VirtualDPad.tsx"],"sourcesContent":["/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for mobile touch controls\n * Provides tactile movement control with visual feedback and haptic response\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 directions\n * - Keyboard navigation (Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - role=\"group\" with descriptive label\n * - 48x48px minimum touch targets\n *\n * @module components/mobile/VirtualDPad\n * @category Mobile Controls\n * @korean 가상 방향 패드\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useState, useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport { handleKeyboardNav, getFocusStyle } from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\nimport { useThrottle } from \"../../../hooks/useThrottle\";\n\n/**\n * 8 directions for movement control\n */\nexport type Direction =\n | \"up\"\n | \"up-right\"\n | \"right\"\n | \"down-right\"\n | \"down\"\n | \"down-left\"\n | \"left\"\n | \"up-left\";\n\n/**\n * Event type for D-Pad interactions\n */\nexport type DPadEventType = \"start\" | \"end\";\n\n/**\n * Props for VirtualDPad component\n */\nexport interface VirtualDPadProps {\n /** Callback when direction changes */\n readonly onMove: (\n direction: Direction | null,\n eventType: DPadEventType,\n ) => void;\n /** Whether D-Pad is disabled */\n readonly disabled?: boolean;\n /** Size of the D-Pad in pixels (default: 140) */\n readonly size?: number;\n /** Position from bottom in pixels (default: 34 for safe area) */\n readonly bottom?: number;\n /** Position from left in pixels (default: 20) */\n readonly left?: number;\n /** Opacity of the D-Pad (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Direction configuration for D-Pad buttons\n */\ninterface DirectionConfig {\n readonly direction: Direction;\n readonly angle: number; // Angle in degrees for positioning\n readonly korean: string; // Korean label\n readonly englishLabel: string; // English label for ARIA\n}\n\n/**\n * 8-directional configuration\n * Arranged clockwise starting from up (0°)\n */\nconst DIRECTIONS: readonly DirectionConfig[] = [\n { direction: \"up\", angle: 0, korean: \"↑\", englishLabel: \"Up\" },\n { direction: \"up-right\", angle: 45, korean: \"↗\", englishLabel: \"Up Right\" },\n { direction: \"right\", angle: 90, korean: \"→\", englishLabel: \"Right\" },\n {\n direction: \"down-right\",\n angle: 135,\n korean: \"↘\",\n englishLabel: \"Down Right\",\n },\n { direction: \"down\", angle: 180, korean: \"↓\", englishLabel: \"Down\" },\n {\n direction: \"down-left\",\n angle: 225,\n korean: \"↙\",\n englishLabel: \"Down Left\",\n },\n { direction: \"left\", angle: 270, korean: \"←\", englishLabel: \"Left\" },\n { direction: \"up-left\", angle: 315, korean: \"↖\", englishLabel: \"Up Left\" },\n] as const;\n\n/**\n * Individual D-Pad button component\n */\ninterface DPadButtonProps {\n readonly config: DirectionConfig;\n readonly active: boolean;\n readonly focused: boolean;\n readonly onStart: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onEnd: (e: React.TouchEvent | React.MouseEvent) => void;\n readonly onKeyDown: (e: React.KeyboardEvent) => void;\n readonly onFocus: () => void;\n readonly onBlur: () => void;\n readonly radius: number; // Radius for button positioning\n readonly buttonSize: number;\n}\n\n/**\n * D-Pad button positioned around the center\n * Memoized to prevent unnecessary re-renders\n */\nconst DPadButton = React.memo<DPadButtonProps>(\n ({\n config,\n active,\n focused,\n onStart,\n onEnd,\n onKeyDown,\n onFocus,\n onBlur,\n radius,\n buttonSize,\n }) => {\n const radian = (config.angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * radius;\n const y = Math.sin(radian) * radius;\n\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n const ariaLabel = createBilingualLabel(\n `이동 ${config.korean}`,\n `Move ${config.englishLabel}`,\n ).label;\n\n return (\n <button\n onTouchStart={onStart}\n onTouchEnd={onEnd}\n onMouseDown={onStart}\n onMouseUp={onEnd}\n onMouseLeave={onEnd}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n onBlur={onBlur}\n style={{\n position: \"absolute\",\n left: `calc(50% + ${x}px - ${buttonSize / 2}px)`,\n top: `calc(50% + ${y}px - ${buttonSize / 2}px)`,\n width: `${buttonSize}px`,\n height: `${buttonSize}px`,\n borderRadius: \"50%\",\n background: active\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : \"rgba(0, 0, 0, 0.5)\",\n border: focused\n ? `3px solid rgb(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b})`\n : `2px solid rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, ${active ? 1 : 0.6})`,\n fontSize: \"20px\",\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.2s ease, opacity 0.2s ease\",\n transform: active ? \"scale(1.1)\" : \"scale(1)\",\n boxShadow: focused\n ? `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : active\n ? `0 0 15px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.8)`\n : \"none\",\n ...getFocusStyle(focused),\n }}\n aria-label={ariaLabel}\n aria-pressed={active}\n role=\"button\"\n tabIndex={0}\n data-testid={`dpad-button-${config.direction}`}\n >\n {config.korean}\n </button>\n );\n },\n (prevProps, nextProps) => {\n return (\n prevProps.active === nextProps.active &&\n prevProps.focused === nextProps.focused\n );\n },\n);\n\nDPadButton.displayName = \"DPadButton\";\n\n/**\n * VirtualDPad Component\n *\n * 8-directional virtual D-Pad for touch-based movement control\n * Features:\n * - 8 directional buttons arranged in a circle\n * - Visual feedback on active direction\n * - Haptic feedback on touch\n * - Korean theming with cyberpunk aesthetics\n * - 48x48px minimum touch targets (improved from iOS 44px guideline)\n * - 140x140px default size for better mobile usability\n *\n * Usage in Combat/Training:\n * - Provides tactical positioning and footwork\n * - Alternative to keyboard WASD controls\n * - Essential for mobile gameplay\n *\n * @example\n * ```tsx\n * <VirtualDPad\n * onMove={(direction, eventType) => {\n * if (eventType === 'start' && direction) {\n * handleMovement(direction);\n * } else if (eventType === 'end') {\n * stopMovement();\n * }\n * }}\n * disabled={isPaused}\n * size={140}\n * bottom={34}\n * />\n * ```\n *\n * @korean 가상방향패드\n */\nconst VirtualDPadComponent: React.FC<VirtualDPadProps> = ({\n onMove,\n disabled = false,\n size = 140,\n bottom = 34,\n left = 20,\n opacity = 0.8,\n}) => {\n const [activeDirection, setActiveDirection] = useState<Direction | null>(\n null,\n );\n const [focusedDirection, setFocusedDirection] = useState<Direction | null>(\n null,\n );\n\n const throttledOnMove = useThrottle(onMove, 16);\n\n /**\n * Handle touch or mouse start on a direction button\n * Throttled for performance\n */\n const handleStart = useCallback(\n (e: React.TouchEvent | React.MouseEvent, direction: Direction) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle touch or mouse end\n * Throttled for performance\n */\n const handleEnd = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n },\n [disabled, throttledOnMove],\n );\n\n /**\n * Handle keyboard navigation for D-Pad\n */\n const handleKeyDown = useCallback(\n (direction: Direction) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n setActiveDirection(direction);\n throttledOnMove(direction, \"start\");\n triggerHaptic(\"light\");\n setTimeout(() => {\n setActiveDirection(null);\n throttledOnMove(null, \"end\");\n }, 150);\n },\n });\n },\n [disabled, throttledOnMove],\n );\n\n const dimensions = useMemo(\n () => ({\n buttonSize: Math.max(48, size * 0.3),\n radius: (size - Math.max(48, size * 0.3)) / 2,\n }),\n [size],\n );\n\n const colors = useMemo(\n () => ({\n primary: getColorRGB(KOREAN_COLORS.PRIMARY_CYAN),\n gold: getColorRGB(KOREAN_COLORS.ACCENT_GOLD),\n }),\n [],\n );\n\n return (\n <Html fullscreen>\n <div\n style={{\n position: \"absolute\",\n bottom: `${bottom}px`,\n left: `${left}px`,\n width: `${size}px`,\n height: `${size}px`,\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n }}\n data-testid=\"virtual-dpad\"\n >\n {/* D-Pad Container */}\n <div\n style={{\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n background: \"rgba(0, 0, 0, 0.5)\",\n borderRadius: \"50%\",\n border: `2px solid rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.8)`,\n boxShadow: `0 0 20px rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.3)`,\n }}\n role=\"group\"\n aria-label={\n createBilingualLabel(\"방향 패드\", \"Directional Pad\").label\n }\n >\n {/* Directional Buttons */}\n {DIRECTIONS.map((config) => (\n <DPadButton\n key={config.direction}\n config={config}\n active={activeDirection === config.direction}\n focused={focusedDirection === config.direction}\n onStart={(e) => handleStart(e, config.direction)}\n onEnd={handleEnd}\n onKeyDown={handleKeyDown(config.direction)}\n onFocus={() => setFocusedDirection(config.direction)}\n onBlur={() => setFocusedDirection(null)}\n radius={dimensions.radius}\n buttonSize={dimensions.buttonSize}\n />\n ))}\n\n {/* Center Indicator */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n width: \"20px\",\n height: \"20px\",\n borderRadius: \"50%\",\n background: activeDirection\n ? `rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.9)`\n : `rgba(${colors.primary.r}, ${colors.primary.g}, ${colors.primary.b}, 0.7)`,\n border: \"2px solid #fff\",\n transition: \"transform 0.15s ease, opacity 0.15s ease\",\n boxShadow: activeDirection\n ? `0 0 15px rgba(${colors.gold.r}, ${colors.gold.g}, ${colors.gold.b}, 0.8)`\n : \"none\",\n }}\n data-testid=\"dpad-center\"\n />\n </div>\n </div>\n </Html>\n );\n};\n\n/**\n * Memoized VirtualDPad with custom comparison\n * Only re-renders when props change\n */\nexport const VirtualDPad = React.memo(\n VirtualDPadComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.disabled === nextProps.disabled &&\n prevProps.size === nextProps.size &&\n prevProps.bottom === nextProps.bottom &&\n prevProps.left === nextProps.left &&\n prevProps.opacity === nextProps.opacity &&\n prevProps.onMove === nextProps.onMove\n );\n },\n);\n\nVirtualDPad.displayName = \"VirtualDPad\";\n"],"mappings":"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"}