blacktrigram 0.7.39 → 0.7.41

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 (354) 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.js.map +1 -1
  14. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  15. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  18. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  19. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  20. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  25. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  26. package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
  27. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  28. package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
  29. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  30. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  31. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  32. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  33. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  34. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  36. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  37. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  38. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  39. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  40. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  41. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  42. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  43. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  44. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  45. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  46. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  47. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  48. package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
  49. package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
  50. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  51. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  52. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  53. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  54. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  55. package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
  56. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  57. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  58. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  59. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  60. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  61. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  62. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  63. package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
  64. package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
  65. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  66. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  67. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  68. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  69. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  72. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  74. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  75. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  76. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  77. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  78. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  79. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  80. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  81. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  83. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  84. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  85. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  86. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  87. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  88. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  90. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  91. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  92. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  93. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  94. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  95. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  96. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  97. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  98. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  99. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  100. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  101. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  102. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  103. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  104. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  105. package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
  106. package/lib/components/shared/base/BaseButton.js.map +1 -1
  107. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  108. package/lib/components/shared/base/BasePanel.js.map +1 -1
  109. package/lib/components/shared/base/BaseText.js.map +1 -1
  110. package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
  111. package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
  112. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  113. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  114. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  115. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  116. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  117. package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
  118. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  119. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  120. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  121. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  122. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  123. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  124. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  125. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  126. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  127. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  128. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  129. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  130. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  131. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  132. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  133. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  134. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  135. package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
  136. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  137. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  138. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  139. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  140. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  141. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  142. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  143. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  144. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  145. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  146. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  147. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  148. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  149. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  150. package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
  151. package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
  152. package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
  153. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  154. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  155. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  156. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  157. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  158. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  159. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  160. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  161. package/lib/components/shared/ui/BackButton.js.map +1 -1
  162. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  163. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  164. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  165. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  166. package/lib/components/shared/ui/SplashScreen.js +2 -2
  167. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  168. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  169. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  170. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  171. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  172. package/lib/constants/bodyDimensions.js.map +1 -1
  173. package/lib/constants/bodyRenderingConstants.js.map +1 -1
  174. package/lib/data/archetypeClothing.js.map +1 -1
  175. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  176. package/lib/data/techniqueMappings.js.map +1 -1
  177. package/lib/data/techniques.js.map +1 -1
  178. package/lib/hooks/useActionFeedback.js.map +1 -1
  179. package/lib/hooks/useBalanceAnimations.js.map +1 -1
  180. package/lib/hooks/useCombatTimer.js.map +1 -1
  181. package/lib/hooks/useDebounce.js.map +1 -1
  182. package/lib/hooks/useHUDLayout.js.map +1 -1
  183. package/lib/hooks/useHandPoseTransitions.js.map +1 -1
  184. package/lib/hooks/useKeyboardControls.js.map +1 -1
  185. package/lib/hooks/useMatchCountdown.js.map +1 -1
  186. package/lib/hooks/useMuscleActivation.js.map +1 -1
  187. package/lib/hooks/usePauseMenu.js.map +1 -1
  188. package/lib/hooks/usePlayerAnimation.js.map +1 -1
  189. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  190. package/lib/hooks/useRoundTransition.js.map +1 -1
  191. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  192. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  193. package/lib/hooks/useThrottle.js.map +1 -1
  194. package/lib/hooks/useTouchControls.js.map +1 -1
  195. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  196. package/lib/hooks/useWindowSize.js.map +1 -1
  197. package/lib/systems/CombatSystem.js.map +1 -1
  198. package/lib/systems/EffectCalculator.js.map +1 -1
  199. package/lib/systems/LayoutSystem.js.map +1 -1
  200. package/lib/systems/PlayerEffectManager.js.map +1 -1
  201. package/lib/systems/ResponsiveScaling.js.map +1 -1
  202. package/lib/systems/TrigramSystem.js.map +1 -1
  203. package/lib/systems/VitalPointSystem.js.map +1 -1
  204. package/lib/systems/ai/AIPersonality.js.map +1 -1
  205. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  206. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  207. package/lib/systems/ai/ComboSystem.js.map +1 -1
  208. package/lib/systems/ai/DecisionTree.js.map +1 -1
  209. package/lib/systems/ai/TrainingAI.js.map +1 -1
  210. package/lib/systems/ai/types.js.map +1 -1
  211. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  212. package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
  213. package/lib/systems/animation/builders/HandPoses.js.map +1 -1
  214. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  215. package/lib/systems/animation/builders/KeyframeInterpolation.js +3 -90
  216. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  217. package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
  218. package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
  219. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  220. package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
  221. package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
  222. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  223. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  224. package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
  225. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  226. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  227. package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
  228. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  229. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  230. package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
  231. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  232. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  233. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  234. package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
  235. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  236. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  237. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  238. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  239. package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
  240. package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
  241. package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
  242. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  243. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  244. package/lib/systems/animation/core/types.js.map +1 -1
  245. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  246. package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
  247. package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
  248. package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
  249. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  250. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  251. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  252. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  253. package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
  254. package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
  255. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  256. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  257. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  258. package/lib/systems/bodypart/types.js.map +1 -1
  259. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  260. package/lib/systems/breathing/feedback.js.map +1 -1
  261. package/lib/systems/breathing/integration.js.map +1 -1
  262. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  263. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  264. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  265. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  266. package/lib/systems/combat/FallIntegration.js.map +1 -1
  267. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  268. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  269. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  270. package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
  271. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  272. package/lib/systems/combat/typeGuards.js.map +1 -1
  273. package/lib/systems/effects.js.map +1 -1
  274. package/lib/systems/game.js.map +1 -1
  275. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  276. package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
  277. package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
  278. package/lib/systems/movement/integration.js.map +1 -1
  279. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  280. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  281. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  282. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  283. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  284. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  285. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  286. package/lib/systems/trigram/KoreanCulture.js.map +1 -1
  287. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  288. package/lib/systems/trigram/StanceManager.js.map +1 -1
  289. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  290. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  291. package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
  292. package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
  293. package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
  294. package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
  295. package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
  296. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  297. package/lib/systems/trigram/techniques/index.js.map +1 -1
  298. package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
  299. package/lib/systems/trigram/types.js.map +1 -1
  300. package/lib/systems/types.js.map +1 -1
  301. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  302. package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
  303. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  304. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  305. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  306. package/lib/types/AccessibilityTypes.js.map +1 -1
  307. package/lib/types/PhysicsTypes.js.map +1 -1
  308. package/lib/types/common.js.map +1 -1
  309. package/lib/types/constants/colors.js.map +1 -1
  310. package/lib/types/constants/designSystem.js.map +1 -1
  311. package/lib/types/constants/layout.js.map +1 -1
  312. package/lib/types/constants/performance.js.map +1 -1
  313. package/lib/types/constants/typography.js.map +1 -1
  314. package/lib/types/facial.js.map +1 -1
  315. package/lib/types/hand-animation.js.map +1 -1
  316. package/lib/types/injury.js.map +1 -1
  317. package/lib/types/physics.js.map +1 -1
  318. package/lib/types/skeletal.js.map +1 -1
  319. package/lib/types/techniqueId.js.map +1 -1
  320. package/lib/utils/accessibility.js.map +1 -1
  321. package/lib/utils/arenaWorldDimensions.js.map +1 -1
  322. package/lib/utils/assetConfig.js.map +1 -1
  323. package/lib/utils/characterScaling.js.map +1 -1
  324. package/lib/utils/colorHelpers.js.map +1 -1
  325. package/lib/utils/colorUtils.js.map +1 -1
  326. package/lib/utils/combatReadiness.js.map +1 -1
  327. package/lib/utils/controlMapping.js.map +1 -1
  328. package/lib/utils/deviceDetection.js.map +1 -1
  329. package/lib/utils/effectUtils.js.map +1 -1
  330. package/lib/utils/fabricTextures.js.map +1 -1
  331. package/lib/utils/hapticFeedback.js.map +1 -1
  332. package/lib/utils/haptics.js.map +1 -1
  333. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  334. package/lib/utils/inputSystem.js.map +1 -1
  335. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  336. package/lib/utils/math.js.map +1 -1
  337. package/lib/utils/mobileLayoutHelpers.js.map +1 -1
  338. package/lib/utils/mobileUIUtils.js.map +1 -1
  339. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  340. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  341. package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
  342. package/lib/utils/performanceOptimization.js.map +1 -1
  343. package/lib/utils/player3DHelpers.js.map +1 -1
  344. package/lib/utils/playerUtils.js.map +1 -1
  345. package/lib/utils/responsiveLayout.js.map +1 -1
  346. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  347. package/lib/utils/responsiveOrientationConstants.js.map +1 -1
  348. package/lib/utils/safeAreaUtils.js.map +1 -1
  349. package/lib/utils/sharedPhysicsConfig.js.map +1 -1
  350. package/lib/utils/skeletonScaling.js.map +1 -1
  351. package/lib/utils/stanceHelpers.js.map +1 -1
  352. package/lib/utils/threeObjectPool.js.map +1 -1
  353. package/lib/utils/visualEffects.js.map +1 -1
  354. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"useTrainingActions.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingActions.ts"],"sourcesContent":["/**\n * useTrainingActions Hook - Training Action Handlers\n *\n * Custom hook for managing training action handlers.\n * Mirrors useCombatActions pattern for consistency.\n *\n * @korean 훈련액션훅 - 훈련 액션 핸들러 관리\n */\n\nimport { useCallback, useRef } from \"react\";\nimport {\n AudioBodyRegion,\n ImpactIntensity,\n} from \"../../../../audio/types\";\nimport type { AttackIntensity } from \"../../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport {\n AnimationState,\n AnimationType,\n getAnimationForTechnique,\n} from \"../../../../systems/animation\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { getTechniquesByStance } from \"../../../../systems/trigram/techniques\";\nimport { KoreanTechniquesSystem } from \"../../../../systems/trigram/KoreanTechniques\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport type { KoreanTechnique } from \"../../../../systems/vitalpoint/types\";\nimport {\n CombatAttackType,\n DamageType,\n PlayerArchetype,\n Position,\n TrigramStance,\n} from \"../../../../types/common\";\nimport {\n DEFAULT_BODY_RADIUS_METERS,\n METERS_TO_TRAINING_UNITS,\n} from \"../../../../types/physicsConstants\";\nimport { calculateDistance3D } from \"../../../../utils/math\";\nimport { TrainingActions, TrainingScreenState } from \"./useTrainingState\";\n\nexport interface UseTrainingActionsConfig {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n readonly playerPosition: Position;\n readonly player3DPosition: [number, number, number];\n readonly dummyPosition: [number, number, number];\n readonly playerArchetype: import(\"../../../../types/common\").PlayerArchetype;\n readonly playerStance: TrigramStance;\n /** Ref to current selected technique's animation type for distance-based hit detection */\n readonly currentTechniqueAnimationTypeRef: React.MutableRefObject<AnimationType>;\n readonly audio: {\n readonly playSFX: (sound: string, volume?: number) => Promise<void>;\n };\n /** Attack sound function from useCombatAudio for playing attack whoosh sounds */\n readonly playAttackSound?: (intensity: AttackIntensity) => Promise<void>;\n /** Bone impact audio function from useCombatAudio or similar hook */\n readonly playBoneImpactSound?: (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly onPlayerUpdate: (updates: {\n currentStance?: TrigramStance;\n lastActionTime?: number;\n position?: Position;\n }) => void;\n readonly playerAnimation: {\n readonly transitionTo: (state: AnimationState) => boolean;\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n readonly currentState: string;\n };\n /** External ref to store pending attack data - shared with animation events */\n readonly pendingAttackRef: React.MutableRefObject<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>;\n /** Currently selected technique ID (from technique bar) */\n readonly selectedTechniqueId?: string;\n /** Callback to set the visual attack animation */\n readonly setAttackAnimation?: (animationName: string | undefined) => void;\n}\n\nexport interface UseTrainingActionsReturn {\n readonly handleStartTraining: () => void;\n readonly handleStopTraining: () => void;\n readonly handleDummyHit: (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean;\n readonly handleDummyDefeated: () => void;\n readonly handleStanceChange: (stanceIndex: number) => void;\n readonly handleAttack: () => void;\n}\n\n/**\n * Get the best default technique for an archetype based on current stance.\n *\n * Each archetype has preferred combat styles:\n * - MUSA: Direct strikes, joint techniques (BLUNT/JOINT damage)\n * - AMSALJA: Nerve strikes, pressure points (NERVE/PRESSURE damage)\n * - HACKER: Precise calculated strikes (high accuracy)\n * - JEONGBO_YOWON: Psychological pressure, submissions (PRESSURE/JOINT)\n * - JOJIK_POKRYEOKBAE: High damage brutal strikes\n *\n * @korean 원형별 기본 기술 선택 - 8개 자세에 따른 최적 기술\n */\nfunction getDefaultTechniqueForArchetype(\n archetype: PlayerArchetype,\n stance: TrigramStance,\n): KoreanTechnique | undefined {\n const techniques = getTechniquesByStance(stance);\n if (techniques.length === 0) return undefined;\n\n // Score each technique based on archetype preference\n const scoredTechniques = techniques.map((tech) => {\n let score = 0;\n const damageType = tech.damageType;\n const attackType = tech.type;\n const isAdvanced =\n (tech.kiCost || 0) >= 10 || (tech.staminaCost || 0) >= 15;\n\n switch (archetype) {\n case PlayerArchetype.MUSA:\n // Musa prefers: strikes, blocks, counter-attacks (BLUNT/JOINT/CRUSHING)\n if (damageType === DamageType.JOINT) score += 30;\n if (damageType === DamageType.CRUSHING) score += 25;\n if (damageType === DamageType.BLUNT) score += 20;\n if (attackType === CombatAttackType.STRIKE) score += 15;\n if (attackType === CombatAttackType.PUNCH) score += 10;\n if (isAdvanced) score += 10;\n break;\n\n case PlayerArchetype.AMSALJA:\n // Amsalja prefers: nerve strikes, pressure points, thrusts\n if (damageType === DamageType.NERVE) score += 35;\n if (damageType === DamageType.PRESSURE) score += 30;\n if (attackType === CombatAttackType.NERVE_STRIKE) score += 25;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 25;\n if (attackType === CombatAttackType.THRUST) score += 15;\n if ((tech.accuracy || 0) >= 0.9) score += 10;\n break;\n\n case PlayerArchetype.HACKER:\n // Hacker prefers: high accuracy, calculated strikes\n if ((tech.accuracy || 0) >= 0.9) score += 30;\n if ((tech.accuracy || 0) >= 0.8) score += 15;\n if (damageType === DamageType.NERVE) score += 20;\n if (damageType === DamageType.INTERNAL) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n // Jeongbo prefers: psychological pressure, submissions\n if (damageType === DamageType.PRESSURE) score += 30;\n if (damageType === DamageType.JOINT) score += 25;\n if (attackType === CombatAttackType.GRAPPLE) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n // Jojik prefers: high damage, brutal techniques\n if ((tech.damage || 0) >= 35) score += 35;\n if ((tech.damage || 0) >= 30) score += 20;\n if (damageType === DamageType.SLASHING) score += 20;\n if (damageType === DamageType.PIERCING) score += 15;\n if (damageType === DamageType.CRUSHING) score += 15;\n if (attackType === CombatAttackType.KICK) score += 10;\n break;\n }\n\n return { technique: tech, score };\n });\n\n // Sort by score descending, return highest scoring technique\n scoredTechniques.sort((a, b) => b.score - a.score);\n return scoredTechniques[0]?.technique;\n}\n\n/**\n * Calculate hit accuracy based on distance and effective reach.\n * Uses PhysicalReachCalculator for animation-aware reach calculation.\n *\n * Distance logic matches CombatSystem: if out of reach, guaranteed miss (accuracy 0).\n * Training scene coordinates are in meters, using METERS_TO_TRAINING_UNITS = 1.0.\n *\n * **IMPORTANT**: Distance calculation accounts for body radius.\n * Center-to-center distance is adjusted by subtracting target body radius\n * because attacks hit the body surface, not the center point.\n * The target body radius is calculated from physical attributes for archetypes,\n * or uses DEFAULT_BODY_RADIUS_METERS for training dummies.\n *\n * Example: If player center is 1.5m from dummy center, but dummy body\n * extends 0.23m from center, the effective distance to hit is 1.27m.\n *\n * @korean 거리 기반 명중률 계산 - 사정거리 밖이면 빗나감\n */\nfunction calculateHitAccuracy(\n playerPos: [number, number, number],\n dummyPos: [number, number, number],\n archetype: import(\"../../../../types/common\").PlayerArchetype,\n stance: TrigramStance,\n animationType?: AnimationType,\n reachConfig?: import(\"../../../../types/physics\").PhysicalReachConfig,\n): number {\n // Calculate 3D distance between player and dummy centers (in meters)\n const centerToCenterDistance = calculateDistance3D(playerPos, dummyPos);\n\n // Get player's physical attributes for reach calculation\n const playerPhysicalAttributes = getArchetypePhysicalAttributes(archetype);\n\n // Training dummy uses default body radius since it has no archetype\n // For combat between players, we would use calculateBodyRadius(targetPhysicalAttributes)\n // 훈련 더미는 원형이 없으므로 기본 몸체 반경 사용\n const targetBodyRadius = DEFAULT_BODY_RADIUS_METERS;\n\n // Effective distance = center-to-center minus target body radius only\n // Note: PhysicalReachCalculator already includes player body pivot/offset in reach calculation,\n // so we only subtract the target radius to avoid double-counting.\n // 유효 거리 = 중심간 거리 - 더미 몸체 반경 (플레이어 몸체 오프셋은 도달 거리에 포함됨)\n const effectiveDistance = Math.max(\n 0,\n centerToCenterDistance - targetBodyRadius,\n );\n\n // If animation type is available, use physics-based reach calculation\n // We use calculateMaxReach (peak time reach) because:\n // 1. Training hit detection happens at animation frame 6 (~100ms)\n // 2. But technique hit timings expect longer animations (e.g., roundhouse 200-480ms)\n // 3. Using max reach ensures the technique can hit if within peak reach range\n // 4. This matches intuitive behavior - if you're close enough to be hit by the kick, it hits\n // 훈련 타격 감지는 최대 도달 거리 사용 (애니메이션 타이밍과 기술 타이밍 불일치 보정)\n if (animationType !== undefined) {\n const maxReachMeters = physicalReachCalculator.calculateMaxReach(\n playerPhysicalAttributes,\n animationType,\n stance,\n reachConfig, // Use technique's designed reach if provided\n );\n\n // Convert reach from meters to training scene units.\n // Training scenes are authored in real-world meters, so we intentionally\n // use a 1:1 conversion here (METERS_TO_TRAINING_UNITS = 1.0).\n // Combat AI works in pixel coordinates with a dynamic px/m ratio.\n const reachInUnits = maxReachMeters * METERS_TO_TRAINING_UNITS;\n\n // STRICT DISTANCE CHECK (matches CombatSystem behavior):\n // Out of reach = guaranteed miss with accuracy 0\n if (effectiveDistance > reachInUnits) {\n return 0;\n }\n\n // Within reach: accuracy based on how centered the hit is\n // Closer = higher accuracy (0.7 to 1.0 range)\n return Math.max(0.7, 1.0 - (effectiveDistance / reachInUnits) * 0.3);\n }\n\n // Fallback: use default punch reach (0.7 meters) when no reach config is supplied.\n const defaultReach = 0.7 * METERS_TO_TRAINING_UNITS;\n if (effectiveDistance > defaultReach) {\n return 0; // Out of reach = miss\n }\n // Within default reach: linear accuracy based on distance\n return Math.max(0.5, 1.0 - (effectiveDistance / defaultReach) * 0.5);\n}\n\n/**\n * useTrainingActions hook\n * Provides training action handlers with proper memoization\n */\nexport function useTrainingActions(\n config: UseTrainingActionsConfig,\n): UseTrainingActionsReturn {\n const {\n state,\n actions,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n audio,\n playBoneImpactSound,\n playAttackSound,\n onPlayerUpdate,\n playerAnimation,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n } = config;\n\n // Ref to store timeout for dummy reset\n const dummyResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleStartTraining = useCallback(() => {\n actions.startTraining();\n audio.playSFX(\"menu_select\");\n }, [actions, audio]);\n\n const handleStopTraining = useCallback(() => {\n // Clear any pending dummy reset timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n dummyResetTimeoutRef.current = null;\n }\n actions.stopTraining();\n audio.playSFX(\"menu_back\");\n }, [actions, audio]);\n\n const handleDummyDefeated = useCallback(() => {\n actions.setFeedback(\"훈련 더미 무력화! | Dummy Defeated!\");\n audio.playSFX(\"ki_release\");\n\n // Clear any existing timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n }\n\n // Reset dummy health after delay\n dummyResetTimeoutRef.current = setTimeout(() => {\n actions.resetDummy();\n }, 2000);\n }, [actions, audio]);\n\n const handleDummyHit = useCallback(\n (\n _vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ): boolean => {\n // Get animation context from the passed attackContext parameter\n // (TrainingScreen3D.tsx should pass the attackData before clearing the ref)\n const animationType = attackContext?.animationType;\n\n // Get technique's reachConfig for accurate reach calculation\n // Priority: attackContext.techniqueId (resolved in handleAttack) > selectedTechniqueId\n // This ensures default techniques (chosen when no explicit selection) also get their reachConfig\n let reachConfig: import(\"../../../../types/physics\").PhysicalReachConfig | undefined;\n const resolvedTechniqueId =\n attackContext?.techniqueId ?? selectedTechniqueId;\n if (resolvedTechniqueId) {\n const technique = KoreanTechniquesSystem.getTechniqueById(resolvedTechniqueId);\n reachConfig = technique?.reachConfig;\n }\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n reachConfig,\n );\n\n // Determine hit position (dummy center)\n const hitPosition: [number, number, number] = [\n dummyPosition[0],\n 1.5,\n dummyPosition[2],\n ];\n\n if (accuracy > 0.5) {\n const points = Math.round(accuracy * 100);\n const damage = Math.round(accuracy * 15); // 0-15 damage based on accuracy\n const isPerfect = accuracy > 0.9;\n\n // Register hit with state (only counts if training)\n if (state.isTraining) {\n actions.registerHit(points, damage, isPerfect);\n }\n\n // Play bone impact audio with anatomical feedback using proper audio system\n if (playBoneImpactSound) {\n void playBoneImpactSound({\n damage,\n remainingHealth: 100, // Dummy has 100 health\n vitalPoint: false,\n hitPosition: { x: hitPosition[0], y: hitPosition[1], z: hitPosition[2] },\n });\n }\n\n // Determine feedback and sound\n let effectType: \"success\" | \"perfect\";\n if (isPerfect) {\n actions.setFeedback(\"완벽한 타격! | Perfect Strike!\");\n audio.playSFX(\"ki_release\");\n effectType = \"perfect\";\n } else if (accuracy > 0.7) {\n actions.setFeedback(\"좋은 타격! | Good Strike!\");\n audio.playSFX(\"ki_charge\");\n effectType = \"success\";\n } else {\n actions.setFeedback(\"타격 성공 | Strike Success\");\n audio.playSFX(\"menu_click\");\n effectType = \"success\";\n }\n\n // Add hit effect\n actions.addHitEffect({\n position: hitPosition,\n type: effectType,\n visible: true,\n damage,\n });\n\n return true;\n } else {\n // Register miss (only counts if training)\n if (state.isTraining) {\n actions.registerMiss();\n }\n actions.setFeedback(\"빗나감 | Miss - Out of reach!\");\n audio.playSFX(\"menu_navigate\");\n\n // Add miss effect\n actions.addHitEffect({\n position: hitPosition,\n type: \"miss\",\n visible: true,\n });\n\n return false;\n }\n },\n [\n state.isTraining,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n actions,\n audio,\n playBoneImpactSound,\n selectedTechniqueId,\n ],\n );\n\n const handleStanceChange = useCallback(\n (stanceIndex: number) => {\n actions.setStanceIndex(stanceIndex);\n const stance = TRIGRAM_STANCES_ORDER[stanceIndex];\n if (stance) {\n // Directly transition to stance guard animation (skips transitional animation)\n // 자세 가드 애니메이션으로 직접 전환 (전환 애니메이션 생략)\n playerAnimation.transitionToStanceGuard(stance);\n onPlayerUpdate({ currentStance: stance });\n audio.playSFX(\"stance_change\");\n }\n },\n [actions, onPlayerUpdate, audio, playerAnimation],\n );\n\n const handleAttack = useCallback(() => {\n // Determine which technique to use:\n // 1. If a technique is explicitly selected from the TechniqueBar, use that\n // 2. Otherwise, use the best default technique for the archetype + stance\n // 사용할 기술 결정: 명시적 선택 또는 원형+자세 기반 기본 기술\n let techniqueToUse: KoreanTechnique | undefined;\n let techniqueId = selectedTechniqueId;\n\n if (!techniqueId) {\n // No technique explicitly selected - get default for archetype + stance\n // 명시적 선택 없음 - 원형과 자세에 맞는 기본 기술 사용\n const defaultTechnique = getDefaultTechniqueForArchetype(\n playerArchetype,\n playerStance,\n );\n if (defaultTechnique) {\n techniqueToUse = defaultTechnique;\n techniqueId = defaultTechnique.id;\n }\n }\n\n // Get the animation type from the technique\n // 기술에서 애니메이션 타입 가져오기\n let animationType = currentTechniqueAnimationTypeRef.current;\n if (techniqueToUse?.animationType) {\n animationType = techniqueToUse.animationType;\n currentTechniqueAnimationTypeRef.current = animationType;\n }\n\n const startTime = performance.now() / 1000; // Current time in seconds\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n techniqueToUse?.reachConfig, // Pass technique's reachConfig for accurate reach\n );\n\n pendingAttackRef.current = {\n accuracy,\n vitalPoint: state.selectedVitalPoint ?? \"generic\",\n animationType,\n startTime,\n techniqueId, // Store resolved technique ID for handleDummyHit\n };\n\n // Set visual attack animation based on technique (AnimationRegistry lookup)\n // This ensures the 3D model plays the correct technique animation (kick vs punch vs elbow)\n // 기술에 따른 시각적 공격 애니메이션 설정 (발차기 vs 주먹 vs 팔꿈치)\n if (setAttackAnimation && techniqueId) {\n const animationName = getAnimationForTechnique(techniqueId);\n setAttackAnimation(animationName);\n }\n\n // Trigger attack animation - this will fire onFrame event at frame 6\n playerAnimation.transitionTo(AnimationState.ATTACK);\n\n // Play attack sound based on technique damage/intensity\n // Resolve technique data if we only have an ID (from TechniqueBar selection)\n if (!techniqueToUse && selectedTechniqueId) {\n // Technique selected from TechniqueBar but not yet resolved\n techniqueToUse = KoreanTechniquesSystem.getTechniqueById(selectedTechniqueId);\n }\n\n if (playAttackSound) {\n // Prefer explicit technique damage when available\n const damage = techniqueToUse?.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n void playAttackSound(intensity);\n } else {\n // Fallback to generic whoosh if playAttackSound not available\n audio.playSFX(\"whoosh\");\n }\n }, [\n state.selectedVitalPoint,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n playerAnimation,\n audio,\n playAttackSound,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n ]);\n\n return {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n };\n}\n\nexport default useTrainingActions;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useTrainingActions.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingActions.ts"],"sourcesContent":["/**\n * useTrainingActions Hook - Training Action Handlers\n *\n * Custom hook for managing training action handlers.\n * Mirrors useCombatActions pattern for consistency.\n *\n * @korean 훈련액션훅 - 훈련 액션 핸들러 관리\n */\n\nimport { useCallback, useRef } from \"react\";\nimport {\n AudioBodyRegion,\n ImpactIntensity,\n} from \"../../../../audio/types\";\nimport type { AttackIntensity } from \"../../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport {\n AnimationState,\n AnimationType,\n getAnimationForTechnique,\n} from \"../../../../systems/animation\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { getTechniquesByStance } from \"../../../../systems/trigram/techniques\";\nimport { KoreanTechniquesSystem } from \"../../../../systems/trigram/KoreanTechniques\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport type { KoreanTechnique } from \"../../../../systems/vitalpoint/types\";\nimport {\n CombatAttackType,\n DamageType,\n PlayerArchetype,\n Position,\n TrigramStance,\n} from \"../../../../types/common\";\nimport {\n DEFAULT_BODY_RADIUS_METERS,\n METERS_TO_TRAINING_UNITS,\n} from \"../../../../types/physicsConstants\";\nimport { calculateDistance3D } from \"../../../../utils/math\";\nimport { TrainingActions, TrainingScreenState } from \"./useTrainingState\";\n\nexport interface UseTrainingActionsConfig {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n readonly playerPosition: Position;\n readonly player3DPosition: [number, number, number];\n readonly dummyPosition: [number, number, number];\n readonly playerArchetype: import(\"../../../../types/common\").PlayerArchetype;\n readonly playerStance: TrigramStance;\n /** Ref to current selected technique's animation type for distance-based hit detection */\n readonly currentTechniqueAnimationTypeRef: React.MutableRefObject<AnimationType>;\n readonly audio: {\n readonly playSFX: (sound: string, volume?: number) => Promise<void>;\n };\n /** Attack sound function from useCombatAudio for playing attack whoosh sounds */\n readonly playAttackSound?: (intensity: AttackIntensity) => Promise<void>;\n /** Bone impact audio function from useCombatAudio or similar hook */\n readonly playBoneImpactSound?: (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly onPlayerUpdate: (updates: {\n currentStance?: TrigramStance;\n lastActionTime?: number;\n position?: Position;\n }) => void;\n readonly playerAnimation: {\n readonly transitionTo: (state: AnimationState) => boolean;\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n readonly currentState: string;\n };\n /** External ref to store pending attack data - shared with animation events */\n readonly pendingAttackRef: React.MutableRefObject<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>;\n /** Currently selected technique ID (from technique bar) */\n readonly selectedTechniqueId?: string;\n /** Callback to set the visual attack animation */\n readonly setAttackAnimation?: (animationName: string | undefined) => void;\n}\n\nexport interface UseTrainingActionsReturn {\n readonly handleStartTraining: () => void;\n readonly handleStopTraining: () => void;\n readonly handleDummyHit: (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean;\n readonly handleDummyDefeated: () => void;\n readonly handleStanceChange: (stanceIndex: number) => void;\n readonly handleAttack: () => void;\n}\n\n/**\n * Get the best default technique for an archetype based on current stance.\n *\n * Each archetype has preferred combat styles:\n * - MUSA: Direct strikes, joint techniques (BLUNT/JOINT damage)\n * - AMSALJA: Nerve strikes, pressure points (NERVE/PRESSURE damage)\n * - HACKER: Precise calculated strikes (high accuracy)\n * - JEONGBO_YOWON: Psychological pressure, submissions (PRESSURE/JOINT)\n * - JOJIK_POKRYEOKBAE: High damage brutal strikes\n *\n * @korean 원형별 기본 기술 선택 - 8개 자세에 따른 최적 기술\n */\nfunction getDefaultTechniqueForArchetype(\n archetype: PlayerArchetype,\n stance: TrigramStance,\n): KoreanTechnique | undefined {\n const techniques = getTechniquesByStance(stance);\n if (techniques.length === 0) return undefined;\n\n // Score each technique based on archetype preference\n const scoredTechniques = techniques.map((tech) => {\n let score = 0;\n const damageType = tech.damageType;\n const attackType = tech.type;\n const isAdvanced =\n (tech.kiCost || 0) >= 10 || (tech.staminaCost || 0) >= 15;\n\n switch (archetype) {\n case PlayerArchetype.MUSA:\n // Musa prefers: strikes, blocks, counter-attacks (BLUNT/JOINT/CRUSHING)\n if (damageType === DamageType.JOINT) score += 30;\n if (damageType === DamageType.CRUSHING) score += 25;\n if (damageType === DamageType.BLUNT) score += 20;\n if (attackType === CombatAttackType.STRIKE) score += 15;\n if (attackType === CombatAttackType.PUNCH) score += 10;\n if (isAdvanced) score += 10;\n break;\n\n case PlayerArchetype.AMSALJA:\n // Amsalja prefers: nerve strikes, pressure points, thrusts\n if (damageType === DamageType.NERVE) score += 35;\n if (damageType === DamageType.PRESSURE) score += 30;\n if (attackType === CombatAttackType.NERVE_STRIKE) score += 25;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 25;\n if (attackType === CombatAttackType.THRUST) score += 15;\n if ((tech.accuracy || 0) >= 0.9) score += 10;\n break;\n\n case PlayerArchetype.HACKER:\n // Hacker prefers: high accuracy, calculated strikes\n if ((tech.accuracy || 0) >= 0.9) score += 30;\n if ((tech.accuracy || 0) >= 0.8) score += 15;\n if (damageType === DamageType.NERVE) score += 20;\n if (damageType === DamageType.INTERNAL) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n // Jeongbo prefers: psychological pressure, submissions\n if (damageType === DamageType.PRESSURE) score += 30;\n if (damageType === DamageType.JOINT) score += 25;\n if (attackType === CombatAttackType.GRAPPLE) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n // Jojik prefers: high damage, brutal techniques\n if ((tech.damage || 0) >= 35) score += 35;\n if ((tech.damage || 0) >= 30) score += 20;\n if (damageType === DamageType.SLASHING) score += 20;\n if (damageType === DamageType.PIERCING) score += 15;\n if (damageType === DamageType.CRUSHING) score += 15;\n if (attackType === CombatAttackType.KICK) score += 10;\n break;\n }\n\n return { technique: tech, score };\n });\n\n // Sort by score descending, return highest scoring technique\n scoredTechniques.sort((a, b) => b.score - a.score);\n return scoredTechniques[0]?.technique;\n}\n\n/**\n * Calculate hit accuracy based on distance and effective reach.\n * Uses PhysicalReachCalculator for animation-aware reach calculation.\n *\n * Distance logic matches CombatSystem: if out of reach, guaranteed miss (accuracy 0).\n * Training scene coordinates are in meters, using METERS_TO_TRAINING_UNITS = 1.0.\n *\n * **IMPORTANT**: Distance calculation accounts for body radius.\n * Center-to-center distance is adjusted by subtracting target body radius\n * because attacks hit the body surface, not the center point.\n * The target body radius is calculated from physical attributes for archetypes,\n * or uses DEFAULT_BODY_RADIUS_METERS for training dummies.\n *\n * Example: If player center is 1.5m from dummy center, but dummy body\n * extends 0.23m from center, the effective distance to hit is 1.27m.\n *\n * @korean 거리 기반 명중률 계산 - 사정거리 밖이면 빗나감\n */\nfunction calculateHitAccuracy(\n playerPos: [number, number, number],\n dummyPos: [number, number, number],\n archetype: import(\"../../../../types/common\").PlayerArchetype,\n stance: TrigramStance,\n animationType?: AnimationType,\n reachConfig?: import(\"../../../../types/physics\").PhysicalReachConfig,\n): number {\n // Calculate 3D distance between player and dummy centers (in meters)\n const centerToCenterDistance = calculateDistance3D(playerPos, dummyPos);\n\n // Get player's physical attributes for reach calculation\n const playerPhysicalAttributes = getArchetypePhysicalAttributes(archetype);\n\n // Training dummy uses default body radius since it has no archetype\n // For combat between players, we would use calculateBodyRadius(targetPhysicalAttributes)\n // 훈련 더미는 원형이 없으므로 기본 몸체 반경 사용\n const targetBodyRadius = DEFAULT_BODY_RADIUS_METERS;\n\n // Effective distance = center-to-center minus target body radius only\n // Note: PhysicalReachCalculator already includes player body pivot/offset in reach calculation,\n // so we only subtract the target radius to avoid double-counting.\n // 유효 거리 = 중심간 거리 - 더미 몸체 반경 (플레이어 몸체 오프셋은 도달 거리에 포함됨)\n const effectiveDistance = Math.max(\n 0,\n centerToCenterDistance - targetBodyRadius,\n );\n\n // If animation type is available, use physics-based reach calculation\n // We use calculateMaxReach (peak time reach) because:\n // 1. Training hit detection happens at animation frame 6 (~100ms)\n // 2. But technique hit timings expect longer animations (e.g., roundhouse 200-480ms)\n // 3. Using max reach ensures the technique can hit if within peak reach range\n // 4. This matches intuitive behavior - if you're close enough to be hit by the kick, it hits\n // 훈련 타격 감지는 최대 도달 거리 사용 (애니메이션 타이밍과 기술 타이밍 불일치 보정)\n if (animationType !== undefined) {\n const maxReachMeters = physicalReachCalculator.calculateMaxReach(\n playerPhysicalAttributes,\n animationType,\n stance,\n reachConfig, // Use technique's designed reach if provided\n );\n\n // Convert reach from meters to training scene units.\n // Training scenes are authored in real-world meters, so we intentionally\n // use a 1:1 conversion here (METERS_TO_TRAINING_UNITS = 1.0).\n // Combat AI works in pixel coordinates with a dynamic px/m ratio.\n const reachInUnits = maxReachMeters * METERS_TO_TRAINING_UNITS;\n\n // STRICT DISTANCE CHECK (matches CombatSystem behavior):\n // Out of reach = guaranteed miss with accuracy 0\n if (effectiveDistance > reachInUnits) {\n return 0;\n }\n\n // Within reach: accuracy based on how centered the hit is\n // Closer = higher accuracy (0.7 to 1.0 range)\n return Math.max(0.7, 1.0 - (effectiveDistance / reachInUnits) * 0.3);\n }\n\n // Fallback: use default punch reach (0.7 meters) when no reach config is supplied.\n const defaultReach = 0.7 * METERS_TO_TRAINING_UNITS;\n if (effectiveDistance > defaultReach) {\n return 0; // Out of reach = miss\n }\n // Within default reach: linear accuracy based on distance\n return Math.max(0.5, 1.0 - (effectiveDistance / defaultReach) * 0.5);\n}\n\n/**\n * useTrainingActions hook\n * Provides training action handlers with proper memoization\n */\nexport function useTrainingActions(\n config: UseTrainingActionsConfig,\n): UseTrainingActionsReturn {\n const {\n state,\n actions,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n audio,\n playBoneImpactSound,\n playAttackSound,\n onPlayerUpdate,\n playerAnimation,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n } = config;\n\n // Ref to store timeout for dummy reset\n const dummyResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleStartTraining = useCallback(() => {\n actions.startTraining();\n audio.playSFX(\"menu_select\");\n }, [actions, audio]);\n\n const handleStopTraining = useCallback(() => {\n // Clear any pending dummy reset timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n dummyResetTimeoutRef.current = null;\n }\n actions.stopTraining();\n audio.playSFX(\"menu_back\");\n }, [actions, audio]);\n\n const handleDummyDefeated = useCallback(() => {\n actions.setFeedback(\"훈련 더미 무력화! | Dummy Defeated!\");\n audio.playSFX(\"ki_release\");\n\n // Clear any existing timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n }\n\n // Reset dummy health after delay\n dummyResetTimeoutRef.current = setTimeout(() => {\n actions.resetDummy();\n }, 2000);\n }, [actions, audio]);\n\n const handleDummyHit = useCallback(\n (\n _vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ): boolean => {\n // Get animation context from the passed attackContext parameter\n // (TrainingScreen3D.tsx should pass the attackData before clearing the ref)\n const animationType = attackContext?.animationType;\n\n // Get technique's reachConfig for accurate reach calculation\n // Priority: attackContext.techniqueId (resolved in handleAttack) > selectedTechniqueId\n // This ensures default techniques (chosen when no explicit selection) also get their reachConfig\n let reachConfig: import(\"../../../../types/physics\").PhysicalReachConfig | undefined;\n const resolvedTechniqueId =\n attackContext?.techniqueId ?? selectedTechniqueId;\n if (resolvedTechniqueId) {\n const technique = KoreanTechniquesSystem.getTechniqueById(resolvedTechniqueId);\n reachConfig = technique?.reachConfig;\n }\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n reachConfig,\n );\n\n // Determine hit position (dummy center)\n const hitPosition: [number, number, number] = [\n dummyPosition[0],\n 1.5,\n dummyPosition[2],\n ];\n\n if (accuracy > 0.5) {\n const points = Math.round(accuracy * 100);\n const damage = Math.round(accuracy * 15); // 0-15 damage based on accuracy\n const isPerfect = accuracy > 0.9;\n\n // Register hit with state (only counts if training)\n if (state.isTraining) {\n actions.registerHit(points, damage, isPerfect);\n }\n\n // Play bone impact audio with anatomical feedback using proper audio system\n if (playBoneImpactSound) {\n void playBoneImpactSound({\n damage,\n remainingHealth: 100, // Dummy has 100 health\n vitalPoint: false,\n hitPosition: { x: hitPosition[0], y: hitPosition[1], z: hitPosition[2] },\n });\n }\n\n // Determine feedback and sound\n let effectType: \"success\" | \"perfect\";\n if (isPerfect) {\n actions.setFeedback(\"완벽한 타격! | Perfect Strike!\");\n audio.playSFX(\"ki_release\");\n effectType = \"perfect\";\n } else if (accuracy > 0.7) {\n actions.setFeedback(\"좋은 타격! | Good Strike!\");\n audio.playSFX(\"ki_charge\");\n effectType = \"success\";\n } else {\n actions.setFeedback(\"타격 성공 | Strike Success\");\n audio.playSFX(\"menu_click\");\n effectType = \"success\";\n }\n\n // Add hit effect\n actions.addHitEffect({\n position: hitPosition,\n type: effectType,\n visible: true,\n damage,\n });\n\n return true;\n } else {\n // Register miss (only counts if training)\n if (state.isTraining) {\n actions.registerMiss();\n }\n actions.setFeedback(\"빗나감 | Miss - Out of reach!\");\n audio.playSFX(\"menu_navigate\");\n\n // Add miss effect\n actions.addHitEffect({\n position: hitPosition,\n type: \"miss\",\n visible: true,\n });\n\n return false;\n }\n },\n [\n state.isTraining,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n actions,\n audio,\n playBoneImpactSound,\n selectedTechniqueId,\n ],\n );\n\n const handleStanceChange = useCallback(\n (stanceIndex: number) => {\n actions.setStanceIndex(stanceIndex);\n const stance = TRIGRAM_STANCES_ORDER[stanceIndex];\n if (stance) {\n // Directly transition to stance guard animation (skips transitional animation)\n // 자세 가드 애니메이션으로 직접 전환 (전환 애니메이션 생략)\n playerAnimation.transitionToStanceGuard(stance);\n onPlayerUpdate({ currentStance: stance });\n audio.playSFX(\"stance_change\");\n }\n },\n [actions, onPlayerUpdate, audio, playerAnimation],\n );\n\n const handleAttack = useCallback(() => {\n // Determine which technique to use:\n // 1. If a technique is explicitly selected from the TechniqueBar, use that\n // 2. Otherwise, use the best default technique for the archetype + stance\n // 사용할 기술 결정: 명시적 선택 또는 원형+자세 기반 기본 기술\n let techniqueToUse: KoreanTechnique | undefined;\n let techniqueId = selectedTechniqueId;\n\n if (!techniqueId) {\n // No technique explicitly selected - get default for archetype + stance\n // 명시적 선택 없음 - 원형과 자세에 맞는 기본 기술 사용\n const defaultTechnique = getDefaultTechniqueForArchetype(\n playerArchetype,\n playerStance,\n );\n if (defaultTechnique) {\n techniqueToUse = defaultTechnique;\n techniqueId = defaultTechnique.id;\n }\n }\n\n // Get the animation type from the technique\n // 기술에서 애니메이션 타입 가져오기\n let animationType = currentTechniqueAnimationTypeRef.current;\n if (techniqueToUse?.animationType) {\n animationType = techniqueToUse.animationType;\n currentTechniqueAnimationTypeRef.current = animationType;\n }\n\n const startTime = performance.now() / 1000; // Current time in seconds\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n techniqueToUse?.reachConfig, // Pass technique's reachConfig for accurate reach\n );\n\n pendingAttackRef.current = {\n accuracy,\n vitalPoint: state.selectedVitalPoint ?? \"generic\",\n animationType,\n startTime,\n techniqueId, // Store resolved technique ID for handleDummyHit\n };\n\n // Set visual attack animation based on technique (AnimationRegistry lookup)\n // This ensures the 3D model plays the correct technique animation (kick vs punch vs elbow)\n // 기술에 따른 시각적 공격 애니메이션 설정 (발차기 vs 주먹 vs 팔꿈치)\n if (setAttackAnimation && techniqueId) {\n const animationName = getAnimationForTechnique(techniqueId);\n setAttackAnimation(animationName);\n }\n\n // Trigger attack animation - this will fire onFrame event at frame 6\n playerAnimation.transitionTo(AnimationState.ATTACK);\n\n // Play attack sound based on technique damage/intensity\n // Resolve technique data if we only have an ID (from TechniqueBar selection)\n if (!techniqueToUse && selectedTechniqueId) {\n // Technique selected from TechniqueBar but not yet resolved\n techniqueToUse = KoreanTechniquesSystem.getTechniqueById(selectedTechniqueId);\n }\n\n if (playAttackSound) {\n // Prefer explicit technique damage when available\n const damage = techniqueToUse?.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n void playAttackSound(intensity);\n } else {\n // Fallback to generic whoosh if playAttackSound not available\n audio.playSFX(\"whoosh\");\n }\n }, [\n state.selectedVitalPoint,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n playerAnimation,\n audio,\n playAttackSound,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n ]);\n\n return {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n };\n}\n\nexport default useTrainingActions;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTrainingLayout.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingLayout.ts"],"sourcesContent":["/**\n * useTrainingLayout Hook - Enhanced Responsive Training Layout\n *\n * Custom hook for managing responsive training screen layout calculations with\n * comprehensive support for all screen sizes from mobile to ultra-wide displays.\n *\n * Enhanced Features:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional scaling for consistent sizing across devices\n * - Optimized layout sizing for each device category\n * - Smooth transitions for resize operations\n * - 60fps performance maintained\n *\n * Uses robust device detection combining user-agent and screen size to ensure\n * mobile controls are shown on all mobile devices, including high-resolution phones.\n *\n * Performance:\n * - Reduces recalculations by checking only breakpoint changes, not exact dimensions\n * - Memoizes layout constants to prevent cascading re-renders\n * - Targets <1ms execution time for layout calculations\n *\n * @param width - Screen width\n * @param height - Screen height\n *\n * @returns Layout constants and training area bounds\n *\n * @example\n * ```typescript\n * const { layoutConstants, trainingAreaBounds, isMobile, screenSize } = useTrainingLayout(1200, 800);\n * ```\n */\n\nimport { useMemo } from \"react\";\nimport { getScreenSize } from \"../../../../systems/ResponsiveScaling\";\nimport { calculateArenaWorldDimensions } from \"../../../../utils/arenaWorldDimensions\";\nimport { shouldUseMobileControls } from \"../../../../utils/deviceDetection\";\nimport { calculateMobileAreaBounds } from \"../../../../utils/mobileLayoutHelpers\";\nimport {\n mobileControlsBottomClearance,\n PORTRAIT_FORCE_MAX_WIDTH_PX,\n PORTRAIT_HYSTERESIS_FACTOR,\n} from \"../../../../utils/responsiveOrientationConstants\";\nimport { getDesktopArenaWidthBudget } from \"../../../../utils/responsiveLayoutHelpers\";\n\nimport type { ScreenSize } from \"../../../../systems/ResponsiveScaling\";\n\nexport interface TrainingLayoutConstants {\n readonly padding: number;\n readonly headerHeight: number;\n readonly buttonHeight: number;\n readonly sectionSpacing: number;\n readonly controlsHeight: number;\n readonly footerHeight: number;\n}\n\nexport interface TrainingAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number; // 3D scale factor for training area (1.0 = desktop, <1.0 = mobile)\n readonly worldWidthMeters: number; // Physical training area width in meters\n readonly worldDepthMeters: number; // Physical training area depth in meters\n}\n\nexport interface TrainingLayout {\n readonly layoutConstants: TrainingLayoutConstants;\n readonly trainingAreaBounds: TrainingAreaBounds;\n readonly isMobile: boolean;\n readonly isPortrait: boolean;\n readonly screenSize: ScreenSize;\n}\n\n/**\n * Custom hook for training screen layout calculations\n * Enhanced with centralized responsive scaling system\n * Optimized to reduce recalculations and improve 60fps performance\n */\nexport function useTrainingLayout(\n width: number,\n height: number,\n): TrainingLayout {\n // Determine screen size category using centralized scaling system\n const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n // Portrait orientation with hysteresis (see responsiveOrientationConstants)\n // 세로 모드 감지 (히스테리시스 적용)\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n // Force mobile branch on narrow portrait viewports even if the user-agent\n // says we're on desktop (devtools emulation + real rotated phones).\n // 좁은 세로 화면에서는 모바일 레이아웃 강제\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n // Centralized layout constants for easier tweaking\n // Enhanced with tablet-specific values for better responsive support\n const layoutConstants = useMemo<TrainingLayoutConstants>(() => {\n // Determine if large desktop\n const isLargeDesktop = screenSize === \"xlarge\";\n const isTablet = screenSize === \"tablet\";\n\n return {\n padding: isMobile ? 20 : isTablet ? 25 : isLargeDesktop ? 35 : 30,\n headerHeight: isMobile ? 80 : isTablet ? 90 : isLargeDesktop ? 110 : 100,\n buttonHeight: isMobile ? 45 : isTablet ? 50 : isLargeDesktop ? 60 : 55,\n sectionSpacing: isMobile ? 15 : isTablet ? 18 : isLargeDesktop ? 25 : 20,\n controlsHeight: isMobile\n ? 120\n : isTablet\n ? 110\n : isLargeDesktop\n ? 150\n : 130,\n footerHeight: isMobile ? 60 : isTablet ? 70 : isLargeDesktop ? 90 : 80,\n };\n }, [isMobile, screenSize]);\n\n // Training area bounds using orientation-aware aspect-ratio sizing\n // Landscape mobile: 4:3 — horizontal dummy + analysis overlay\n // Portrait mobile : 3:4 — vertical dummy + bottom training controls fit\n const trainingAreaBounds = useMemo<TrainingAreaBounds>(() => {\n const areaY = layoutConstants.headerHeight + layoutConstants.padding;\n\n // Calculate world dimensions based on screen resolution (not device type)\n const worldDimensions = calculateArenaWorldDimensions(width);\n\n // Mobile-specific training area sizing for better screen fit\n if (isMobile) {\n const isExtraSmall = width < 380;\n const topClearance = isExtraSmall ? 75 : 80;\n // Portrait needs the full bottom band reserved (training controls +\n // footer + virtual controls) so the arena doesn't end up behind them.\n // Training's virtual-controls band is lighter than Combat's, so the\n // shared helper picks the \"training\" variant.\n const bottomClearance = mobileControlsBottomClearance(\n layoutConstants.controlsHeight,\n layoutConstants.footerHeight,\n isExtraSmall,\n isPortrait,\n \"training\",\n );\n\n return calculateMobileAreaBounds(\n width,\n height,\n topClearance,\n bottomClearance,\n areaY,\n isPortrait ? \"portrait\" : \"landscape\",\n );\n }\n\n // Desktop training area sizing - create 4:3 aspect ratio arena\n const totalReservedHeight =\n layoutConstants.headerHeight +\n layoutConstants.controlsHeight +\n layoutConstants.footerHeight;\n const totalPadding = layoutConstants.padding * 3;\n const availableHeight = height - totalReservedHeight - totalPadding;\n const availableWidth = getDesktopArenaWidthBudget(width);\n\n // Calculate arena dimensions with 4:3 aspect ratio (width > height)\n // Start with available width, constrain by height if needed\n let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n // If height is constrained, recalculate from height\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\n // Calculate pixels-per-meter and scale\n const pixelsPerMeter = arenaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - arenaWidth) / 2, // Center horizontally\n y: areaY,\n width: arenaWidth,\n height: arenaHeight, // 4:3 aspect ratio\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n }, [width, height, layoutConstants, isMobile, isPortrait]);\n\n return {\n layoutConstants,\n trainingAreaBounds,\n isMobile,\n isPortrait,\n screenSize,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,kBACd,OACA,QACgB;CAEhB,MAAM,aAAa,cAAc,cAAc,MAAM,EAAE,CAAC,MAAM,CAAC;CAI/D,MAAM,aAAa,SAAS,QAAQ;CAKpC,MAAM,WACJ,yBAAyB,IACxB,cAAc,QAAA;CAIjB,MAAM,kBAAkB,cAAuC;EAE7D,MAAM,iBAAiB,eAAe;EACtC,MAAM,WAAW,eAAe;AAEhC,SAAO;GACL,SAAS,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GAC/D,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,MAAM;GACrE,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACpE,gBAAgB,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACtE,gBAAgB,WACZ,MACA,WACE,MACA,iBACE,MACA;GACR,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACrE;IACA,CAAC,UAAU,WAAW,CAAC;AAyE1B,QAAO;EACL;EACA,oBAtEyB,cAAkC;GAC3D,MAAM,QAAQ,gBAAgB,eAAe,gBAAgB;GAG7D,MAAM,kBAAkB,8BAA8B,MAAM;AAG5D,OAAI,UAAU;IACZ,MAAM,eAAe,QAAQ;AAc7B,WAAO,0BACL,OACA,QAfmB,eAAe,KAAK,IAKjB,8BACtB,gBAAgB,gBAChB,gBAAgB,cAChB,cACA,YACA,WAOA,EACA,OACA,aAAa,aAAa,YAC3B;;GAIH,MAAM,sBACJ,gBAAgB,eAChB,gBAAgB,iBAChB,gBAAgB;GAClB,MAAM,eAAe,gBAAgB,UAAU;GAC/C,MAAM,kBAAkB,SAAS,sBAAsB;GAKvD,IAAI,aAJmB,2BAA2B,MAIjC;GACjB,IAAI,cAAc,cAAc,IAAI;AAGpC,OAAI,cAAc,iBAAiB;AACjC,kBAAc;AACd,iBAAa,eAAe,IAAI;;GAMlC,MAAM,QAFiB,aAAa,gBAAgB,cAErB;AAE/B,UAAO;IACL,IAAI,QAAQ,cAAc;IAC1B,GAAG;IACH,OAAO;IACP,QAAQ;IACR;IACA,kBAAkB,gBAAgB;IAClC,kBAAkB,gBAAgB;IACnC;KACA;GAAC;GAAO;GAAQ;GAAiB;GAAU;GAAW,CAIvD;EACA;EACA;EACA;EACD"}
1
+ {"version":3,"file":"useTrainingLayout.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingLayout.ts"],"sourcesContent":["/**\n * useTrainingLayout Hook - Enhanced Responsive Training Layout\n *\n * Custom hook for managing responsive training screen layout calculations with\n * comprehensive support for all screen sizes from mobile to ultra-wide displays.\n *\n * Enhanced Features:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional scaling for consistent sizing across devices\n * - Optimized layout sizing for each device category\n * - Smooth transitions for resize operations\n * - 60fps performance maintained\n *\n * Uses robust device detection combining user-agent and screen size to ensure\n * mobile controls are shown on all mobile devices, including high-resolution phones.\n *\n * Performance:\n * - Reduces recalculations by checking only breakpoint changes, not exact dimensions\n * - Memoizes layout constants to prevent cascading re-renders\n * - Targets <1ms execution time for layout calculations\n *\n * @param width - Screen width\n * @param height - Screen height\n *\n * @returns Layout constants and training area bounds\n *\n * @example\n * ```typescript\n * const { layoutConstants, trainingAreaBounds, isMobile, screenSize } = useTrainingLayout(1200, 800);\n * ```\n */\n\nimport { useMemo } from \"react\";\nimport { getScreenSize } from \"../../../../systems/ResponsiveScaling\";\nimport { calculateArenaWorldDimensions } from \"../../../../utils/arenaWorldDimensions\";\nimport { shouldUseMobileControls } from \"../../../../utils/deviceDetection\";\nimport { calculateMobileAreaBounds } from \"../../../../utils/mobileLayoutHelpers\";\nimport {\n mobileControlsBottomClearance,\n PORTRAIT_FORCE_MAX_WIDTH_PX,\n PORTRAIT_HYSTERESIS_FACTOR,\n} from \"../../../../utils/responsiveOrientationConstants\";\nimport { getDesktopArenaWidthBudget } from \"../../../../utils/responsiveLayoutHelpers\";\n\nimport type { ScreenSize } from \"../../../../systems/ResponsiveScaling\";\n\nexport interface TrainingLayoutConstants {\n readonly padding: number;\n readonly headerHeight: number;\n readonly buttonHeight: number;\n readonly sectionSpacing: number;\n readonly controlsHeight: number;\n readonly footerHeight: number;\n}\n\nexport interface TrainingAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number; // 3D scale factor for training area (1.0 = desktop, <1.0 = mobile)\n readonly worldWidthMeters: number; // Physical training area width in meters\n readonly worldDepthMeters: number; // Physical training area depth in meters\n}\n\nexport interface TrainingLayout {\n readonly layoutConstants: TrainingLayoutConstants;\n readonly trainingAreaBounds: TrainingAreaBounds;\n readonly isMobile: boolean;\n readonly isPortrait: boolean;\n readonly screenSize: ScreenSize;\n}\n\n/**\n * Custom hook for training screen layout calculations\n * Enhanced with centralized responsive scaling system\n * Optimized to reduce recalculations and improve 60fps performance\n */\nexport function useTrainingLayout(\n width: number,\n height: number,\n): TrainingLayout {\n // Determine screen size category using centralized scaling system\n const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n // Portrait orientation with hysteresis (see responsiveOrientationConstants)\n // 세로 모드 감지 (히스테리시스 적용)\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n // Force mobile branch on narrow portrait viewports even if the user-agent\n // says we're on desktop (devtools emulation + real rotated phones).\n // 좁은 세로 화면에서는 모바일 레이아웃 강제\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n // Centralized layout constants for easier tweaking\n // Enhanced with tablet-specific values for better responsive support\n const layoutConstants = useMemo<TrainingLayoutConstants>(() => {\n // Determine if large desktop\n const isLargeDesktop = screenSize === \"xlarge\";\n const isTablet = screenSize === \"tablet\";\n\n return {\n padding: isMobile ? 20 : isTablet ? 25 : isLargeDesktop ? 35 : 30,\n headerHeight: isMobile ? 80 : isTablet ? 90 : isLargeDesktop ? 110 : 100,\n buttonHeight: isMobile ? 45 : isTablet ? 50 : isLargeDesktop ? 60 : 55,\n sectionSpacing: isMobile ? 15 : isTablet ? 18 : isLargeDesktop ? 25 : 20,\n controlsHeight: isMobile\n ? 120\n : isTablet\n ? 110\n : isLargeDesktop\n ? 150\n : 130,\n footerHeight: isMobile ? 60 : isTablet ? 70 : isLargeDesktop ? 90 : 80,\n };\n }, [isMobile, screenSize]);\n\n // Training area bounds using orientation-aware aspect-ratio sizing\n // Landscape mobile: 4:3 — horizontal dummy + analysis overlay\n // Portrait mobile : 3:4 — vertical dummy + bottom training controls fit\n const trainingAreaBounds = useMemo<TrainingAreaBounds>(() => {\n const areaY = layoutConstants.headerHeight + layoutConstants.padding;\n\n // Calculate world dimensions based on screen resolution (not device type)\n const worldDimensions = calculateArenaWorldDimensions(width);\n\n // Mobile-specific training area sizing for better screen fit\n if (isMobile) {\n const isExtraSmall = width < 380;\n const topClearance = isExtraSmall ? 75 : 80;\n // Portrait needs the full bottom band reserved (training controls +\n // footer + virtual controls) so the arena doesn't end up behind them.\n // Training's virtual-controls band is lighter than Combat's, so the\n // shared helper picks the \"training\" variant.\n const bottomClearance = mobileControlsBottomClearance(\n layoutConstants.controlsHeight,\n layoutConstants.footerHeight,\n isExtraSmall,\n isPortrait,\n \"training\",\n );\n\n return calculateMobileAreaBounds(\n width,\n height,\n topClearance,\n bottomClearance,\n areaY,\n isPortrait ? \"portrait\" : \"landscape\",\n );\n }\n\n // Desktop training area sizing - create 4:3 aspect ratio arena\n const totalReservedHeight =\n layoutConstants.headerHeight +\n layoutConstants.controlsHeight +\n layoutConstants.footerHeight;\n const totalPadding = layoutConstants.padding * 3;\n const availableHeight = height - totalReservedHeight - totalPadding;\n const availableWidth = getDesktopArenaWidthBudget(width);\n\n // Calculate arena dimensions with 4:3 aspect ratio (width > height)\n // Start with available width, constrain by height if needed\n let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n // If height is constrained, recalculate from height\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\n // Calculate pixels-per-meter and scale\n const pixelsPerMeter = arenaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - arenaWidth) / 2, // Center horizontally\n y: areaY,\n width: arenaWidth,\n height: arenaHeight, // 4:3 aspect ratio\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n }, [width, height, layoutConstants, isMobile, isPortrait]);\n\n return {\n layoutConstants,\n trainingAreaBounds,\n isMobile,\n isPortrait,\n screenSize,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,kBACd,OACA,QACgB;CAEhB,MAAM,aAAa,cAAc,cAAc,MAAM,EAAE,CAAC,MAAM,CAAC;CAI/D,MAAM,aAAa,SAAS,QAAQ;CAKpC,MAAM,WACJ,yBAAyB,IACxB,cAAc,QAAA;CAIjB,MAAM,kBAAkB,cAAuC;EAE7D,MAAM,iBAAiB,eAAe;EACtC,MAAM,WAAW,eAAe;EAEhC,OAAO;GACL,SAAS,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GAC/D,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,MAAM;GACrE,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACpE,gBAAgB,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACtE,gBAAgB,WACZ,MACA,WACE,MACA,iBACE,MACA;GACR,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACrE;IACA,CAAC,UAAU,WAAW,CAAC;CAyE1B,OAAO;EACL;EACA,oBAtEyB,cAAkC;GAC3D,MAAM,QAAQ,gBAAgB,eAAe,gBAAgB;GAG7D,MAAM,kBAAkB,8BAA8B,MAAM;GAG5D,IAAI,UAAU;IACZ,MAAM,eAAe,QAAQ;IAc7B,OAAO,0BACL,OACA,QAfmB,eAAe,KAAK,IAKjB,8BACtB,gBAAgB,gBAChB,gBAAgB,cAChB,cACA,YACA,WAOA,EACA,OACA,aAAa,aAAa,YAC3B;;GAIH,MAAM,sBACJ,gBAAgB,eAChB,gBAAgB,iBAChB,gBAAgB;GAClB,MAAM,eAAe,gBAAgB,UAAU;GAC/C,MAAM,kBAAkB,SAAS,sBAAsB;GAKvD,IAAI,aAJmB,2BAA2B,MAIjC;GACjB,IAAI,cAAc,cAAc,IAAI;GAGpC,IAAI,cAAc,iBAAiB;IACjC,cAAc;IACd,aAAa,eAAe,IAAI;;GAMlC,MAAM,QAFiB,aAAa,gBAAgB,cAErB;GAE/B,OAAO;IACL,IAAI,QAAQ,cAAc;IAC1B,GAAG;IACH,OAAO;IACP,QAAQ;IACR;IACA,kBAAkB,gBAAgB;IAClC,kBAAkB,gBAAgB;IACnC;KACA;GAAC;GAAO;GAAQ;GAAiB;GAAU;GAAW,CAIvD;EACA;EACA;EACA;EACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTrainingState.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingState.ts"],"sourcesContent":["/**\n * useTrainingState Hook - Consolidated Training State Management\n *\n * Custom hook for managing training state using useReducer.\n * Mirrors useCombatState pattern for consistency.\n *\n * @korean 훈련상태관리훅 - useReducer를 사용한 통합 훈련 상태 관리\n */\n\nimport { useCallback, useMemo, useReducer } from \"react\";\n// Re-export types from components for consistency\nimport type { AnatomyLayer } from \"../components/AnatomyOverlay3D\";\nimport type { FootworkDrill } from \"../components/FootworkDrillsOverlayHtml\";\nimport type { TrainingMode } from \"../components/TrainingModeSelectorOverlayHtml\";\n\n// Re-export for convenience\nexport type { AnatomyLayer, FootworkDrill, TrainingMode };\n\n/**\n * Training statistics\n */\nexport interface TrainingStats {\n readonly score: number;\n readonly combo: number;\n readonly hits: number;\n readonly misses: number;\n readonly accuracy: number;\n readonly sessionDuration?: number;\n readonly bestCombo?: number;\n readonly perfectStrikes?: number;\n}\n\n/**\n * Hit effect for training\n */\nexport interface TrainingHitEffect {\n readonly id: number;\n readonly position: [number, number, number];\n readonly type: \"success\" | \"perfect\" | \"miss\";\n readonly visible: boolean;\n readonly damage?: number;\n}\n\n/**\n * Training screen state managed by the reducer\n */\nexport interface TrainingScreenState {\n readonly trainingMode: TrainingMode;\n readonly isTraining: boolean;\n readonly selectedVitalPoint: string | null;\n readonly feedback: string;\n readonly showFeedback: boolean;\n readonly hitEffects: TrainingHitEffect[];\n readonly nextEffectId: number;\n readonly dummyHealth: number;\n readonly sessionStartTime: number | null;\n readonly sessionDuration: number;\n readonly perfectStrikes: number;\n readonly bestCombo: number;\n readonly stats: TrainingStats;\n readonly currentStanceIndex: number;\n readonly stanceWheelExpanded: boolean;\n readonly visibleAnatomyLayers: AnatomyLayer[];\n // Footwork drill state\n readonly footworkDrillActive: boolean;\n readonly footworkDrillType: FootworkDrill;\n readonly footworkDrillStep: number;\n}\n\n/**\n * Training state actions\n */\ntype TrainingAction =\n | { type: \"SET_TRAINING_MODE\"; payload: TrainingMode }\n | { type: \"START_TRAINING\" }\n | { type: \"STOP_TRAINING\" }\n | { type: \"SET_SELECTED_VITAL_POINT\"; payload: string | null }\n | { type: \"SET_FEEDBACK\"; payload: { feedback: string; show: boolean } }\n | { type: \"HIDE_FEEDBACK\" }\n | { type: \"ADD_HIT_EFFECT\"; payload: Omit<TrainingHitEffect, \"id\"> }\n | { type: \"REMOVE_HIT_EFFECT\"; payload: number }\n | { type: \"SET_DUMMY_HEALTH\"; payload: number }\n | { type: \"RESET_DUMMY\" }\n | { type: \"UPDATE_SESSION_DURATION\"; payload: number }\n | { type: \"INCREMENT_PERFECT_STRIKES\" }\n | { type: \"UPDATE_STATS\"; payload: Partial<TrainingStats> }\n | {\n type: \"REGISTER_HIT\";\n payload: { points: number; damage: number; isPerfect: boolean };\n }\n | { type: \"REGISTER_MISS\" }\n | { type: \"SET_STANCE_INDEX\"; payload: number }\n | { type: \"TOGGLE_STANCE_WHEEL\" }\n | { type: \"UPDATE_BEST_COMBO\"; payload: number }\n | { type: \"TOGGLE_ANATOMY_LAYER\"; payload: AnatomyLayer }\n | { type: \"SET_ANATOMY_LAYERS\"; payload: AnatomyLayer[] }\n | { type: \"START_FOOTWORK_DRILL\"; payload: FootworkDrill }\n | { type: \"STOP_FOOTWORK_DRILL\" }\n | { type: \"ADVANCE_FOOTWORK_STEP\" }\n | { type: \"RESET_FOOTWORK_DRILL\" };\n\n/**\n * Initial training state\n */\nconst initialState: TrainingScreenState = {\n trainingMode: \"basics\",\n isTraining: false,\n selectedVitalPoint: null,\n feedback: \"\",\n showFeedback: false,\n hitEffects: [],\n nextEffectId: 0,\n dummyHealth: 100,\n sessionStartTime: null,\n sessionDuration: 0,\n perfectStrikes: 0,\n bestCombo: 0,\n stats: {\n score: 0,\n combo: 0,\n hits: 0,\n misses: 0,\n accuracy: 0,\n },\n currentStanceIndex: 0,\n stanceWheelExpanded: false,\n visibleAnatomyLayers: [],\n footworkDrillActive: false,\n footworkDrillType: \"circular_left\",\n footworkDrillStep: 0,\n};\n\n/**\n * Training state reducer\n */\nfunction trainingReducer(\n state: TrainingScreenState,\n action: TrainingAction\n): TrainingScreenState {\n switch (action.type) {\n case \"SET_TRAINING_MODE\":\n return { ...state, trainingMode: action.payload };\n\n case \"START_TRAINING\":\n return {\n ...state,\n isTraining: true,\n sessionStartTime: Date.now(),\n dummyHealth: 100,\n perfectStrikes: 0,\n bestCombo: 0,\n stats: {\n score: 0,\n combo: 0,\n hits: 0,\n misses: 0,\n accuracy: 0,\n },\n feedback: \"훈련 시작! | Training Start!\",\n showFeedback: true,\n };\n\n case \"STOP_TRAINING\":\n return {\n ...state,\n isTraining: false,\n sessionStartTime: null,\n sessionDuration: 0,\n feedback: \"훈련 종료 | Training End\",\n showFeedback: true,\n };\n\n case \"SET_SELECTED_VITAL_POINT\":\n return { ...state, selectedVitalPoint: action.payload };\n\n case \"SET_FEEDBACK\":\n return {\n ...state,\n feedback: action.payload.feedback,\n showFeedback: action.payload.show,\n };\n\n case \"HIDE_FEEDBACK\":\n return { ...state, showFeedback: false };\n\n case \"ADD_HIT_EFFECT\":\n return {\n ...state,\n hitEffects: [\n ...state.hitEffects,\n { ...action.payload, id: state.nextEffectId },\n ],\n nextEffectId: state.nextEffectId + 1,\n };\n\n case \"REMOVE_HIT_EFFECT\":\n return {\n ...state,\n hitEffects: state.hitEffects.filter((e) => e.id !== action.payload),\n };\n\n case \"SET_DUMMY_HEALTH\":\n return { ...state, dummyHealth: action.payload };\n\n case \"RESET_DUMMY\":\n return {\n ...state,\n dummyHealth: 100,\n feedback: \"더미 재설정 | Dummy Reset\",\n showFeedback: true,\n };\n\n case \"UPDATE_SESSION_DURATION\":\n return { ...state, sessionDuration: action.payload };\n\n case \"INCREMENT_PERFECT_STRIKES\":\n return { ...state, perfectStrikes: state.perfectStrikes + 1 };\n\n case \"UPDATE_STATS\":\n return { ...state, stats: { ...state.stats, ...action.payload } };\n\n case \"REGISTER_HIT\": {\n const newHits = state.stats.hits + 1;\n const totalAttempts = newHits + state.stats.misses;\n const newCombo = state.stats.combo + 1;\n const newBestCombo = Math.max(state.bestCombo, newCombo);\n\n return {\n ...state,\n perfectStrikes: action.payload.isPerfect\n ? state.perfectStrikes + 1\n : state.perfectStrikes,\n bestCombo: newBestCombo,\n dummyHealth: Math.max(0, state.dummyHealth - action.payload.damage),\n stats: {\n ...state.stats,\n score: state.stats.score + action.payload.points,\n combo: newCombo,\n hits: newHits,\n accuracy: totalAttempts > 0 ? (newHits / totalAttempts) * 100 : 0,\n },\n };\n }\n\n case \"REGISTER_MISS\": {\n const newMisses = state.stats.misses + 1;\n const totalAttempts = state.stats.hits + newMisses;\n\n return {\n ...state,\n stats: {\n ...state.stats,\n combo: 0,\n misses: newMisses,\n accuracy:\n totalAttempts > 0 ? (state.stats.hits / totalAttempts) * 100 : 0,\n },\n };\n }\n\n case \"SET_STANCE_INDEX\":\n return { ...state, currentStanceIndex: action.payload };\n\n case \"TOGGLE_STANCE_WHEEL\":\n return { ...state, stanceWheelExpanded: !state.stanceWheelExpanded };\n\n case \"UPDATE_BEST_COMBO\":\n return {\n ...state,\n bestCombo: Math.max(state.bestCombo, action.payload),\n };\n\n case \"TOGGLE_ANATOMY_LAYER\": {\n const layer = action.payload;\n const currentLayers = state.visibleAnatomyLayers;\n const newLayers = currentLayers.includes(layer)\n ? currentLayers.filter((l) => l !== layer)\n : [...currentLayers, layer];\n return { ...state, visibleAnatomyLayers: newLayers };\n }\n\n case \"SET_ANATOMY_LAYERS\":\n return { ...state, visibleAnatomyLayers: action.payload };\n\n case \"START_FOOTWORK_DRILL\":\n return {\n ...state,\n footworkDrillActive: true,\n footworkDrillType: action.payload,\n footworkDrillStep: 0,\n feedback: \"보법 훈련 시작! | Footwork drill started!\",\n showFeedback: true,\n };\n\n case \"STOP_FOOTWORK_DRILL\":\n return {\n ...state,\n footworkDrillActive: false,\n footworkDrillStep: 0,\n feedback: \"보법 훈련 종료! | Footwork drill stopped!\",\n showFeedback: true,\n };\n\n case \"ADVANCE_FOOTWORK_STEP\":\n return {\n ...state,\n footworkDrillStep: state.footworkDrillStep + 1,\n };\n\n case \"RESET_FOOTWORK_DRILL\":\n return {\n ...state,\n footworkDrillStep: 0,\n };\n\n default:\n return state;\n }\n}\n\n/**\n * Training state actions interface\n */\nexport interface TrainingActions {\n readonly setTrainingMode: (mode: TrainingMode) => void;\n readonly startTraining: () => void;\n readonly stopTraining: () => void;\n readonly setSelectedVitalPoint: (point: string | null) => void;\n readonly setFeedback: (feedback: string, show?: boolean) => void;\n readonly hideFeedback: () => void;\n readonly addHitEffect: (effect: Omit<TrainingHitEffect, \"id\">) => void;\n readonly removeHitEffect: (id: number) => void;\n readonly setDummyHealth: (health: number) => void;\n readonly resetDummy: () => void;\n readonly updateSessionDuration: (duration: number) => void;\n readonly registerHit: (\n points: number,\n damage: number,\n isPerfect: boolean\n ) => void;\n readonly registerMiss: () => void;\n readonly setStanceIndex: (index: number) => void;\n readonly toggleStanceWheel: () => void;\n readonly updateBestCombo: (combo: number) => void;\n readonly toggleAnatomyLayer: (layer: AnatomyLayer) => void;\n readonly setAnatomyLayers: (layers: AnatomyLayer[]) => void;\n readonly startFootworkDrill: (drillType: FootworkDrill) => void;\n readonly stopFootworkDrill: () => void;\n readonly advanceFootworkStep: () => void;\n readonly resetFootworkDrill: () => void;\n}\n\n/**\n * Hook return type\n */\nexport interface UseTrainingStateReturn {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n}\n\n/**\n * useTrainingState hook\n * Provides consolidated training state management using useReducer\n */\nexport function useTrainingState(): UseTrainingStateReturn {\n const [state, dispatch] = useReducer(trainingReducer, initialState);\n\n // Memoize individual callbacks to prevent recreating on every render\n const setTrainingMode = useCallback(\n (mode: TrainingMode) =>\n dispatch({ type: \"SET_TRAINING_MODE\", payload: mode }),\n []\n );\n const startTraining = useCallback(\n () => dispatch({ type: \"START_TRAINING\" }),\n []\n );\n const stopTraining = useCallback(\n () => dispatch({ type: \"STOP_TRAINING\" }),\n []\n );\n const setSelectedVitalPoint = useCallback(\n (point: string | null) =>\n dispatch({ type: \"SET_SELECTED_VITAL_POINT\", payload: point }),\n []\n );\n const setFeedback = useCallback(\n (feedback: string, show = true) =>\n dispatch({ type: \"SET_FEEDBACK\", payload: { feedback, show } }),\n []\n );\n const hideFeedback = useCallback(\n () => dispatch({ type: \"HIDE_FEEDBACK\" }),\n []\n );\n const addHitEffect = useCallback(\n (effect: Omit<TrainingHitEffect, \"id\">) =>\n dispatch({ type: \"ADD_HIT_EFFECT\", payload: effect }),\n []\n );\n const removeHitEffect = useCallback(\n (id: number) => dispatch({ type: \"REMOVE_HIT_EFFECT\", payload: id }),\n []\n );\n const setDummyHealth = useCallback(\n (health: number) => dispatch({ type: \"SET_DUMMY_HEALTH\", payload: health }),\n []\n );\n const resetDummy = useCallback(() => dispatch({ type: \"RESET_DUMMY\" }), []);\n const updateSessionDuration = useCallback(\n (duration: number) =>\n dispatch({ type: \"UPDATE_SESSION_DURATION\", payload: duration }),\n []\n );\n const registerHit = useCallback(\n (points: number, damage: number, isPerfect: boolean) =>\n dispatch({\n type: \"REGISTER_HIT\",\n payload: { points, damage, isPerfect },\n }),\n []\n );\n const registerMiss = useCallback(\n () => dispatch({ type: \"REGISTER_MISS\" }),\n []\n );\n const setStanceIndex = useCallback(\n (index: number) => dispatch({ type: \"SET_STANCE_INDEX\", payload: index }),\n []\n );\n const toggleStanceWheel = useCallback(\n () => dispatch({ type: \"TOGGLE_STANCE_WHEEL\" }),\n []\n );\n const updateBestCombo = useCallback(\n (combo: number) => dispatch({ type: \"UPDATE_BEST_COMBO\", payload: combo }),\n []\n );\n const toggleAnatomyLayer = useCallback(\n (layer: AnatomyLayer) =>\n dispatch({ type: \"TOGGLE_ANATOMY_LAYER\", payload: layer }),\n []\n );\n const setAnatomyLayers = useCallback(\n (layers: AnatomyLayer[]) =>\n dispatch({ type: \"SET_ANATOMY_LAYERS\", payload: layers }),\n []\n );\n const startFootworkDrill = useCallback((drillType: FootworkDrill) => {\n dispatch({ type: \"START_FOOTWORK_DRILL\", payload: drillType });\n }, []);\n const stopFootworkDrill = useCallback(() => {\n dispatch({ type: \"STOP_FOOTWORK_DRILL\" });\n }, []);\n const advanceFootworkStep = useCallback(() => {\n dispatch({ type: \"ADVANCE_FOOTWORK_STEP\" });\n }, []);\n const resetFootworkDrill = useCallback(() => {\n dispatch({ type: \"RESET_FOOTWORK_DRILL\" });\n }, []);\n\n // Memoize the actions object to prevent unnecessary re-renders\n // All callbacks are stable since they have empty dependency arrays\n const actions: TrainingActions = useMemo(\n () => ({\n setTrainingMode,\n startTraining,\n stopTraining,\n setSelectedVitalPoint,\n setFeedback,\n hideFeedback,\n addHitEffect,\n removeHitEffect,\n setDummyHealth,\n resetDummy,\n updateSessionDuration,\n registerHit,\n registerMiss,\n setStanceIndex,\n toggleStanceWheel,\n updateBestCombo,\n toggleAnatomyLayer,\n setAnatomyLayers,\n startFootworkDrill,\n stopFootworkDrill,\n advanceFootworkStep,\n resetFootworkDrill,\n }),\n [\n setTrainingMode,\n startTraining,\n stopTraining,\n setSelectedVitalPoint,\n setFeedback,\n hideFeedback,\n addHitEffect,\n removeHitEffect,\n setDummyHealth,\n resetDummy,\n updateSessionDuration,\n registerHit,\n registerMiss,\n setStanceIndex,\n toggleStanceWheel,\n updateBestCombo,\n toggleAnatomyLayer,\n setAnatomyLayers,\n startFootworkDrill,\n stopFootworkDrill,\n advanceFootworkStep,\n resetFootworkDrill,\n ]\n );\n\n return { state, actions };\n}\n\nexport default useTrainingState;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useTrainingState.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingState.ts"],"sourcesContent":["/**\n * useTrainingState Hook - Consolidated Training State Management\n *\n * Custom hook for managing training state using useReducer.\n * Mirrors useCombatState pattern for consistency.\n *\n * @korean 훈련상태관리훅 - useReducer를 사용한 통합 훈련 상태 관리\n */\n\nimport { useCallback, useMemo, useReducer } from \"react\";\n// Re-export types from components for consistency\nimport type { AnatomyLayer } from \"../components/AnatomyOverlay3D\";\nimport type { FootworkDrill } from \"../components/FootworkDrillsOverlayHtml\";\nimport type { TrainingMode } from \"../components/TrainingModeSelectorOverlayHtml\";\n\n// Re-export for convenience\nexport type { AnatomyLayer, FootworkDrill, TrainingMode };\n\n/**\n * Training statistics\n */\nexport interface TrainingStats {\n readonly score: number;\n readonly combo: number;\n readonly hits: number;\n readonly misses: number;\n readonly accuracy: number;\n readonly sessionDuration?: number;\n readonly bestCombo?: number;\n readonly perfectStrikes?: number;\n}\n\n/**\n * Hit effect for training\n */\nexport interface TrainingHitEffect {\n readonly id: number;\n readonly position: [number, number, number];\n readonly type: \"success\" | \"perfect\" | \"miss\";\n readonly visible: boolean;\n readonly damage?: number;\n}\n\n/**\n * Training screen state managed by the reducer\n */\nexport interface TrainingScreenState {\n readonly trainingMode: TrainingMode;\n readonly isTraining: boolean;\n readonly selectedVitalPoint: string | null;\n readonly feedback: string;\n readonly showFeedback: boolean;\n readonly hitEffects: TrainingHitEffect[];\n readonly nextEffectId: number;\n readonly dummyHealth: number;\n readonly sessionStartTime: number | null;\n readonly sessionDuration: number;\n readonly perfectStrikes: number;\n readonly bestCombo: number;\n readonly stats: TrainingStats;\n readonly currentStanceIndex: number;\n readonly stanceWheelExpanded: boolean;\n readonly visibleAnatomyLayers: AnatomyLayer[];\n // Footwork drill state\n readonly footworkDrillActive: boolean;\n readonly footworkDrillType: FootworkDrill;\n readonly footworkDrillStep: number;\n}\n\n/**\n * Training state actions\n */\ntype TrainingAction =\n | { type: \"SET_TRAINING_MODE\"; payload: TrainingMode }\n | { type: \"START_TRAINING\" }\n | { type: \"STOP_TRAINING\" }\n | { type: \"SET_SELECTED_VITAL_POINT\"; payload: string | null }\n | { type: \"SET_FEEDBACK\"; payload: { feedback: string; show: boolean } }\n | { type: \"HIDE_FEEDBACK\" }\n | { type: \"ADD_HIT_EFFECT\"; payload: Omit<TrainingHitEffect, \"id\"> }\n | { type: \"REMOVE_HIT_EFFECT\"; payload: number }\n | { type: \"SET_DUMMY_HEALTH\"; payload: number }\n | { type: \"RESET_DUMMY\" }\n | { type: \"UPDATE_SESSION_DURATION\"; payload: number }\n | { type: \"INCREMENT_PERFECT_STRIKES\" }\n | { type: \"UPDATE_STATS\"; payload: Partial<TrainingStats> }\n | {\n type: \"REGISTER_HIT\";\n payload: { points: number; damage: number; isPerfect: boolean };\n }\n | { type: \"REGISTER_MISS\" }\n | { type: \"SET_STANCE_INDEX\"; payload: number }\n | { type: \"TOGGLE_STANCE_WHEEL\" }\n | { type: \"UPDATE_BEST_COMBO\"; payload: number }\n | { type: \"TOGGLE_ANATOMY_LAYER\"; payload: AnatomyLayer }\n | { type: \"SET_ANATOMY_LAYERS\"; payload: AnatomyLayer[] }\n | { type: \"START_FOOTWORK_DRILL\"; payload: FootworkDrill }\n | { type: \"STOP_FOOTWORK_DRILL\" }\n | { type: \"ADVANCE_FOOTWORK_STEP\" }\n | { type: \"RESET_FOOTWORK_DRILL\" };\n\n/**\n * Initial training state\n */\nconst initialState: TrainingScreenState = {\n trainingMode: \"basics\",\n isTraining: false,\n selectedVitalPoint: null,\n feedback: \"\",\n showFeedback: false,\n hitEffects: [],\n nextEffectId: 0,\n dummyHealth: 100,\n sessionStartTime: null,\n sessionDuration: 0,\n perfectStrikes: 0,\n bestCombo: 0,\n stats: {\n score: 0,\n combo: 0,\n hits: 0,\n misses: 0,\n accuracy: 0,\n },\n currentStanceIndex: 0,\n stanceWheelExpanded: false,\n visibleAnatomyLayers: [],\n footworkDrillActive: false,\n footworkDrillType: \"circular_left\",\n footworkDrillStep: 0,\n};\n\n/**\n * Training state reducer\n */\nfunction trainingReducer(\n state: TrainingScreenState,\n action: TrainingAction\n): TrainingScreenState {\n switch (action.type) {\n case \"SET_TRAINING_MODE\":\n return { ...state, trainingMode: action.payload };\n\n case \"START_TRAINING\":\n return {\n ...state,\n isTraining: true,\n sessionStartTime: Date.now(),\n dummyHealth: 100,\n perfectStrikes: 0,\n bestCombo: 0,\n stats: {\n score: 0,\n combo: 0,\n hits: 0,\n misses: 0,\n accuracy: 0,\n },\n feedback: \"훈련 시작! | Training Start!\",\n showFeedback: true,\n };\n\n case \"STOP_TRAINING\":\n return {\n ...state,\n isTraining: false,\n sessionStartTime: null,\n sessionDuration: 0,\n feedback: \"훈련 종료 | Training End\",\n showFeedback: true,\n };\n\n case \"SET_SELECTED_VITAL_POINT\":\n return { ...state, selectedVitalPoint: action.payload };\n\n case \"SET_FEEDBACK\":\n return {\n ...state,\n feedback: action.payload.feedback,\n showFeedback: action.payload.show,\n };\n\n case \"HIDE_FEEDBACK\":\n return { ...state, showFeedback: false };\n\n case \"ADD_HIT_EFFECT\":\n return {\n ...state,\n hitEffects: [\n ...state.hitEffects,\n { ...action.payload, id: state.nextEffectId },\n ],\n nextEffectId: state.nextEffectId + 1,\n };\n\n case \"REMOVE_HIT_EFFECT\":\n return {\n ...state,\n hitEffects: state.hitEffects.filter((e) => e.id !== action.payload),\n };\n\n case \"SET_DUMMY_HEALTH\":\n return { ...state, dummyHealth: action.payload };\n\n case \"RESET_DUMMY\":\n return {\n ...state,\n dummyHealth: 100,\n feedback: \"더미 재설정 | Dummy Reset\",\n showFeedback: true,\n };\n\n case \"UPDATE_SESSION_DURATION\":\n return { ...state, sessionDuration: action.payload };\n\n case \"INCREMENT_PERFECT_STRIKES\":\n return { ...state, perfectStrikes: state.perfectStrikes + 1 };\n\n case \"UPDATE_STATS\":\n return { ...state, stats: { ...state.stats, ...action.payload } };\n\n case \"REGISTER_HIT\": {\n const newHits = state.stats.hits + 1;\n const totalAttempts = newHits + state.stats.misses;\n const newCombo = state.stats.combo + 1;\n const newBestCombo = Math.max(state.bestCombo, newCombo);\n\n return {\n ...state,\n perfectStrikes: action.payload.isPerfect\n ? state.perfectStrikes + 1\n : state.perfectStrikes,\n bestCombo: newBestCombo,\n dummyHealth: Math.max(0, state.dummyHealth - action.payload.damage),\n stats: {\n ...state.stats,\n score: state.stats.score + action.payload.points,\n combo: newCombo,\n hits: newHits,\n accuracy: totalAttempts > 0 ? (newHits / totalAttempts) * 100 : 0,\n },\n };\n }\n\n case \"REGISTER_MISS\": {\n const newMisses = state.stats.misses + 1;\n const totalAttempts = state.stats.hits + newMisses;\n\n return {\n ...state,\n stats: {\n ...state.stats,\n combo: 0,\n misses: newMisses,\n accuracy:\n totalAttempts > 0 ? (state.stats.hits / totalAttempts) * 100 : 0,\n },\n };\n }\n\n case \"SET_STANCE_INDEX\":\n return { ...state, currentStanceIndex: action.payload };\n\n case \"TOGGLE_STANCE_WHEEL\":\n return { ...state, stanceWheelExpanded: !state.stanceWheelExpanded };\n\n case \"UPDATE_BEST_COMBO\":\n return {\n ...state,\n bestCombo: Math.max(state.bestCombo, action.payload),\n };\n\n case \"TOGGLE_ANATOMY_LAYER\": {\n const layer = action.payload;\n const currentLayers = state.visibleAnatomyLayers;\n const newLayers = currentLayers.includes(layer)\n ? currentLayers.filter((l) => l !== layer)\n : [...currentLayers, layer];\n return { ...state, visibleAnatomyLayers: newLayers };\n }\n\n case \"SET_ANATOMY_LAYERS\":\n return { ...state, visibleAnatomyLayers: action.payload };\n\n case \"START_FOOTWORK_DRILL\":\n return {\n ...state,\n footworkDrillActive: true,\n footworkDrillType: action.payload,\n footworkDrillStep: 0,\n feedback: \"보법 훈련 시작! | Footwork drill started!\",\n showFeedback: true,\n };\n\n case \"STOP_FOOTWORK_DRILL\":\n return {\n ...state,\n footworkDrillActive: false,\n footworkDrillStep: 0,\n feedback: \"보법 훈련 종료! | Footwork drill stopped!\",\n showFeedback: true,\n };\n\n case \"ADVANCE_FOOTWORK_STEP\":\n return {\n ...state,\n footworkDrillStep: state.footworkDrillStep + 1,\n };\n\n case \"RESET_FOOTWORK_DRILL\":\n return {\n ...state,\n footworkDrillStep: 0,\n };\n\n default:\n return state;\n }\n}\n\n/**\n * Training state actions interface\n */\nexport interface TrainingActions {\n readonly setTrainingMode: (mode: TrainingMode) => void;\n readonly startTraining: () => void;\n readonly stopTraining: () => void;\n readonly setSelectedVitalPoint: (point: string | null) => void;\n readonly setFeedback: (feedback: string, show?: boolean) => void;\n readonly hideFeedback: () => void;\n readonly addHitEffect: (effect: Omit<TrainingHitEffect, \"id\">) => void;\n readonly removeHitEffect: (id: number) => void;\n readonly setDummyHealth: (health: number) => void;\n readonly resetDummy: () => void;\n readonly updateSessionDuration: (duration: number) => void;\n readonly registerHit: (\n points: number,\n damage: number,\n isPerfect: boolean\n ) => void;\n readonly registerMiss: () => void;\n readonly setStanceIndex: (index: number) => void;\n readonly toggleStanceWheel: () => void;\n readonly updateBestCombo: (combo: number) => void;\n readonly toggleAnatomyLayer: (layer: AnatomyLayer) => void;\n readonly setAnatomyLayers: (layers: AnatomyLayer[]) => void;\n readonly startFootworkDrill: (drillType: FootworkDrill) => void;\n readonly stopFootworkDrill: () => void;\n readonly advanceFootworkStep: () => void;\n readonly resetFootworkDrill: () => void;\n}\n\n/**\n * Hook return type\n */\nexport interface UseTrainingStateReturn {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n}\n\n/**\n * useTrainingState hook\n * Provides consolidated training state management using useReducer\n */\nexport function useTrainingState(): UseTrainingStateReturn {\n const [state, dispatch] = useReducer(trainingReducer, initialState);\n\n // Memoize individual callbacks to prevent recreating on every render\n const setTrainingMode = useCallback(\n (mode: TrainingMode) =>\n dispatch({ type: \"SET_TRAINING_MODE\", payload: mode }),\n []\n );\n const startTraining = useCallback(\n () => dispatch({ type: \"START_TRAINING\" }),\n []\n );\n const stopTraining = useCallback(\n () => dispatch({ type: \"STOP_TRAINING\" }),\n []\n );\n const setSelectedVitalPoint = useCallback(\n (point: string | null) =>\n dispatch({ type: \"SET_SELECTED_VITAL_POINT\", payload: point }),\n []\n );\n const setFeedback = useCallback(\n (feedback: string, show = true) =>\n dispatch({ type: \"SET_FEEDBACK\", payload: { feedback, show } }),\n []\n );\n const hideFeedback = useCallback(\n () => dispatch({ type: \"HIDE_FEEDBACK\" }),\n []\n );\n const addHitEffect = useCallback(\n (effect: Omit<TrainingHitEffect, \"id\">) =>\n dispatch({ type: \"ADD_HIT_EFFECT\", payload: effect }),\n []\n );\n const removeHitEffect = useCallback(\n (id: number) => dispatch({ type: \"REMOVE_HIT_EFFECT\", payload: id }),\n []\n );\n const setDummyHealth = useCallback(\n (health: number) => dispatch({ type: \"SET_DUMMY_HEALTH\", payload: health }),\n []\n );\n const resetDummy = useCallback(() => dispatch({ type: \"RESET_DUMMY\" }), []);\n const updateSessionDuration = useCallback(\n (duration: number) =>\n dispatch({ type: \"UPDATE_SESSION_DURATION\", payload: duration }),\n []\n );\n const registerHit = useCallback(\n (points: number, damage: number, isPerfect: boolean) =>\n dispatch({\n type: \"REGISTER_HIT\",\n payload: { points, damage, isPerfect },\n }),\n []\n );\n const registerMiss = useCallback(\n () => dispatch({ type: \"REGISTER_MISS\" }),\n []\n );\n const setStanceIndex = useCallback(\n (index: number) => dispatch({ type: \"SET_STANCE_INDEX\", payload: index }),\n []\n );\n const toggleStanceWheel = useCallback(\n () => dispatch({ type: \"TOGGLE_STANCE_WHEEL\" }),\n []\n );\n const updateBestCombo = useCallback(\n (combo: number) => dispatch({ type: \"UPDATE_BEST_COMBO\", payload: combo }),\n []\n );\n const toggleAnatomyLayer = useCallback(\n (layer: AnatomyLayer) =>\n dispatch({ type: \"TOGGLE_ANATOMY_LAYER\", payload: layer }),\n []\n );\n const setAnatomyLayers = useCallback(\n (layers: AnatomyLayer[]) =>\n dispatch({ type: \"SET_ANATOMY_LAYERS\", payload: layers }),\n []\n );\n const startFootworkDrill = useCallback((drillType: FootworkDrill) => {\n dispatch({ type: \"START_FOOTWORK_DRILL\", payload: drillType });\n }, []);\n const stopFootworkDrill = useCallback(() => {\n dispatch({ type: \"STOP_FOOTWORK_DRILL\" });\n }, []);\n const advanceFootworkStep = useCallback(() => {\n dispatch({ type: \"ADVANCE_FOOTWORK_STEP\" });\n }, []);\n const resetFootworkDrill = useCallback(() => {\n dispatch({ type: \"RESET_FOOTWORK_DRILL\" });\n }, []);\n\n // Memoize the actions object to prevent unnecessary re-renders\n // All callbacks are stable since they have empty dependency arrays\n const actions: TrainingActions = useMemo(\n () => ({\n setTrainingMode,\n startTraining,\n stopTraining,\n setSelectedVitalPoint,\n setFeedback,\n hideFeedback,\n addHitEffect,\n removeHitEffect,\n setDummyHealth,\n resetDummy,\n updateSessionDuration,\n registerHit,\n registerMiss,\n setStanceIndex,\n toggleStanceWheel,\n updateBestCombo,\n toggleAnatomyLayer,\n setAnatomyLayers,\n startFootworkDrill,\n stopFootworkDrill,\n advanceFootworkStep,\n resetFootworkDrill,\n }),\n [\n setTrainingMode,\n startTraining,\n stopTraining,\n setSelectedVitalPoint,\n setFeedback,\n hideFeedback,\n addHitEffect,\n removeHitEffect,\n setDummyHealth,\n resetDummy,\n updateSessionDuration,\n registerHit,\n registerMiss,\n setStanceIndex,\n toggleStanceWheel,\n updateBestCombo,\n toggleAnatomyLayer,\n setAnatomyLayers,\n startFootworkDrill,\n stopFootworkDrill,\n advanceFootworkStep,\n resetFootworkDrill,\n ]\n );\n\n return { state, actions };\n}\n\nexport default useTrainingState;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BaseButton.js","names":[],"sources":["../../../../src/components/shared/base/BaseButton.tsx"],"sourcesContent":["/**\n * BaseButton - Enhanced button component with Korean theming\n *\n * Builds on existing KoreanButton with extracted common logic\n * Provides consistent button styling across the application\n *\n * Now with Html overlay positioning helpers for:\n * - Consistent z-index management\n * - Performance optimization with distanceFactor\n * - GPU acceleration\n *\n * @module components/base\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, {\n useCallback,\n useMemo,\n useState,\n useEffect,\n useRef,\n} from \"react\";\nimport { KOREAN_COLORS, UI_DIMENSIONS } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../utils/htmlOverlayHelpers\";\nimport type { HtmlOverlayLayer } from \"../../../types/HtmlOverlayTypes\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\n\n/**\n * Props for BaseButton component\n */\nexport interface BaseButtonProps {\n readonly korean: string;\n readonly english: string;\n readonly onClick: () => void;\n readonly disabled?: boolean;\n readonly variant?: \"primary\" | \"secondary\" | \"danger\";\n readonly size?: \"sm\" | \"md\" | \"lg\";\n readonly position?: [number, number, number];\n readonly fullWidth?: boolean;\n readonly testId?: string;\n readonly isMobile?: boolean;\n /** Html overlay layer for z-index (default: 'hud') */\n readonly layer?: HtmlOverlayLayer;\n /** Whether button should occlude behind 3D objects (default: false) */\n readonly occlude?: boolean;\n /** ARIA label for accessibility (optional, defaults to korean text) */\n readonly ariaLabel?: string;\n /** ARIA described by ID for additional context */\n readonly ariaDescribedBy?: string;\n /** Auto-focus on mount (default: false) */\n readonly autoFocus?: boolean;\n}\n\n/**\n * BaseButton Component\n *\n * Enhanced Korean-themed button with common functionality extracted.\n * Uses useKoreanTheme hook for consistent styling.\n * Now includes Html overlay positioning helpers for proper z-index and performance.\n *\n * WCAG 2.1 AA Accessibility Features:\n * - Proper ARIA labels and semantic HTML\n * - Keyboard navigation support (Enter and Space keys)\n * - Visible focus indicators with high contrast\n * - Disabled state properly communicated\n * - Minimum 44x44px touch targets on mobile\n *\n * Optimized with React.memo for performance\n *\n * @example\n * ```tsx\n * <BaseButton\n * korean=\"공격\"\n * english=\"Attack\"\n * onClick={() => console.log(\"Attack\")}\n * variant=\"primary\"\n * size=\"md\"\n * layer=\"hud\"\n * ariaLabel=\"Attack button\"\n * />\n * ```\n */\nconst BaseButtonComponent: React.FC<BaseButtonProps> = ({\n korean,\n english,\n onClick,\n disabled = false,\n variant = \"primary\",\n size = \"md\",\n position = [0, 0, 0],\n fullWidth = false,\n testId,\n isMobile = false,\n layer = \"hud\",\n occlude = false,\n ariaLabel,\n ariaDescribedBy,\n autoFocus = false,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n const [isPressed, setIsPressed] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const buttonRef = useRef<HTMLButtonElement>(null);\n\n // Use Korean theme hook for consistent styling\n const { buttonVariant, buttonSize, fontFamily, accessibility } =\n useKoreanTheme({\n variant,\n size,\n disabled,\n isMobile,\n });\n\n // Auto-focus on mount if requested\n useEffect(() => {\n if (autoFocus && buttonRef.current) {\n buttonRef.current.focus();\n }\n }, [autoFocus]);\n\n // Track screen width for responsive distance factor updates on resize\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH,\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // Calculate optimal distance factor for button\n const distanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"button\", isMobile);\n }, [screenWidth, isMobile]);\n\n // Apply Html overlay styles with proper z-index\n const overlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(layer, true, distanceFactor, true, occlude);\n }, [layer, distanceFactor, occlude]);\n\n const handleClick = useCallback(() => {\n if (!disabled) {\n onClick();\n }\n }, [onClick, disabled]);\n\n const handleMouseEnter = useCallback(() => {\n if (!disabled) {\n setIsHovered(true);\n }\n }, [disabled]);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n setIsPressed(false);\n }, []);\n\n const handleMouseDown = useCallback(() => {\n if (!disabled) {\n setIsPressed(true);\n }\n }, [disabled]);\n\n const handleMouseUp = useCallback(() => {\n setIsPressed(false);\n }, []);\n\n const handleFocus = useCallback(() => {\n setIsFocused(true);\n }, []);\n\n const handleBlur = useCallback(() => {\n setIsFocused(false);\n }, []);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLButtonElement>) => {\n // Set pressed state for visual feedback on keyboard activation\n if ((e.key === \"Enter\" || e.key === \" \") && !disabled) {\n setIsPressed(true);\n }\n },\n [disabled],\n );\n\n const handleKeyUp = useCallback(\n (e: React.KeyboardEvent<HTMLButtonElement>) => {\n // Clear pressed state - native button behavior will trigger onClick\n if ((e.key === \"Enter\" || e.key === \" \") && !disabled) {\n setIsPressed(false);\n }\n },\n [disabled],\n );\n\n // Memoize button styles for performance\n const buttonStyle = useMemo<React.CSSProperties>(() => {\n let background = hexToRgbaString(buttonVariant.background, 0.9);\n\n if (isPressed) {\n background = buttonVariant.activeBg;\n } else if (isHovered) {\n background = buttonVariant.hoverBg;\n }\n\n return {\n background,\n border: `${buttonSize.borderWidth} solid ${hexToRgbaString(buttonVariant.border)}`,\n color: hexToRgbaString(buttonVariant.text),\n padding: buttonSize.padding,\n fontSize: buttonSize.fontSize,\n fontFamily: fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n opacity: disabled ? 0.5 : 1,\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n textAlign: \"center\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n width: fullWidth ? \"100%\" : \"auto\",\n // Ensure minimum touch target size on mobile (WCAG 2.1 AA)\n minWidth: isMobile ? accessibility.minTouchTarget : \"auto\",\n minHeight: isMobile ? accessibility.minTouchTarget : \"auto\",\n boxShadow:\n isHovered && !disabled\n ? `0 0 10px ${hexToRgbaString(buttonVariant.border, 0.5)}`\n : \"none\",\n transform: isPressed && !disabled ? \"scale(0.98)\" : \"scale(1)\",\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n // Apply GPU acceleration from overlay style\n WebkitTransform: overlayStyle.transform,\n zIndex: overlayStyle.zIndex,\n // WCAG 2.1 AA compliant focus indicator\n outline: isFocused && !disabled ? accessibility.focusOutline : \"none\",\n outlineOffset:\n isFocused && !disabled ? accessibility.focusOutlineOffset : \"0\",\n };\n }, [\n buttonVariant,\n buttonSize,\n fontFamily,\n disabled,\n fullWidth,\n isHovered,\n isPressed,\n isFocused,\n overlayStyle,\n isMobile,\n accessibility,\n ]);\n\n return (\n <Html\n position={position}\n center={overlayStyle.center}\n distanceFactor={overlayStyle.distanceFactor}\n occlude={overlayStyle.occlude}\n style={{ pointerEvents: overlayStyle.pointerEvents }}\n >\n <button\n ref={buttonRef}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n onKeyUp={handleKeyUp}\n disabled={disabled}\n type=\"button\"\n aria-label={ariaLabel ?? `${korean} ${english}`}\n aria-describedby={ariaDescribedBy}\n aria-disabled={disabled}\n style={buttonStyle}\n data-testid={testId ?? \"base-button\"}\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"2px\",\n }}\n >\n <span lang=\"ko\" style={{ fontSize: \"1em\" }}>\n {korean}\n </span>\n <span\n lang=\"en\"\n style={{\n fontSize: \"0.75em\",\n opacity: 0.8,\n fontStyle: \"italic\",\n }}\n >\n {english}\n </span>\n </div>\n </button>\n </Html>\n );\n};\n\n// Export memoized component for performance optimization\nexport const BaseButton = React.memo(BaseButtonComponent);\n\nBaseButton.displayName = \"BaseButton\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"BaseButton.js","names":[],"sources":["../../../../src/components/shared/base/BaseButton.tsx"],"sourcesContent":["/**\n * BaseButton - Enhanced button component with Korean theming\n *\n * Builds on existing KoreanButton with extracted common logic\n * Provides consistent button styling across the application\n *\n * Now with Html overlay positioning helpers for:\n * - Consistent z-index management\n * - Performance optimization with distanceFactor\n * - GPU acceleration\n *\n * @module components/base\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, {\n useCallback,\n useMemo,\n useState,\n useEffect,\n useRef,\n} from \"react\";\nimport { KOREAN_COLORS, UI_DIMENSIONS } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../utils/htmlOverlayHelpers\";\nimport type { HtmlOverlayLayer } from \"../../../types/HtmlOverlayTypes\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\n\n/**\n * Props for BaseButton component\n */\nexport interface BaseButtonProps {\n readonly korean: string;\n readonly english: string;\n readonly onClick: () => void;\n readonly disabled?: boolean;\n readonly variant?: \"primary\" | \"secondary\" | \"danger\";\n readonly size?: \"sm\" | \"md\" | \"lg\";\n readonly position?: [number, number, number];\n readonly fullWidth?: boolean;\n readonly testId?: string;\n readonly isMobile?: boolean;\n /** Html overlay layer for z-index (default: 'hud') */\n readonly layer?: HtmlOverlayLayer;\n /** Whether button should occlude behind 3D objects (default: false) */\n readonly occlude?: boolean;\n /** ARIA label for accessibility (optional, defaults to korean text) */\n readonly ariaLabel?: string;\n /** ARIA described by ID for additional context */\n readonly ariaDescribedBy?: string;\n /** Auto-focus on mount (default: false) */\n readonly autoFocus?: boolean;\n}\n\n/**\n * BaseButton Component\n *\n * Enhanced Korean-themed button with common functionality extracted.\n * Uses useKoreanTheme hook for consistent styling.\n * Now includes Html overlay positioning helpers for proper z-index and performance.\n *\n * WCAG 2.1 AA Accessibility Features:\n * - Proper ARIA labels and semantic HTML\n * - Keyboard navigation support (Enter and Space keys)\n * - Visible focus indicators with high contrast\n * - Disabled state properly communicated\n * - Minimum 44x44px touch targets on mobile\n *\n * Optimized with React.memo for performance\n *\n * @example\n * ```tsx\n * <BaseButton\n * korean=\"공격\"\n * english=\"Attack\"\n * onClick={() => console.log(\"Attack\")}\n * variant=\"primary\"\n * size=\"md\"\n * layer=\"hud\"\n * ariaLabel=\"Attack button\"\n * />\n * ```\n */\nconst BaseButtonComponent: React.FC<BaseButtonProps> = ({\n korean,\n english,\n onClick,\n disabled = false,\n variant = \"primary\",\n size = \"md\",\n position = [0, 0, 0],\n fullWidth = false,\n testId,\n isMobile = false,\n layer = \"hud\",\n occlude = false,\n ariaLabel,\n ariaDescribedBy,\n autoFocus = false,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n const [isPressed, setIsPressed] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const buttonRef = useRef<HTMLButtonElement>(null);\n\n // Use Korean theme hook for consistent styling\n const { buttonVariant, buttonSize, fontFamily, accessibility } =\n useKoreanTheme({\n variant,\n size,\n disabled,\n isMobile,\n });\n\n // Auto-focus on mount if requested\n useEffect(() => {\n if (autoFocus && buttonRef.current) {\n buttonRef.current.focus();\n }\n }, [autoFocus]);\n\n // Track screen width for responsive distance factor updates on resize\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH,\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // Calculate optimal distance factor for button\n const distanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"button\", isMobile);\n }, [screenWidth, isMobile]);\n\n // Apply Html overlay styles with proper z-index\n const overlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(layer, true, distanceFactor, true, occlude);\n }, [layer, distanceFactor, occlude]);\n\n const handleClick = useCallback(() => {\n if (!disabled) {\n onClick();\n }\n }, [onClick, disabled]);\n\n const handleMouseEnter = useCallback(() => {\n if (!disabled) {\n setIsHovered(true);\n }\n }, [disabled]);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n setIsPressed(false);\n }, []);\n\n const handleMouseDown = useCallback(() => {\n if (!disabled) {\n setIsPressed(true);\n }\n }, [disabled]);\n\n const handleMouseUp = useCallback(() => {\n setIsPressed(false);\n }, []);\n\n const handleFocus = useCallback(() => {\n setIsFocused(true);\n }, []);\n\n const handleBlur = useCallback(() => {\n setIsFocused(false);\n }, []);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLButtonElement>) => {\n // Set pressed state for visual feedback on keyboard activation\n if ((e.key === \"Enter\" || e.key === \" \") && !disabled) {\n setIsPressed(true);\n }\n },\n [disabled],\n );\n\n const handleKeyUp = useCallback(\n (e: React.KeyboardEvent<HTMLButtonElement>) => {\n // Clear pressed state - native button behavior will trigger onClick\n if ((e.key === \"Enter\" || e.key === \" \") && !disabled) {\n setIsPressed(false);\n }\n },\n [disabled],\n );\n\n // Memoize button styles for performance\n const buttonStyle = useMemo<React.CSSProperties>(() => {\n let background = hexToRgbaString(buttonVariant.background, 0.9);\n\n if (isPressed) {\n background = buttonVariant.activeBg;\n } else if (isHovered) {\n background = buttonVariant.hoverBg;\n }\n\n return {\n background,\n border: `${buttonSize.borderWidth} solid ${hexToRgbaString(buttonVariant.border)}`,\n color: hexToRgbaString(buttonVariant.text),\n padding: buttonSize.padding,\n fontSize: buttonSize.fontSize,\n fontFamily: fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n opacity: disabled ? 0.5 : 1,\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n textAlign: \"center\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n width: fullWidth ? \"100%\" : \"auto\",\n // Ensure minimum touch target size on mobile (WCAG 2.1 AA)\n minWidth: isMobile ? accessibility.minTouchTarget : \"auto\",\n minHeight: isMobile ? accessibility.minTouchTarget : \"auto\",\n boxShadow:\n isHovered && !disabled\n ? `0 0 10px ${hexToRgbaString(buttonVariant.border, 0.5)}`\n : \"none\",\n transform: isPressed && !disabled ? \"scale(0.98)\" : \"scale(1)\",\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n // Apply GPU acceleration from overlay style\n WebkitTransform: overlayStyle.transform,\n zIndex: overlayStyle.zIndex,\n // WCAG 2.1 AA compliant focus indicator\n outline: isFocused && !disabled ? accessibility.focusOutline : \"none\",\n outlineOffset:\n isFocused && !disabled ? accessibility.focusOutlineOffset : \"0\",\n };\n }, [\n buttonVariant,\n buttonSize,\n fontFamily,\n disabled,\n fullWidth,\n isHovered,\n isPressed,\n isFocused,\n overlayStyle,\n isMobile,\n accessibility,\n ]);\n\n return (\n <Html\n position={position}\n center={overlayStyle.center}\n distanceFactor={overlayStyle.distanceFactor}\n occlude={overlayStyle.occlude}\n style={{ pointerEvents: overlayStyle.pointerEvents }}\n >\n <button\n ref={buttonRef}\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onFocus={handleFocus}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n onKeyUp={handleKeyUp}\n disabled={disabled}\n type=\"button\"\n aria-label={ariaLabel ?? `${korean} ${english}`}\n aria-describedby={ariaDescribedBy}\n aria-disabled={disabled}\n style={buttonStyle}\n data-testid={testId ?? \"base-button\"}\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"2px\",\n }}\n >\n <span lang=\"ko\" style={{ fontSize: \"1em\" }}>\n {korean}\n </span>\n <span\n lang=\"en\"\n style={{\n fontSize: \"0.75em\",\n opacity: 0.8,\n fontStyle: \"italic\",\n }}\n >\n {english}\n </span>\n </div>\n </button>\n </Html>\n );\n};\n\n// Export memoized component for performance optimization\nexport const BaseButton = React.memo(BaseButtonComponent);\n\nBaseButton.displayName = \"BaseButton\";\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BaseButtonOverlayHtml.js","names":[],"sources":["../../../../src/components/shared/base/BaseButtonOverlayHtml.tsx"],"sourcesContent":["/**\n * BaseButtonOverlayHtml - HTML button component with Korean theming (non-Three.js)\n * \n * A version of BaseButton that doesn't require Three.js/Canvas context\n * Can be used in regular DOM components\n * \n * @module components/base\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { getKoreanButtonWithGlow } from \"../../../utils/koreanThemeHelpers\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\nimport { UIHaptics } from \"../../../utils/hapticFeedback\";\n\n/**\n * Props for BaseButtonOverlayHtml component\n */\nexport interface BaseButtonOverlayHtmlProps {\n readonly korean: string;\n readonly english: string;\n readonly onClick: () => void;\n readonly onMouseEnter?: () => void;\n readonly disabled?: boolean;\n readonly variant?: \"primary\" | \"secondary\" | \"danger\";\n readonly size?: \"sm\" | \"md\" | \"lg\";\n readonly fullWidth?: boolean;\n readonly testId?: string;\n readonly isMobile?: boolean;\n readonly className?: string;\n readonly style?: React.CSSProperties;\n readonly autoFocus?: boolean;\n readonly ariaLabel?: string;\n readonly ariaCurrent?: React.AriaAttributes[\"aria-current\"];\n}\n\n/**\n * BaseButtonOverlayHtml Component\n * \n * HTML button with Korean theming (no Three.js dependency).\n * Uses useKoreanTheme hook for consistent styling.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when props haven't changed\n * - Event handlers already use useCallback internally\n * \n * @example\n * ```tsx\n * <BaseButtonOverlayHtml\n * korean=\"확인\"\n * english=\"Confirm\"\n * onClick={() => handleConfirm()}\n * variant=\"primary\"\n * size=\"md\"\n * />\n * ```\n */\nexport const BaseButtonOverlayHtml = React.memo<BaseButtonOverlayHtmlProps>(\n ({\n korean,\n english,\n onClick,\n onMouseEnter,\n disabled = false,\n variant = \"primary\",\n size = \"md\",\n fullWidth = false,\n testId,\n isMobile = false,\n className,\n style: customStyle,\n autoFocus = false,\n ariaLabel,\n ariaCurrent,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n const [isPressed, setIsPressed] = useState(false);\n\n // Use Korean theme hook for size and font info only\n const { buttonSize } = useKoreanTheme({\n variant,\n size,\n disabled,\n isMobile,\n });\n\n const handleClick = useCallback(() => {\n if (!disabled) {\n UIHaptics.buttonTap(); // Add haptic feedback\n onClick();\n }\n }, [onClick, disabled]);\n\n const handleMouseEnterInternal = useCallback(() => {\n if (!disabled) {\n UIHaptics.menuHover(); // Add haptic feedback\n setIsHovered(true);\n onMouseEnter?.();\n }\n }, [disabled, onMouseEnter]);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n setIsPressed(false);\n }, []);\n\n const handleMouseDown = useCallback(() => {\n if (!disabled) {\n setIsPressed(true);\n }\n }, [disabled]);\n\n const handleMouseUp = useCallback(() => {\n setIsPressed(false);\n }, []);\n\n // Memoize button styles for performance - now using enhanced visual effects\n const buttonStyle = useMemo<React.CSSProperties>(() => {\n // Get enhanced button styles with neon glow\n const enhancedStyles = getKoreanButtonWithGlow({\n variant,\n isHovered: isHovered && !disabled,\n isPressed: isPressed && !disabled,\n isFocused: false,\n glowIntensity: \"medium\",\n hoverAnimation: \"combined\",\n });\n\n // Merge with size-specific styles\n // Note: fontSize from enhancedStyles is intentionally overridden by buttonSize.fontSize\n // to ensure consistent sizing across button size variants (small/medium/large).\n // The Korean font optimization (anti-aliasing, letter-spacing) is preserved.\n return {\n ...enhancedStyles,\n padding: buttonSize.padding,\n fontSize: buttonSize.fontSize, // Override for size consistency\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n opacity: disabled ? 0.5 : 1,\n borderRadius: \"4px\",\n textAlign: \"center\" as const,\n userSelect: \"none\" as const,\n WebkitUserSelect: \"none\" as const,\n width: fullWidth ? \"100%\" : \"auto\",\n ...customStyle,\n };\n }, [\n variant,\n buttonSize,\n disabled,\n fullWidth,\n isHovered,\n isPressed,\n customStyle,\n ]);\n\n return (\n <button\n onClick={handleClick}\n onMouseEnter={handleMouseEnterInternal}\n onMouseLeave={handleMouseLeave}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n disabled={disabled}\n style={buttonStyle}\n className={className}\n data-testid={testId ?? \"base-button-html\"}\n autoFocus={autoFocus}\n aria-label={ariaLabel ?? `${korean} | ${english}`}\n aria-current={ariaCurrent}\n >\n <div style={{ \n display: \"flex\", \n flexDirection: \"column\", \n alignItems: \"center\",\n gap: \"2px\"\n }}>\n <span style={{ fontSize: \"1em\" }}>{korean}</span>\n <span style={{ \n fontSize: \"0.75em\", \n opacity: 0.8,\n fontStyle: \"italic\"\n }}>\n {english}\n </span>\n </div>\n </button>\n );\n});\n\nBaseButtonOverlayHtml.displayName = \"BaseButtonOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,IAAa,wBAAwB,MAAM,MACxC,EACC,QACA,SACA,SACA,cACA,WAAW,OACb,UAAU,WACV,OAAO,MACP,YAAY,OACZ,QACA,WAAW,OACX,WACA,OAAO,aACP,YAAY,OACZ,WACA,kBACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CACjD,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CAGjD,MAAM,EAAE,eAAe,eAAe;EACpC;EACA;EACA;EACA;EACD,CAAC;AAuEF,QACE,oBAAC,UAAD;EACE,SAvEgB,kBAAkB;AACpC,OAAI,CAAC,UAAU;AACb,cAAU,WAAW;AACrB,aAAS;;KAEV,CAAC,SAAS,SAAS,CAkET;EACT,cAjE6B,kBAAkB;AACjD,OAAI,CAAC,UAAU;AACb,cAAU,WAAW;AACrB,iBAAa,KAAK;AAClB,oBAAgB;;KAEjB,CAAC,UAAU,aAAa,CA2DT;EACd,cA1DqB,kBAAkB;AACzC,gBAAa,MAAM;AACnB,gBAAa,MAAM;KAClB,EAAE,CAuDa;EACd,aAtDoB,kBAAkB;AACxC,OAAI,CAAC,SACH,cAAa,KAAK;KAEnB,CAAC,SAAS,CAkDI;EACb,WAjDkB,kBAAkB;AACtC,gBAAa,MAAM;KAClB,EAAE,CA+CU;EACD;EACV,OA9CgB,cAAmC;AAerD,UAAO;IACL,GAdqB,wBAAwB;KAC7C;KACA,WAAW,aAAa,CAAC;KACzB,WAAW,aAAa,CAAC;KACzB,WAAW;KACX,eAAe;KACf,gBAAgB;KACjB,CAOI;IACH,SAAS,WAAW;IACpB,UAAU,WAAW;IACrB,QAAQ,WAAW,gBAAgB;IACnC,SAAS,WAAW,KAAM;IAC1B,cAAc;IACd,WAAW;IACX,YAAY;IACZ,kBAAkB;IAClB,OAAO,YAAY,SAAS;IAC5B,GAAG;IACJ;KACA;GACD;GACA;GACA;GACA;GACA;GACA;GACA;GACD,CAUU;EACI;EACX,eAAa,UAAU;EACZ;EACX,cAAY,aAAa,GAAG,OAAO,KAAK;EACxC,gBAAc;YAEd,qBAAC,OAAD;GAAK,OAAO;IACV,SAAS;IACT,eAAe;IACf,YAAY;IACZ,KAAK;IACN;aALD,CAME,oBAAC,QAAD;IAAM,OAAO,EAAE,UAAU,OAAO;cAAG;IAAc,CAAA,EACjD,oBAAC,QAAD;IAAM,OAAO;KACX,UAAU;KACV,SAAS;KACT,WAAW;KACZ;cACE;IACI,CAAA,CACH;;EACC,CAAA;EAEX;AAEF,sBAAsB,cAAc"}
1
+ {"version":3,"file":"BaseButtonOverlayHtml.js","names":[],"sources":["../../../../src/components/shared/base/BaseButtonOverlayHtml.tsx"],"sourcesContent":["/**\n * BaseButtonOverlayHtml - HTML button component with Korean theming (non-Three.js)\n * \n * A version of BaseButton that doesn't require Three.js/Canvas context\n * Can be used in regular DOM components\n * \n * @module components/base\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { getKoreanButtonWithGlow } from \"../../../utils/koreanThemeHelpers\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\nimport { UIHaptics } from \"../../../utils/hapticFeedback\";\n\n/**\n * Props for BaseButtonOverlayHtml component\n */\nexport interface BaseButtonOverlayHtmlProps {\n readonly korean: string;\n readonly english: string;\n readonly onClick: () => void;\n readonly onMouseEnter?: () => void;\n readonly disabled?: boolean;\n readonly variant?: \"primary\" | \"secondary\" | \"danger\";\n readonly size?: \"sm\" | \"md\" | \"lg\";\n readonly fullWidth?: boolean;\n readonly testId?: string;\n readonly isMobile?: boolean;\n readonly className?: string;\n readonly style?: React.CSSProperties;\n readonly autoFocus?: boolean;\n readonly ariaLabel?: string;\n readonly ariaCurrent?: React.AriaAttributes[\"aria-current\"];\n}\n\n/**\n * BaseButtonOverlayHtml Component\n * \n * HTML button with Korean theming (no Three.js dependency).\n * Uses useKoreanTheme hook for consistent styling.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when props haven't changed\n * - Event handlers already use useCallback internally\n * \n * @example\n * ```tsx\n * <BaseButtonOverlayHtml\n * korean=\"확인\"\n * english=\"Confirm\"\n * onClick={() => handleConfirm()}\n * variant=\"primary\"\n * size=\"md\"\n * />\n * ```\n */\nexport const BaseButtonOverlayHtml = React.memo<BaseButtonOverlayHtmlProps>(\n ({\n korean,\n english,\n onClick,\n onMouseEnter,\n disabled = false,\n variant = \"primary\",\n size = \"md\",\n fullWidth = false,\n testId,\n isMobile = false,\n className,\n style: customStyle,\n autoFocus = false,\n ariaLabel,\n ariaCurrent,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n const [isPressed, setIsPressed] = useState(false);\n\n // Use Korean theme hook for size and font info only\n const { buttonSize } = useKoreanTheme({\n variant,\n size,\n disabled,\n isMobile,\n });\n\n const handleClick = useCallback(() => {\n if (!disabled) {\n UIHaptics.buttonTap(); // Add haptic feedback\n onClick();\n }\n }, [onClick, disabled]);\n\n const handleMouseEnterInternal = useCallback(() => {\n if (!disabled) {\n UIHaptics.menuHover(); // Add haptic feedback\n setIsHovered(true);\n onMouseEnter?.();\n }\n }, [disabled, onMouseEnter]);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n setIsPressed(false);\n }, []);\n\n const handleMouseDown = useCallback(() => {\n if (!disabled) {\n setIsPressed(true);\n }\n }, [disabled]);\n\n const handleMouseUp = useCallback(() => {\n setIsPressed(false);\n }, []);\n\n // Memoize button styles for performance - now using enhanced visual effects\n const buttonStyle = useMemo<React.CSSProperties>(() => {\n // Get enhanced button styles with neon glow\n const enhancedStyles = getKoreanButtonWithGlow({\n variant,\n isHovered: isHovered && !disabled,\n isPressed: isPressed && !disabled,\n isFocused: false,\n glowIntensity: \"medium\",\n hoverAnimation: \"combined\",\n });\n\n // Merge with size-specific styles\n // Note: fontSize from enhancedStyles is intentionally overridden by buttonSize.fontSize\n // to ensure consistent sizing across button size variants (small/medium/large).\n // The Korean font optimization (anti-aliasing, letter-spacing) is preserved.\n return {\n ...enhancedStyles,\n padding: buttonSize.padding,\n fontSize: buttonSize.fontSize, // Override for size consistency\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n opacity: disabled ? 0.5 : 1,\n borderRadius: \"4px\",\n textAlign: \"center\" as const,\n userSelect: \"none\" as const,\n WebkitUserSelect: \"none\" as const,\n width: fullWidth ? \"100%\" : \"auto\",\n ...customStyle,\n };\n }, [\n variant,\n buttonSize,\n disabled,\n fullWidth,\n isHovered,\n isPressed,\n customStyle,\n ]);\n\n return (\n <button\n onClick={handleClick}\n onMouseEnter={handleMouseEnterInternal}\n onMouseLeave={handleMouseLeave}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n disabled={disabled}\n style={buttonStyle}\n className={className}\n data-testid={testId ?? \"base-button-html\"}\n autoFocus={autoFocus}\n aria-label={ariaLabel ?? `${korean} | ${english}`}\n aria-current={ariaCurrent}\n >\n <div style={{ \n display: \"flex\", \n flexDirection: \"column\", \n alignItems: \"center\",\n gap: \"2px\"\n }}>\n <span style={{ fontSize: \"1em\" }}>{korean}</span>\n <span style={{ \n fontSize: \"0.75em\", \n opacity: 0.8,\n fontStyle: \"italic\"\n }}>\n {english}\n </span>\n </div>\n </button>\n );\n});\n\nBaseButtonOverlayHtml.displayName = \"BaseButtonOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,IAAa,wBAAwB,MAAM,MACxC,EACC,QACA,SACA,SACA,cACA,WAAW,OACb,UAAU,WACV,OAAO,MACP,YAAY,OACZ,QACA,WAAW,OACX,WACA,OAAO,aACP,YAAY,OACZ,WACA,kBACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CACjD,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CAGjD,MAAM,EAAE,eAAe,eAAe;EACpC;EACA;EACA;EACA;EACD,CAAC;CAuEF,OACE,oBAAC,UAAD;EACE,SAvEgB,kBAAkB;GACpC,IAAI,CAAC,UAAU;IACb,UAAU,WAAW;IACrB,SAAS;;KAEV,CAAC,SAAS,SAAS,CAkET;EACT,cAjE6B,kBAAkB;GACjD,IAAI,CAAC,UAAU;IACb,UAAU,WAAW;IACrB,aAAa,KAAK;IAClB,gBAAgB;;KAEjB,CAAC,UAAU,aAAa,CA2DT;EACd,cA1DqB,kBAAkB;GACzC,aAAa,MAAM;GACnB,aAAa,MAAM;KAClB,EAAE,CAuDa;EACd,aAtDoB,kBAAkB;GACxC,IAAI,CAAC,UACH,aAAa,KAAK;KAEnB,CAAC,SAAS,CAkDI;EACb,WAjDkB,kBAAkB;GACtC,aAAa,MAAM;KAClB,EAAE,CA+CU;EACD;EACV,OA9CgB,cAAmC;GAerD,OAAO;IACL,GAdqB,wBAAwB;KAC7C;KACA,WAAW,aAAa,CAAC;KACzB,WAAW,aAAa,CAAC;KACzB,WAAW;KACX,eAAe;KACf,gBAAgB;KACjB,CAOI;IACH,SAAS,WAAW;IACpB,UAAU,WAAW;IACrB,QAAQ,WAAW,gBAAgB;IACnC,SAAS,WAAW,KAAM;IAC1B,cAAc;IACd,WAAW;IACX,YAAY;IACZ,kBAAkB;IAClB,OAAO,YAAY,SAAS;IAC5B,GAAG;IACJ;KACA;GACD;GACA;GACA;GACA;GACA;GACA;GACA;GACD,CAUU;EACI;EACX,eAAa,UAAU;EACZ;EACX,cAAY,aAAa,GAAG,OAAO,KAAK;EACxC,gBAAc;YAEd,qBAAC,OAAD;GAAK,OAAO;IACV,SAAS;IACT,eAAe;IACf,YAAY;IACZ,KAAK;IACN;aALD,CAME,oBAAC,QAAD;IAAM,OAAO,EAAE,UAAU,OAAO;cAAG;IAAc,CAAA,EACjD,oBAAC,QAAD;IAAM,OAAO;KACX,UAAU;KACV,SAAS;KACT,WAAW;KACZ;cACE;IACI,CAAA,CACH;;EACC,CAAA;EAEX;AAEF,sBAAsB,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"BasePanel.js","names":[],"sources":["../../../../src/components/shared/base/BasePanel.tsx"],"sourcesContent":["/**\n * BasePanel - Enhanced panel component with Korean theming\n * \n * Builds on existing KoreanPanel with extracted common logic\n * Provides consistent panel styling across the application\n * \n * @module components/base\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\n\n/**\n * Props for BasePanel component\n */\nexport interface BasePanelProps {\n readonly children: React.ReactNode;\n readonly position?: [number, number, number];\n readonly width?: number | string;\n readonly height?: number | string;\n readonly padding?: number;\n readonly variant?: \"default\" | \"bordered\" | \"elevated\";\n readonly testId?: string;\n readonly isMobile?: boolean;\n /** ARIA role for semantic HTML (default: \"region\") */\n readonly ariaRole?: \"region\" | \"article\" | \"complementary\" | \"navigation\" | \"main\";\n /** ARIA label for accessibility */\n readonly ariaLabel?: string;\n /** ARIA described by ID for additional context */\n readonly ariaDescribedBy?: string;\n}\n\n/**\n * BasePanel Component\n * \n * Enhanced Korean-themed panel with common functionality extracted.\n * Uses useKoreanTheme hook for consistent styling.\n * \n * WCAG 2.1 AA Accessibility Features:\n * - Proper ARIA roles for semantic HTML\n * - ARIA labels for screen reader context\n * - Responsive padding for touch targets\n * \n * Optimized with React.memo for performance\n * \n * @example\n * ```tsx\n * <BasePanel \n * variant=\"bordered\" \n * padding={20}\n * ariaRole=\"region\"\n * ariaLabel=\"Combat statistics panel\"\n * >\n * <h1>Panel Content</h1>\n * </BasePanel>\n * ```\n */\nconst BasePanelComponent: React.FC<BasePanelProps> = ({\n children,\n position = [0, 0, 0],\n width = \"auto\",\n height = \"auto\",\n padding = 16,\n variant = \"default\",\n testId,\n isMobile = false,\n ariaRole = \"region\",\n ariaLabel,\n ariaDescribedBy,\n}) => {\n // Use Korean theme hook for consistent styling\n const { panelVariant, fontFamily } = useKoreanTheme({\n variant,\n isMobile,\n });\n\n // Memoize panel styles for performance\n const panelStyle = useMemo<React.CSSProperties>(() => {\n return {\n width,\n height,\n padding: `${padding}px`,\n borderRadius: \"8px\",\n fontFamily: fontFamily.KOREAN,\n background: panelVariant.background,\n border: panelVariant.border,\n boxShadow: panelVariant.boxShadow,\n };\n }, [width, height, padding, panelVariant, fontFamily]);\n\n return (\n <Html position={position} center>\n <div \n style={panelStyle} \n data-testid={testId ?? \"base-panel\"}\n role={ariaRole}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedBy}\n >\n {children}\n </div>\n </Html>\n );\n};\n\n// Export memoized component for performance optimization\nexport const BasePanel = React.memo(BasePanelComponent);\n\nBasePanel.displayName = \"BasePanel\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,IAAM,sBAAgD,EACpD,UACA,WAAW;CAAC;CAAG;CAAG;CAAE,EACpB,QAAQ,QACR,SAAS,QACT,UAAU,IACV,UAAU,WACV,QACA,WAAW,OACX,WAAW,UACX,WACA,sBACI;CAEJ,MAAM,EAAE,cAAc,eAAe,eAAe;EAClD;EACA;EACD,CAAC;AAgBF,QACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;YACxB,oBAAC,OAAD;GACE,OAhBa,cAAmC;AACpD,WAAO;KACL;KACA;KACA,SAAS,GAAG,QAAQ;KACpB,cAAc;KACd,YAAY,WAAW;KACvB,YAAY,aAAa;KACzB,QAAQ,aAAa;KACrB,WAAW,aAAa;KACzB;MACA;IAAC;IAAO;IAAQ;IAAS;IAAc;IAAW,CAKxC;GACP,eAAa,UAAU;GACvB,MAAM;GACN,cAAY;GACZ,oBAAkB;GAEjB;GACG,CAAA;EACD,CAAA;;AAKX,IAAa,YAAY,MAAM,KAAK,mBAAmB;AAEvD,UAAU,cAAc"}
1
+ {"version":3,"file":"BasePanel.js","names":[],"sources":["../../../../src/components/shared/base/BasePanel.tsx"],"sourcesContent":["/**\n * BasePanel - Enhanced panel component with Korean theming\n * \n * Builds on existing KoreanPanel with extracted common logic\n * Provides consistent panel styling across the application\n * \n * @module components/base\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\n\n/**\n * Props for BasePanel component\n */\nexport interface BasePanelProps {\n readonly children: React.ReactNode;\n readonly position?: [number, number, number];\n readonly width?: number | string;\n readonly height?: number | string;\n readonly padding?: number;\n readonly variant?: \"default\" | \"bordered\" | \"elevated\";\n readonly testId?: string;\n readonly isMobile?: boolean;\n /** ARIA role for semantic HTML (default: \"region\") */\n readonly ariaRole?: \"region\" | \"article\" | \"complementary\" | \"navigation\" | \"main\";\n /** ARIA label for accessibility */\n readonly ariaLabel?: string;\n /** ARIA described by ID for additional context */\n readonly ariaDescribedBy?: string;\n}\n\n/**\n * BasePanel Component\n * \n * Enhanced Korean-themed panel with common functionality extracted.\n * Uses useKoreanTheme hook for consistent styling.\n * \n * WCAG 2.1 AA Accessibility Features:\n * - Proper ARIA roles for semantic HTML\n * - ARIA labels for screen reader context\n * - Responsive padding for touch targets\n * \n * Optimized with React.memo for performance\n * \n * @example\n * ```tsx\n * <BasePanel \n * variant=\"bordered\" \n * padding={20}\n * ariaRole=\"region\"\n * ariaLabel=\"Combat statistics panel\"\n * >\n * <h1>Panel Content</h1>\n * </BasePanel>\n * ```\n */\nconst BasePanelComponent: React.FC<BasePanelProps> = ({\n children,\n position = [0, 0, 0],\n width = \"auto\",\n height = \"auto\",\n padding = 16,\n variant = \"default\",\n testId,\n isMobile = false,\n ariaRole = \"region\",\n ariaLabel,\n ariaDescribedBy,\n}) => {\n // Use Korean theme hook for consistent styling\n const { panelVariant, fontFamily } = useKoreanTheme({\n variant,\n isMobile,\n });\n\n // Memoize panel styles for performance\n const panelStyle = useMemo<React.CSSProperties>(() => {\n return {\n width,\n height,\n padding: `${padding}px`,\n borderRadius: \"8px\",\n fontFamily: fontFamily.KOREAN,\n background: panelVariant.background,\n border: panelVariant.border,\n boxShadow: panelVariant.boxShadow,\n };\n }, [width, height, padding, panelVariant, fontFamily]);\n\n return (\n <Html position={position} center>\n <div \n style={panelStyle} \n data-testid={testId ?? \"base-panel\"}\n role={ariaRole}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedBy}\n >\n {children}\n </div>\n </Html>\n );\n};\n\n// Export memoized component for performance optimization\nexport const BasePanel = React.memo(BasePanelComponent);\n\nBasePanel.displayName = \"BasePanel\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,IAAM,sBAAgD,EACpD,UACA,WAAW;CAAC;CAAG;CAAG;CAAE,EACpB,QAAQ,QACR,SAAS,QACT,UAAU,IACV,UAAU,WACV,QACA,WAAW,OACX,WAAW,UACX,WACA,sBACI;CAEJ,MAAM,EAAE,cAAc,eAAe,eAAe;EAClD;EACA;EACD,CAAC;CAgBF,OACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;YACxB,oBAAC,OAAD;GACE,OAhBa,cAAmC;IACpD,OAAO;KACL;KACA;KACA,SAAS,GAAG,QAAQ;KACpB,cAAc;KACd,YAAY,WAAW;KACvB,YAAY,aAAa;KACzB,QAAQ,aAAa;KACrB,WAAW,aAAa;KACzB;MACA;IAAC;IAAO;IAAQ;IAAS;IAAc;IAAW,CAKxC;GACP,eAAa,UAAU;GACvB,MAAM;GACN,cAAY;GACZ,oBAAkB;GAEjB;GACG,CAAA;EACD,CAAA;;AAKX,IAAa,YAAY,MAAM,KAAK,mBAAmB;AAEvD,UAAU,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"BaseText.js","names":[],"sources":["../../../../src/components/shared/base/BaseText.tsx"],"sourcesContent":["/**\n * BaseText - Enhanced bilingual text component with Korean theming\n *\n * Builds on existing KoreanText with extracted common logic\n * Provides consistent text styling across the application\n *\n * Now with Html overlay positioning helpers for:\n * - Consistent z-index management\n * - Performance optimization with distanceFactor\n * - GPU acceleration\n *\n * @module components/base\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo, useState, useEffect } from \"react\";\nimport { KOREAN_COLORS, UI_DIMENSIONS } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../utils/htmlOverlayHelpers\";\nimport type { HtmlOverlayLayer } from \"../../../types/HtmlOverlayTypes\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\n\n/**\n * Props for BaseText component\n */\nexport interface BaseTextProps {\n readonly korean: string;\n readonly english: string;\n readonly position?: [number, number, number];\n readonly size?: \"small\" | \"medium\" | \"large\" | \"xlarge\";\n readonly color?: number;\n readonly align?: \"left\" | \"center\" | \"right\";\n readonly weight?: \"normal\" | \"bold\";\n readonly layout?: \"vertical\" | \"horizontal\";\n readonly testId?: string;\n readonly isMobile?: boolean;\n /** Html overlay layer for z-index (default: 'hud') */\n readonly layer?: HtmlOverlayLayer;\n /** Whether text should occlude behind 3D objects (default: false) */\n readonly occlude?: boolean;\n /** ARIA label for accessibility (optional) */\n readonly ariaLabel?: string;\n /** ARIA live region for dynamic content (optional) */\n readonly ariaLive?: \"polite\" | \"assertive\" | \"off\";\n}\n\n/**\n * BaseText Component\n *\n * Enhanced bilingual text component with Korean cyberpunk styling.\n * Uses useKoreanTheme hook for consistent text sizing and styling.\n * Now includes Html overlay positioning helpers for proper z-index and performance.\n *\n * Korean Typography Features:\n * - Optimized line height (1.6) for Korean character readability\n * - Letter spacing (-0.01em) for tighter Korean text\n * - Word break (keep-all) to prevent breaking Korean words mid-syllable\n * - Proper language attributes (lang=\"ko\" / lang=\"en\")\n *\n * WCAG 2.1 AA Accessibility Features:\n * - Proper language attributes for screen readers\n * - Optional ARIA labels for additional context\n * - ARIA live regions for dynamic content\n *\n * Optimized with React.memo for performance\n *\n * @example\n * ```tsx\n * <BaseText\n * korean=\"공격\"\n * english=\"Attack\"\n * size=\"large\"\n * layout=\"vertical\"\n * layer=\"hud\"\n * ariaLive=\"polite\"\n * />\n * ```\n */\nconst BaseTextComponent: React.FC<BaseTextProps> = ({\n korean,\n english,\n position = [0, 0, 0],\n size = \"medium\",\n color = KOREAN_COLORS.TEXT_PRIMARY,\n align = \"center\",\n weight = \"normal\",\n layout = \"vertical\",\n testId,\n isMobile = false,\n layer = \"hud\",\n occlude = false,\n ariaLabel,\n ariaLive = \"off\",\n}) => {\n // Use Korean theme hook for consistent text sizing\n const { textSize, koreanTypography } = useKoreanTheme({\n size,\n isMobile,\n });\n\n // Track screen width for responsive distance factor updates on resize\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH,\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // Calculate optimal distance factor for text\n const distanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"text\", isMobile);\n }, [screenWidth, isMobile]);\n\n // Apply Html overlay styles with proper z-index\n const overlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(layer, false, distanceFactor, true, occlude);\n }, [layer, distanceFactor, occlude]);\n\n // Memoize text styles for performance with Korean typography optimization\n const textStyle = useMemo<React.CSSProperties>(\n () => ({\n color: hexToRgbaString(color),\n fontFamily: koreanTypography.fontFamily,\n textAlign: align,\n fontWeight: weight === \"bold\" ? \"bold\" : \"normal\",\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n // Korean typography optimization\n lineHeight: koreanTypography.lineHeight,\n letterSpacing: koreanTypography.letterSpacing,\n wordBreak: koreanTypography.wordBreak,\n wordWrap: koreanTypography.wordWrap,\n }),\n [color, align, weight, koreanTypography],\n );\n\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"flex\",\n flexDirection: layout === \"vertical\" ? \"column\" : \"row\",\n alignItems: \"center\",\n gap: layout === \"vertical\" ? \"4px\" : \"8px\",\n // Apply GPU acceleration from overlay style\n transform: overlayStyle.transform,\n pointerEvents: overlayStyle.pointerEvents,\n zIndex: overlayStyle.zIndex,\n }),\n [layout, overlayStyle],\n );\n\n const koreanStyle = useMemo<React.CSSProperties>(\n () => ({\n ...textStyle,\n fontSize: textSize.korean,\n }),\n [textStyle, textSize.korean],\n );\n\n const englishStyle = useMemo<React.CSSProperties>(\n () => ({\n ...textStyle,\n fontSize: textSize.english,\n opacity: 0.8,\n fontStyle: \"italic\",\n }),\n [textStyle, textSize.english],\n );\n\n return (\n <Html\n position={position}\n center={overlayStyle.center}\n distanceFactor={overlayStyle.distanceFactor}\n occlude={overlayStyle.occlude}\n style={{ pointerEvents: overlayStyle.pointerEvents }}\n >\n <div\n style={containerStyle}\n data-testid={testId ?? \"base-text\"}\n aria-label={ariaLabel}\n aria-live={ariaLive}\n >\n <span lang=\"ko\" style={koreanStyle}>\n {korean}\n </span>\n {layout === \"vertical\" && (\n <span lang=\"en\" style={englishStyle}>\n {english}\n </span>\n )}\n {layout === \"horizontal\" && (\n <span lang=\"en\" style={englishStyle}>\n | {english}\n </span>\n )}\n </div>\n </Html>\n );\n};\n\n// Export memoized component for performance optimization\nexport const BaseText = React.memo(BaseTextComponent);\n\nBaseText.displayName = \"BaseText\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiFA,IAAM,qBAA8C,EAClD,QACA,SACA,WAAW;CAAC;CAAG;CAAG;CAAE,EACpB,OAAO,UACP,QAAQ,cAAc,cACtB,QAAQ,UACR,SAAS,UACT,SAAS,YACT,QACA,WAAW,OACX,QAAQ,OACR,UAAU,OACV,WACA,WAAW,YACP;CAEJ,MAAM,EAAE,UAAU,qBAAqB,eAAe;EACpD;EACA;EACD,CAAC;CAGF,MAAM,CAAC,aAAa,kBAAkB,eACpC,OAAO,WAAW,cACd,OAAO,aACP,cAAc,qBACnB;AAED,iBAAgB;AACd,MAAI,OAAO,WAAW,YAAa;EAEnC,MAAM,qBAAqB;AACzB,kBAAe,OAAO,WAAW;;AAGnC,SAAO,iBAAiB,UAAU,aAAa;AAC/C,eAAa,OAAO,oBAAoB,UAAU,aAAa;IAC9D,EAAE,CAAC;CAGN,MAAM,iBAAiB,cAAc;AACnC,SAAO,wBAAwB,aAAa,QAAQ,SAAS;IAC5D,CAAC,aAAa,SAAS,CAAC;CAG3B,MAAM,eAAe,cAAc;AACjC,SAAO,uBAAuB,OAAO,OAAO,gBAAgB,MAAM,QAAQ;IACzE;EAAC;EAAO;EAAgB;EAAQ,CAAC;CAGpC,MAAM,YAAY,eACT;EACL,OAAO,gBAAgB,MAAM;EAC7B,YAAY,iBAAiB;EAC7B,WAAW;EACX,YAAY,WAAW,SAAS,SAAS;EACzC,YAAY,aAAa,gBAAgB,cAAc,aAAa,GAAI;EACxE,YAAY;EACZ,kBAAkB;EAElB,YAAY,iBAAiB;EAC7B,eAAe,iBAAiB;EAChC,WAAW,iBAAiB;EAC5B,UAAU,iBAAiB;EAC5B,GACD;EAAC;EAAO;EAAO;EAAQ;EAAiB,CACzC;CAED,MAAM,iBAAiB,eACd;EACL,SAAS;EACT,eAAe,WAAW,aAAa,WAAW;EAClD,YAAY;EACZ,KAAK,WAAW,aAAa,QAAQ;EAErC,WAAW,aAAa;EACxB,eAAe,aAAa;EAC5B,QAAQ,aAAa;EACtB,GACD,CAAC,QAAQ,aAAa,CACvB;CAED,MAAM,cAAc,eACX;EACL,GAAG;EACH,UAAU,SAAS;EACpB,GACD,CAAC,WAAW,SAAS,OAAO,CAC7B;CAED,MAAM,eAAe,eACZ;EACL,GAAG;EACH,UAAU,SAAS;EACnB,SAAS;EACT,WAAW;EACZ,GACD,CAAC,WAAW,SAAS,QAAQ,CAC9B;AAED,QACE,oBAAC,MAAD;EACY;EACV,QAAQ,aAAa;EACrB,gBAAgB,aAAa;EAC7B,SAAS,aAAa;EACtB,OAAO,EAAE,eAAe,aAAa,eAAe;YAEpD,qBAAC,OAAD;GACE,OAAO;GACP,eAAa,UAAU;GACvB,cAAY;GACZ,aAAW;aAJb;IAME,oBAAC,QAAD;KAAM,MAAK;KAAK,OAAO;eACpB;KACI,CAAA;IACN,WAAW,cACV,oBAAC,QAAD;KAAM,MAAK;KAAK,OAAO;eACpB;KACI,CAAA;IAER,WAAW,gBACV,qBAAC,QAAD;KAAM,MAAK;KAAK,OAAO;eAAvB,CAAqC,MAChC,QACE;;IAEL;;EACD,CAAA;;AAKX,IAAa,WAAW,MAAM,KAAK,kBAAkB;AAErD,SAAS,cAAc"}
1
+ {"version":3,"file":"BaseText.js","names":[],"sources":["../../../../src/components/shared/base/BaseText.tsx"],"sourcesContent":["/**\n * BaseText - Enhanced bilingual text component with Korean theming\n *\n * Builds on existing KoreanText with extracted common logic\n * Provides consistent text styling across the application\n *\n * Now with Html overlay positioning helpers for:\n * - Consistent z-index management\n * - Performance optimization with distanceFactor\n * - GPU acceleration\n *\n * @module components/base\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo, useState, useEffect } from \"react\";\nimport { KOREAN_COLORS, UI_DIMENSIONS } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../utils/htmlOverlayHelpers\";\nimport type { HtmlOverlayLayer } from \"../../../types/HtmlOverlayTypes\";\nimport { useKoreanTheme } from \"./useKoreanTheme\";\n\n/**\n * Props for BaseText component\n */\nexport interface BaseTextProps {\n readonly korean: string;\n readonly english: string;\n readonly position?: [number, number, number];\n readonly size?: \"small\" | \"medium\" | \"large\" | \"xlarge\";\n readonly color?: number;\n readonly align?: \"left\" | \"center\" | \"right\";\n readonly weight?: \"normal\" | \"bold\";\n readonly layout?: \"vertical\" | \"horizontal\";\n readonly testId?: string;\n readonly isMobile?: boolean;\n /** Html overlay layer for z-index (default: 'hud') */\n readonly layer?: HtmlOverlayLayer;\n /** Whether text should occlude behind 3D objects (default: false) */\n readonly occlude?: boolean;\n /** ARIA label for accessibility (optional) */\n readonly ariaLabel?: string;\n /** ARIA live region for dynamic content (optional) */\n readonly ariaLive?: \"polite\" | \"assertive\" | \"off\";\n}\n\n/**\n * BaseText Component\n *\n * Enhanced bilingual text component with Korean cyberpunk styling.\n * Uses useKoreanTheme hook for consistent text sizing and styling.\n * Now includes Html overlay positioning helpers for proper z-index and performance.\n *\n * Korean Typography Features:\n * - Optimized line height (1.6) for Korean character readability\n * - Letter spacing (-0.01em) for tighter Korean text\n * - Word break (keep-all) to prevent breaking Korean words mid-syllable\n * - Proper language attributes (lang=\"ko\" / lang=\"en\")\n *\n * WCAG 2.1 AA Accessibility Features:\n * - Proper language attributes for screen readers\n * - Optional ARIA labels for additional context\n * - ARIA live regions for dynamic content\n *\n * Optimized with React.memo for performance\n *\n * @example\n * ```tsx\n * <BaseText\n * korean=\"공격\"\n * english=\"Attack\"\n * size=\"large\"\n * layout=\"vertical\"\n * layer=\"hud\"\n * ariaLive=\"polite\"\n * />\n * ```\n */\nconst BaseTextComponent: React.FC<BaseTextProps> = ({\n korean,\n english,\n position = [0, 0, 0],\n size = \"medium\",\n color = KOREAN_COLORS.TEXT_PRIMARY,\n align = \"center\",\n weight = \"normal\",\n layout = \"vertical\",\n testId,\n isMobile = false,\n layer = \"hud\",\n occlude = false,\n ariaLabel,\n ariaLive = \"off\",\n}) => {\n // Use Korean theme hook for consistent text sizing\n const { textSize, koreanTypography } = useKoreanTheme({\n size,\n isMobile,\n });\n\n // Track screen width for responsive distance factor updates on resize\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH,\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // Calculate optimal distance factor for text\n const distanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"text\", isMobile);\n }, [screenWidth, isMobile]);\n\n // Apply Html overlay styles with proper z-index\n const overlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(layer, false, distanceFactor, true, occlude);\n }, [layer, distanceFactor, occlude]);\n\n // Memoize text styles for performance with Korean typography optimization\n const textStyle = useMemo<React.CSSProperties>(\n () => ({\n color: hexToRgbaString(color),\n fontFamily: koreanTypography.fontFamily,\n textAlign: align,\n fontWeight: weight === \"bold\" ? \"bold\" : \"normal\",\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n // Korean typography optimization\n lineHeight: koreanTypography.lineHeight,\n letterSpacing: koreanTypography.letterSpacing,\n wordBreak: koreanTypography.wordBreak,\n wordWrap: koreanTypography.wordWrap,\n }),\n [color, align, weight, koreanTypography],\n );\n\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"flex\",\n flexDirection: layout === \"vertical\" ? \"column\" : \"row\",\n alignItems: \"center\",\n gap: layout === \"vertical\" ? \"4px\" : \"8px\",\n // Apply GPU acceleration from overlay style\n transform: overlayStyle.transform,\n pointerEvents: overlayStyle.pointerEvents,\n zIndex: overlayStyle.zIndex,\n }),\n [layout, overlayStyle],\n );\n\n const koreanStyle = useMemo<React.CSSProperties>(\n () => ({\n ...textStyle,\n fontSize: textSize.korean,\n }),\n [textStyle, textSize.korean],\n );\n\n const englishStyle = useMemo<React.CSSProperties>(\n () => ({\n ...textStyle,\n fontSize: textSize.english,\n opacity: 0.8,\n fontStyle: \"italic\",\n }),\n [textStyle, textSize.english],\n );\n\n return (\n <Html\n position={position}\n center={overlayStyle.center}\n distanceFactor={overlayStyle.distanceFactor}\n occlude={overlayStyle.occlude}\n style={{ pointerEvents: overlayStyle.pointerEvents }}\n >\n <div\n style={containerStyle}\n data-testid={testId ?? \"base-text\"}\n aria-label={ariaLabel}\n aria-live={ariaLive}\n >\n <span lang=\"ko\" style={koreanStyle}>\n {korean}\n </span>\n {layout === \"vertical\" && (\n <span lang=\"en\" style={englishStyle}>\n {english}\n </span>\n )}\n {layout === \"horizontal\" && (\n <span lang=\"en\" style={englishStyle}>\n | {english}\n </span>\n )}\n </div>\n </Html>\n );\n};\n\n// Export memoized component for performance optimization\nexport const BaseText = React.memo(BaseTextComponent);\n\nBaseText.displayName = \"BaseText\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiFA,IAAM,qBAA8C,EAClD,QACA,SACA,WAAW;CAAC;CAAG;CAAG;CAAE,EACpB,OAAO,UACP,QAAQ,cAAc,cACtB,QAAQ,UACR,SAAS,UACT,SAAS,YACT,QACA,WAAW,OACX,QAAQ,OACR,UAAU,OACV,WACA,WAAW,YACP;CAEJ,MAAM,EAAE,UAAU,qBAAqB,eAAe;EACpD;EACA;EACD,CAAC;CAGF,MAAM,CAAC,aAAa,kBAAkB,eACpC,OAAO,WAAW,cACd,OAAO,aACP,cAAc,qBACnB;CAED,gBAAgB;EACd,IAAI,OAAO,WAAW,aAAa;EAEnC,MAAM,qBAAqB;GACzB,eAAe,OAAO,WAAW;;EAGnC,OAAO,iBAAiB,UAAU,aAAa;EAC/C,aAAa,OAAO,oBAAoB,UAAU,aAAa;IAC9D,EAAE,CAAC;CAGN,MAAM,iBAAiB,cAAc;EACnC,OAAO,wBAAwB,aAAa,QAAQ,SAAS;IAC5D,CAAC,aAAa,SAAS,CAAC;CAG3B,MAAM,eAAe,cAAc;EACjC,OAAO,uBAAuB,OAAO,OAAO,gBAAgB,MAAM,QAAQ;IACzE;EAAC;EAAO;EAAgB;EAAQ,CAAC;CAGpC,MAAM,YAAY,eACT;EACL,OAAO,gBAAgB,MAAM;EAC7B,YAAY,iBAAiB;EAC7B,WAAW;EACX,YAAY,WAAW,SAAS,SAAS;EACzC,YAAY,aAAa,gBAAgB,cAAc,aAAa,GAAI;EACxE,YAAY;EACZ,kBAAkB;EAElB,YAAY,iBAAiB;EAC7B,eAAe,iBAAiB;EAChC,WAAW,iBAAiB;EAC5B,UAAU,iBAAiB;EAC5B,GACD;EAAC;EAAO;EAAO;EAAQ;EAAiB,CACzC;CAED,MAAM,iBAAiB,eACd;EACL,SAAS;EACT,eAAe,WAAW,aAAa,WAAW;EAClD,YAAY;EACZ,KAAK,WAAW,aAAa,QAAQ;EAErC,WAAW,aAAa;EACxB,eAAe,aAAa;EAC5B,QAAQ,aAAa;EACtB,GACD,CAAC,QAAQ,aAAa,CACvB;CAED,MAAM,cAAc,eACX;EACL,GAAG;EACH,UAAU,SAAS;EACpB,GACD,CAAC,WAAW,SAAS,OAAO,CAC7B;CAED,MAAM,eAAe,eACZ;EACL,GAAG;EACH,UAAU,SAAS;EACnB,SAAS;EACT,WAAW;EACZ,GACD,CAAC,WAAW,SAAS,QAAQ,CAC9B;CAED,OACE,oBAAC,MAAD;EACY;EACV,QAAQ,aAAa;EACrB,gBAAgB,aAAa;EAC7B,SAAS,aAAa;EACtB,OAAO,EAAE,eAAe,aAAa,eAAe;YAEpD,qBAAC,OAAD;GACE,OAAO;GACP,eAAa,UAAU;GACvB,cAAY;GACZ,aAAW;aAJb;IAME,oBAAC,QAAD;KAAM,MAAK;KAAK,OAAO;eACpB;KACI,CAAA;IACN,WAAW,cACV,oBAAC,QAAD;KAAM,MAAK;KAAK,OAAO;eACpB;KACI,CAAA;IAER,WAAW,gBACV,qBAAC,QAAD;KAAM,MAAK;KAAK,OAAO;eAAvB,CAAqC,MAChC,QACE;;IAEL;;EACD,CAAA;;AAKX,IAAa,WAAW,MAAM,KAAK,kBAAkB;AAErD,SAAS,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"useKoreanTheme.js","names":[],"sources":["../../../../src/components/shared/base/useKoreanTheme.ts"],"sourcesContent":["/**\n * useKoreanTheme - Custom hook for Korean cyberpunk theming\n * \n * Provides centralized Korean theme styling and responsive utilities\n * Eliminates code duplication across UI components\n * \n * Enhanced with:\n * - Korean typography optimization (line height, letter spacing, word break)\n * - Accessibility features (focus indicators, touch targets, high contrast)\n * - WCAG 2.1 AA compliant color contrast ratios\n * \n * @module components/base\n */\n\nimport { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\n\n/**\n * Button variant configuration\n */\nexport interface ButtonVariantConfig {\n readonly background: number;\n readonly border: number;\n readonly text: number;\n readonly hoverBg: string;\n readonly activeBg: string;\n}\n\n/**\n * Panel variant configuration\n */\nexport interface PanelVariantConfig {\n readonly background: string;\n readonly border: string;\n readonly boxShadow: string;\n}\n\n/**\n * Size dimension configuration\n */\nexport interface SizeDimensions {\n readonly padding: string;\n readonly fontSize: string;\n readonly borderWidth: string;\n}\n\n/**\n * Text size configuration\n */\nexport interface TextSizeConfig {\n readonly korean: string;\n readonly english: string;\n}\n\n/**\n * Korean typography configuration\n */\nexport interface KoreanTypographyConfig {\n readonly fontFamily: string;\n readonly lineHeight: number;\n readonly letterSpacing: string;\n readonly wordBreak: \"normal\" | \"break-all\" | \"keep-all\" | \"break-word\";\n readonly wordWrap: \"normal\" | \"break-word\";\n}\n\n/**\n * Accessibility configuration\n */\nexport interface AccessibilityConfig {\n readonly focusOutline: string;\n readonly focusOutlineOffset: string;\n readonly minTouchTarget: string;\n readonly highContrastFocusOutline: string;\n}\n\n/**\n * Korean theme hook configuration\n */\nexport interface UseKoreanThemeConfig {\n readonly variant?: \"primary\" | \"secondary\" | \"danger\" | \"default\" | \"bordered\" | \"elevated\";\n readonly size?: \"sm\" | \"md\" | \"lg\" | \"small\" | \"medium\" | \"large\" | \"xlarge\";\n readonly disabled?: boolean;\n readonly isMobile?: boolean;\n readonly highContrast?: boolean;\n}\n\n/**\n * Custom hook for Korean cyberpunk theming\n * \n * Provides consistent styling patterns for all Korean-themed components\n * Enhanced with Korean typography optimization and accessibility features\n * \n * @example\n * ```tsx\n * const { buttonVariant, sizeDimensions, koreanTypography } = useKoreanTheme({\n * variant: \"primary\",\n * size: \"md\"\n * });\n * ```\n */\nexport function useKoreanTheme(config: UseKoreanThemeConfig = {}) {\n const {\n variant = \"primary\",\n size = \"md\",\n disabled = false,\n isMobile = false,\n highContrast = false,\n } = config;\n\n /**\n * Get button variant colors\n */\n const buttonVariant = useMemo<ButtonVariantConfig>(() => {\n switch (variant) {\n case \"primary\":\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2),\n };\n case \"secondary\":\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n border: KOREAN_COLORS.ACCENT_GOLD,\n text: KOREAN_COLORS.TEXT_PRIMARY,\n hoverBg: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.2),\n };\n case \"danger\":\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n border: KOREAN_COLORS.ACCENT_RED,\n text: KOREAN_COLORS.ACCENT_RED,\n hoverBg: hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.2),\n };\n default:\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2),\n };\n }\n }, [variant]);\n\n /**\n * Get panel variant styling\n */\n const panelVariant = useMemo<PanelVariantConfig>(() => {\n switch (variant) {\n case \"bordered\":\n return {\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n };\n case \"elevated\":\n return {\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.5)}`,\n boxShadow: `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)},\n 0 0 20px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.2)}\n `,\n };\n case \"default\":\n default:\n return {\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.UI_BORDER, 0.5)}`,\n boxShadow: \"none\",\n };\n }\n }, [variant]);\n\n /**\n * Get size dimensions for buttons with touch-optimized mobile values\n * Mobile: 48px+ minimum touch targets with 16px+ Korean font\n */\n const buttonSize = useMemo<SizeDimensions>(() => {\n // Touch-optimized sizing for mobile (no scale down)\n const scale = isMobile ? 1.0 : 1.0; // Keep full size on mobile for 48px+ targets\n \n switch (size) {\n case \"sm\":\n case \"small\":\n return {\n padding: `${Math.round(8 * scale)}px ${Math.round(16 * scale)}px`,\n fontSize: isMobile ? \"14px\" : `${Math.round(14 * scale)}px`, // Maintain readable size\n borderWidth: \"1px\",\n };\n case \"lg\":\n case \"large\":\n return {\n padding: `${Math.round(16 * scale)}px ${Math.round(32 * scale)}px`,\n fontSize: isMobile ? \"18px\" : `${Math.round(20 * scale)}px`, // 18px minimum for Korean\n borderWidth: \"3px\",\n };\n case \"md\":\n case \"medium\":\n default:\n return {\n padding: `${Math.round(12 * scale)}px ${Math.round(24 * scale)}px`,\n fontSize: isMobile ? \"16px\" : `${Math.round(16 * scale)}px`, // 16px minimum for Korean\n borderWidth: \"2px\",\n };\n }\n }, [size, isMobile]);\n\n /**\n * Get text size configuration\n */\n const textSize = useMemo<TextSizeConfig>(() => {\n const scale = isMobile ? 0.9 : 1.0;\n \n switch (size) {\n case \"small\":\n return {\n korean: `${Math.round(14 * scale)}px`,\n english: `${Math.round(12 * scale)}px`,\n };\n case \"large\":\n return {\n korean: `${Math.round(24 * scale)}px`,\n english: `${Math.round(18 * scale)}px`,\n };\n case \"xlarge\":\n return {\n korean: `${Math.round(32 * scale)}px`,\n english: `${Math.round(24 * scale)}px`,\n };\n case \"medium\":\n default:\n return {\n korean: `${Math.round(18 * scale)}px`,\n english: `${Math.round(14 * scale)}px`,\n };\n }\n }, [size, isMobile]);\n\n /**\n * Calculate responsive size\n */\n const calculateResponsiveSize = useMemo(() => {\n return (baseSize: number) => {\n return isMobile ? Math.round(baseSize * 0.8) : baseSize;\n };\n }, [isMobile]);\n\n /**\n * Apply Korean theme to base styles\n */\n const applyKoreanTheme = useMemo(() => {\n return (baseStyle: React.CSSProperties): React.CSSProperties => {\n return {\n ...baseStyle,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n opacity: disabled ? 0.5 : 1,\n };\n };\n }, [disabled]);\n\n /**\n * Korean typography configuration\n * Optimized for Korean character readability\n * \n * - Line height: 1.6 for Korean characters (vs 1.5 for Latin)\n * - Letter spacing: -0.01em for tighter Korean spacing\n * - Word break: keep-all to prevent breaking Korean words\n * - Word wrap: break-word for long words\n */\n const koreanTypography = useMemo<KoreanTypographyConfig>(() => ({\n fontFamily: FONT_FAMILY.KOREAN,\n lineHeight: 1.6, // Optimal for Korean character readability\n letterSpacing: \"-0.01em\", // Tighter spacing for Korean\n wordBreak: \"keep-all\", // Prevent breaking Korean words mid-syllable\n wordWrap: \"break-word\", // Wrap long words appropriately\n }), []);\n\n /**\n * Accessibility configuration\n * WCAG 2.1 AA compliant focus indicators and touch targets\n */\n const accessibility = useMemo<AccessibilityConfig>(() => ({\n focusOutline: highContrast\n ? `4px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)}` // High contrast mode\n : `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN)}`, // Normal mode\n focusOutlineOffset: \"2px\",\n minTouchTarget: \"44px\", // WCAG 2.1 AA minimum touch target size\n highContrastFocusOutline: `4px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)}`,\n }), [highContrast]);\n\n return {\n buttonVariant,\n panelVariant,\n buttonSize,\n textSize,\n calculateResponsiveSize,\n applyKoreanTheme,\n koreanTypography,\n accessibility,\n colors: KOREAN_COLORS,\n fontFamily: FONT_FAMILY,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useKoreanTheme.js","names":[],"sources":["../../../../src/components/shared/base/useKoreanTheme.ts"],"sourcesContent":["/**\n * useKoreanTheme - Custom hook for Korean cyberpunk theming\n * \n * Provides centralized Korean theme styling and responsive utilities\n * Eliminates code duplication across UI components\n * \n * Enhanced with:\n * - Korean typography optimization (line height, letter spacing, word break)\n * - Accessibility features (focus indicators, touch targets, high contrast)\n * - WCAG 2.1 AA compliant color contrast ratios\n * \n * @module components/base\n */\n\nimport { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\n\n/**\n * Button variant configuration\n */\nexport interface ButtonVariantConfig {\n readonly background: number;\n readonly border: number;\n readonly text: number;\n readonly hoverBg: string;\n readonly activeBg: string;\n}\n\n/**\n * Panel variant configuration\n */\nexport interface PanelVariantConfig {\n readonly background: string;\n readonly border: string;\n readonly boxShadow: string;\n}\n\n/**\n * Size dimension configuration\n */\nexport interface SizeDimensions {\n readonly padding: string;\n readonly fontSize: string;\n readonly borderWidth: string;\n}\n\n/**\n * Text size configuration\n */\nexport interface TextSizeConfig {\n readonly korean: string;\n readonly english: string;\n}\n\n/**\n * Korean typography configuration\n */\nexport interface KoreanTypographyConfig {\n readonly fontFamily: string;\n readonly lineHeight: number;\n readonly letterSpacing: string;\n readonly wordBreak: \"normal\" | \"break-all\" | \"keep-all\" | \"break-word\";\n readonly wordWrap: \"normal\" | \"break-word\";\n}\n\n/**\n * Accessibility configuration\n */\nexport interface AccessibilityConfig {\n readonly focusOutline: string;\n readonly focusOutlineOffset: string;\n readonly minTouchTarget: string;\n readonly highContrastFocusOutline: string;\n}\n\n/**\n * Korean theme hook configuration\n */\nexport interface UseKoreanThemeConfig {\n readonly variant?: \"primary\" | \"secondary\" | \"danger\" | \"default\" | \"bordered\" | \"elevated\";\n readonly size?: \"sm\" | \"md\" | \"lg\" | \"small\" | \"medium\" | \"large\" | \"xlarge\";\n readonly disabled?: boolean;\n readonly isMobile?: boolean;\n readonly highContrast?: boolean;\n}\n\n/**\n * Custom hook for Korean cyberpunk theming\n * \n * Provides consistent styling patterns for all Korean-themed components\n * Enhanced with Korean typography optimization and accessibility features\n * \n * @example\n * ```tsx\n * const { buttonVariant, sizeDimensions, koreanTypography } = useKoreanTheme({\n * variant: \"primary\",\n * size: \"md\"\n * });\n * ```\n */\nexport function useKoreanTheme(config: UseKoreanThemeConfig = {}) {\n const {\n variant = \"primary\",\n size = \"md\",\n disabled = false,\n isMobile = false,\n highContrast = false,\n } = config;\n\n /**\n * Get button variant colors\n */\n const buttonVariant = useMemo<ButtonVariantConfig>(() => {\n switch (variant) {\n case \"primary\":\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2),\n };\n case \"secondary\":\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n border: KOREAN_COLORS.ACCENT_GOLD,\n text: KOREAN_COLORS.TEXT_PRIMARY,\n hoverBg: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.2),\n };\n case \"danger\":\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n border: KOREAN_COLORS.ACCENT_RED,\n text: KOREAN_COLORS.ACCENT_RED,\n hoverBg: hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.2),\n };\n default:\n return {\n background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.1),\n activeBg: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2),\n };\n }\n }, [variant]);\n\n /**\n * Get panel variant styling\n */\n const panelVariant = useMemo<PanelVariantConfig>(() => {\n switch (variant) {\n case \"bordered\":\n return {\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n };\n case \"elevated\":\n return {\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.5)}`,\n boxShadow: `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)},\n 0 0 20px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.2)}\n `,\n };\n case \"default\":\n default:\n return {\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.UI_BORDER, 0.5)}`,\n boxShadow: \"none\",\n };\n }\n }, [variant]);\n\n /**\n * Get size dimensions for buttons with touch-optimized mobile values\n * Mobile: 48px+ minimum touch targets with 16px+ Korean font\n */\n const buttonSize = useMemo<SizeDimensions>(() => {\n // Touch-optimized sizing for mobile (no scale down)\n const scale = isMobile ? 1.0 : 1.0; // Keep full size on mobile for 48px+ targets\n \n switch (size) {\n case \"sm\":\n case \"small\":\n return {\n padding: `${Math.round(8 * scale)}px ${Math.round(16 * scale)}px`,\n fontSize: isMobile ? \"14px\" : `${Math.round(14 * scale)}px`, // Maintain readable size\n borderWidth: \"1px\",\n };\n case \"lg\":\n case \"large\":\n return {\n padding: `${Math.round(16 * scale)}px ${Math.round(32 * scale)}px`,\n fontSize: isMobile ? \"18px\" : `${Math.round(20 * scale)}px`, // 18px minimum for Korean\n borderWidth: \"3px\",\n };\n case \"md\":\n case \"medium\":\n default:\n return {\n padding: `${Math.round(12 * scale)}px ${Math.round(24 * scale)}px`,\n fontSize: isMobile ? \"16px\" : `${Math.round(16 * scale)}px`, // 16px minimum for Korean\n borderWidth: \"2px\",\n };\n }\n }, [size, isMobile]);\n\n /**\n * Get text size configuration\n */\n const textSize = useMemo<TextSizeConfig>(() => {\n const scale = isMobile ? 0.9 : 1.0;\n \n switch (size) {\n case \"small\":\n return {\n korean: `${Math.round(14 * scale)}px`,\n english: `${Math.round(12 * scale)}px`,\n };\n case \"large\":\n return {\n korean: `${Math.round(24 * scale)}px`,\n english: `${Math.round(18 * scale)}px`,\n };\n case \"xlarge\":\n return {\n korean: `${Math.round(32 * scale)}px`,\n english: `${Math.round(24 * scale)}px`,\n };\n case \"medium\":\n default:\n return {\n korean: `${Math.round(18 * scale)}px`,\n english: `${Math.round(14 * scale)}px`,\n };\n }\n }, [size, isMobile]);\n\n /**\n * Calculate responsive size\n */\n const calculateResponsiveSize = useMemo(() => {\n return (baseSize: number) => {\n return isMobile ? Math.round(baseSize * 0.8) : baseSize;\n };\n }, [isMobile]);\n\n /**\n * Apply Korean theme to base styles\n */\n const applyKoreanTheme = useMemo(() => {\n return (baseStyle: React.CSSProperties): React.CSSProperties => {\n return {\n ...baseStyle,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n opacity: disabled ? 0.5 : 1,\n };\n };\n }, [disabled]);\n\n /**\n * Korean typography configuration\n * Optimized for Korean character readability\n * \n * - Line height: 1.6 for Korean characters (vs 1.5 for Latin)\n * - Letter spacing: -0.01em for tighter Korean spacing\n * - Word break: keep-all to prevent breaking Korean words\n * - Word wrap: break-word for long words\n */\n const koreanTypography = useMemo<KoreanTypographyConfig>(() => ({\n fontFamily: FONT_FAMILY.KOREAN,\n lineHeight: 1.6, // Optimal for Korean character readability\n letterSpacing: \"-0.01em\", // Tighter spacing for Korean\n wordBreak: \"keep-all\", // Prevent breaking Korean words mid-syllable\n wordWrap: \"break-word\", // Wrap long words appropriately\n }), []);\n\n /**\n * Accessibility configuration\n * WCAG 2.1 AA compliant focus indicators and touch targets\n */\n const accessibility = useMemo<AccessibilityConfig>(() => ({\n focusOutline: highContrast\n ? `4px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)}` // High contrast mode\n : `3px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN)}`, // Normal mode\n focusOutlineOffset: \"2px\",\n minTouchTarget: \"44px\", // WCAG 2.1 AA minimum touch target size\n highContrastFocusOutline: `4px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)}`,\n }), [highContrast]);\n\n return {\n buttonVariant,\n panelVariant,\n buttonSize,\n textSize,\n calculateResponsiveSize,\n applyKoreanTheme,\n koreanTypography,\n accessibility,\n colors: KOREAN_COLORS,\n fontFamily: FONT_FAMILY,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PerformanceDebugOverlayHtml.js","names":[],"sources":["../../../../src/components/shared/debug/PerformanceDebugOverlayHtml.tsx"],"sourcesContent":["/**\n * Performance Debug Overlay\n *\n * Shows real-time animation performance metrics in development mode.\n * Displays frame times, cache hit rate, and object pool status.\n *\n * Only visible in development mode.\n *\n * @module components/shared/debug/PerformanceDebugOverlayHtml\n * @category Debug\n * @korean 성능디버그오버레이\n */\n\nimport React, { useEffect, useState } from \"react\";\nimport { performanceMonitor } from \"../../../systems/animation\";\nimport { ThreeObjectPools } from \"../../../utils/threeObjectPool\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexToRgbaString, hexColorToCSS } from \"../../../utils/colorUtils\";\n\n/**\n * Performance metrics interface\n * @korean 성능지표\n */\ninterface PerformanceMetrics {\n avgFrameTime: number;\n maxFrameTime: number;\n cacheHitRate: number;\n cacheEntries: number;\n}\n\n/**\n * Pool status interface\n * @korean 풀상태\n */\ninterface PoolStatus {\n euler: number;\n vector3: number;\n matrix4: number;\n quaternion: number;\n}\n\n/**\n * Performance Debug Overlay Component\n *\n * Shows real-time animation performance metrics:\n * - Average/max frame times\n * - Cache hit rate\n * - Object pool utilization\n *\n * Only renders in development mode.\n *\n * Optimized with React.memo for 60fps performance:\n * - Memoized to prevent parent re-renders from affecting it\n * - Internal state updates only via interval\n *\n * @returns Performance overlay or null in production\n * @korean 성능디버그오버레이컴포넌트\n */\nexport const PerformanceDebugOverlayHtml = React.memo(() => {\n const [metrics, setMetrics] = useState<PerformanceMetrics>({\n avgFrameTime: 0,\n maxFrameTime: 0,\n cacheHitRate: 0,\n cacheEntries: 0,\n });\n const [pools, setPools] = useState<PoolStatus>({\n euler: 0,\n vector3: 0,\n matrix4: 0,\n quaternion: 0,\n });\n\n useEffect(() => {\n // Update metrics every second\n const interval = setInterval(() => {\n const newMetrics = performanceMonitor.getMetrics();\n setMetrics({\n avgFrameTime: newMetrics.avgFrameTime,\n maxFrameTime: newMetrics.maxFrameTime,\n cacheHitRate: newMetrics.cacheHitRate,\n cacheEntries: newMetrics.cacheEntries,\n });\n setPools(ThreeObjectPools.getStatus());\n }, 1000);\n\n return () => clearInterval(interval);\n }, []);\n\n // Only show in development\n if (import.meta.env.PROD) {\n return null;\n }\n\n // Color coding for frame times using KOREAN_COLORS\n const frameTimeColor =\n metrics.avgFrameTime < 5\n ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN) // Green: Target met\n : metrics.avgFrameTime < 8\n ? hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW) // Yellow: Warning\n : hexColorToCSS(KOREAN_COLORS.ACCENT_RED); // Red: Critical\n\n const cacheColor =\n metrics.cacheHitRate > 0.9\n ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN) // Green: Excellent\n : metrics.cacheHitRate > 0.7\n ? hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW) // Yellow: Good\n : hexColorToCSS(KOREAN_COLORS.ACCENT_RED); // Red: Poor\n\n const poolColor = (available: number, threshold: number) =>\n available > threshold\n ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN)\n : hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW);\n\n return (\n <div\n style={{\n position: \"fixed\",\n top: 10,\n right: 10,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n color: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n padding: \"12px\",\n fontFamily: \"monospace\",\n fontSize: \"11px\",\n lineHeight: \"1.4\",\n zIndex: 9999,\n border: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"4px\",\n minWidth: \"200px\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n }}\n data-testid=\"performance-debug-overlay\"\n >\n <div\n style={{\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n }}\n >\n 🎯 Animation Performance\n </div>\n <div\n style={{\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n marginBottom: \"6px\",\n }}\n />\n\n {/* Frame Times */}\n <div style={{ marginBottom: \"4px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Avg Frame:{\" \"}\n </span>\n <span style={{ color: frameTimeColor, fontWeight: \"bold\" }}>\n {metrics.avgFrameTime.toFixed(2)}ms\n </span>\n <span\n style={{\n color: hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_LIGHT),\n fontSize: \"9px\",\n marginLeft: \"4px\",\n }}\n >\n (target: &lt;5ms)\n </span>\n </div>\n <div style={{ marginBottom: \"6px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Max Frame:{\" \"}\n </span>\n <span style={{ color: frameTimeColor }}>\n {metrics.maxFrameTime.toFixed(2)}ms\n </span>\n </div>\n\n {/* Cache Performance */}\n <div style={{ marginBottom: \"4px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Cache Hit:{\" \"}\n </span>\n <span style={{ color: cacheColor, fontWeight: \"bold\" }}>\n {(metrics.cacheHitRate * 100).toFixed(1)}%\n </span>\n <span\n style={{\n color: hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_LIGHT),\n fontSize: \"9px\",\n marginLeft: \"4px\",\n }}\n >\n (target: &gt;90%)\n </span>\n </div>\n <div style={{ marginBottom: \"8px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Cached:{\" \"}\n </span>\n <span>{metrics.cacheEntries} keyframes</span>\n </div>\n\n <div\n style={{\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n marginBottom: \"6px\",\n }}\n />\n\n {/* Object Pools */}\n <div\n style={{\n fontSize: \"10px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginBottom: \"4px\",\n }}\n >\n Object Pools (available)\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Euler:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.euler, 100) }}>\n {pools.euler}\n </span>\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Vector3:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.vector3, 100) }}>\n {pools.vector3}\n </span>\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Matrix4:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.matrix4, 50) }}>\n {pools.matrix4}\n </span>\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Quaternion:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.quaternion, 50) }}>\n {pools.quaternion}\n </span>\n </div>\n\n {/* Performance Status */}\n <div\n style={{\n borderTop: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n marginTop: \"8px\",\n paddingTop: \"6px\",\n }}\n >\n <div style={{ fontSize: \"10px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Status:{\" \"}\n </span>\n {metrics.avgFrameTime < 5 && metrics.cacheHitRate > 0.9 ? (\n <span\n style={{\n color: hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN),\n fontWeight: \"bold\",\n }}\n >\n ✓ OPTIMAL\n </span>\n ) : metrics.avgFrameTime < 8 && metrics.cacheHitRate > 0.7 ? (\n <span\n style={{ color: hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW) }}\n >\n ⚠ GOOD\n </span>\n ) : (\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.ACCENT_RED) }}>\n ✗ NEEDS OPTIMIZATION\n </span>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nPerformanceDebugOverlayHtml.displayName = \"PerformanceDebugOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,IAAa,8BAA8B,MAAM,WAAW;CAC1D,MAAM,CAAC,SAAS,cAAc,SAA6B;EACzD,cAAc;EACd,cAAc;EACd,cAAc;EACd,cAAc;EACf,CAAC;CACF,MAAM,CAAC,OAAO,YAAY,SAAqB;EAC7C,OAAO;EACP,SAAS;EACT,SAAS;EACT,YAAY;EACb,CAAC;AAEF,iBAAgB;EAEd,MAAM,WAAW,kBAAkB;GACjC,MAAM,aAAa,mBAAmB,YAAY;AAClD,cAAW;IACT,cAAc,WAAW;IACzB,cAAc,WAAW;IACzB,cAAc,WAAW;IACzB,cAAc,WAAW;IAC1B,CAAC;AACF,YAAS,iBAAiB,WAAW,CAAC;KACrC,IAAK;AAER,eAAa,cAAc,SAAS;IACnC,EAAE,CAAC;AAIJ,QAAO;EAsMT;AAEF,4BAA4B,cAAc"}
1
+ {"version":3,"file":"PerformanceDebugOverlayHtml.js","names":[],"sources":["../../../../src/components/shared/debug/PerformanceDebugOverlayHtml.tsx"],"sourcesContent":["/**\n * Performance Debug Overlay\n *\n * Shows real-time animation performance metrics in development mode.\n * Displays frame times, cache hit rate, and object pool status.\n *\n * Only visible in development mode.\n *\n * @module components/shared/debug/PerformanceDebugOverlayHtml\n * @category Debug\n * @korean 성능디버그오버레이\n */\n\nimport React, { useEffect, useState } from \"react\";\nimport { performanceMonitor } from \"../../../systems/animation\";\nimport { ThreeObjectPools } from \"../../../utils/threeObjectPool\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexToRgbaString, hexColorToCSS } from \"../../../utils/colorUtils\";\n\n/**\n * Performance metrics interface\n * @korean 성능지표\n */\ninterface PerformanceMetrics {\n avgFrameTime: number;\n maxFrameTime: number;\n cacheHitRate: number;\n cacheEntries: number;\n}\n\n/**\n * Pool status interface\n * @korean 풀상태\n */\ninterface PoolStatus {\n euler: number;\n vector3: number;\n matrix4: number;\n quaternion: number;\n}\n\n/**\n * Performance Debug Overlay Component\n *\n * Shows real-time animation performance metrics:\n * - Average/max frame times\n * - Cache hit rate\n * - Object pool utilization\n *\n * Only renders in development mode.\n *\n * Optimized with React.memo for 60fps performance:\n * - Memoized to prevent parent re-renders from affecting it\n * - Internal state updates only via interval\n *\n * @returns Performance overlay or null in production\n * @korean 성능디버그오버레이컴포넌트\n */\nexport const PerformanceDebugOverlayHtml = React.memo(() => {\n const [metrics, setMetrics] = useState<PerformanceMetrics>({\n avgFrameTime: 0,\n maxFrameTime: 0,\n cacheHitRate: 0,\n cacheEntries: 0,\n });\n const [pools, setPools] = useState<PoolStatus>({\n euler: 0,\n vector3: 0,\n matrix4: 0,\n quaternion: 0,\n });\n\n useEffect(() => {\n // Update metrics every second\n const interval = setInterval(() => {\n const newMetrics = performanceMonitor.getMetrics();\n setMetrics({\n avgFrameTime: newMetrics.avgFrameTime,\n maxFrameTime: newMetrics.maxFrameTime,\n cacheHitRate: newMetrics.cacheHitRate,\n cacheEntries: newMetrics.cacheEntries,\n });\n setPools(ThreeObjectPools.getStatus());\n }, 1000);\n\n return () => clearInterval(interval);\n }, []);\n\n // Only show in development\n if (import.meta.env.PROD) {\n return null;\n }\n\n // Color coding for frame times using KOREAN_COLORS\n const frameTimeColor =\n metrics.avgFrameTime < 5\n ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN) // Green: Target met\n : metrics.avgFrameTime < 8\n ? hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW) // Yellow: Warning\n : hexColorToCSS(KOREAN_COLORS.ACCENT_RED); // Red: Critical\n\n const cacheColor =\n metrics.cacheHitRate > 0.9\n ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN) // Green: Excellent\n : metrics.cacheHitRate > 0.7\n ? hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW) // Yellow: Good\n : hexColorToCSS(KOREAN_COLORS.ACCENT_RED); // Red: Poor\n\n const poolColor = (available: number, threshold: number) =>\n available > threshold\n ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN)\n : hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW);\n\n return (\n <div\n style={{\n position: \"fixed\",\n top: 10,\n right: 10,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n color: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n padding: \"12px\",\n fontFamily: \"monospace\",\n fontSize: \"11px\",\n lineHeight: \"1.4\",\n zIndex: 9999,\n border: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"4px\",\n minWidth: \"200px\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n }}\n data-testid=\"performance-debug-overlay\"\n >\n <div\n style={{\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n }}\n >\n 🎯 Animation Performance\n </div>\n <div\n style={{\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n marginBottom: \"6px\",\n }}\n />\n\n {/* Frame Times */}\n <div style={{ marginBottom: \"4px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Avg Frame:{\" \"}\n </span>\n <span style={{ color: frameTimeColor, fontWeight: \"bold\" }}>\n {metrics.avgFrameTime.toFixed(2)}ms\n </span>\n <span\n style={{\n color: hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_LIGHT),\n fontSize: \"9px\",\n marginLeft: \"4px\",\n }}\n >\n (target: &lt;5ms)\n </span>\n </div>\n <div style={{ marginBottom: \"6px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Max Frame:{\" \"}\n </span>\n <span style={{ color: frameTimeColor }}>\n {metrics.maxFrameTime.toFixed(2)}ms\n </span>\n </div>\n\n {/* Cache Performance */}\n <div style={{ marginBottom: \"4px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Cache Hit:{\" \"}\n </span>\n <span style={{ color: cacheColor, fontWeight: \"bold\" }}>\n {(metrics.cacheHitRate * 100).toFixed(1)}%\n </span>\n <span\n style={{\n color: hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_LIGHT),\n fontSize: \"9px\",\n marginLeft: \"4px\",\n }}\n >\n (target: &gt;90%)\n </span>\n </div>\n <div style={{ marginBottom: \"8px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Cached:{\" \"}\n </span>\n <span>{metrics.cacheEntries} keyframes</span>\n </div>\n\n <div\n style={{\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n marginBottom: \"6px\",\n }}\n />\n\n {/* Object Pools */}\n <div\n style={{\n fontSize: \"10px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginBottom: \"4px\",\n }}\n >\n Object Pools (available)\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Euler:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.euler, 100) }}>\n {pools.euler}\n </span>\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Vector3:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.vector3, 100) }}>\n {pools.vector3}\n </span>\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Matrix4:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.matrix4, 50) }}>\n {pools.matrix4}\n </span>\n </div>\n <div style={{ marginBottom: \"2px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Quaternion:{\" \"}\n </span>\n <span style={{ color: poolColor(pools.quaternion, 50) }}>\n {pools.quaternion}\n </span>\n </div>\n\n {/* Performance Status */}\n <div\n style={{\n borderTop: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n marginTop: \"8px\",\n paddingTop: \"6px\",\n }}\n >\n <div style={{ fontSize: \"10px\" }}>\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n Status:{\" \"}\n </span>\n {metrics.avgFrameTime < 5 && metrics.cacheHitRate > 0.9 ? (\n <span\n style={{\n color: hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN),\n fontWeight: \"bold\",\n }}\n >\n ✓ OPTIMAL\n </span>\n ) : metrics.avgFrameTime < 8 && metrics.cacheHitRate > 0.7 ? (\n <span\n style={{ color: hexColorToCSS(KOREAN_COLORS.WARNING_YELLOW) }}\n >\n ⚠ GOOD\n </span>\n ) : (\n <span style={{ color: hexColorToCSS(KOREAN_COLORS.ACCENT_RED) }}>\n ✗ NEEDS OPTIMIZATION\n </span>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nPerformanceDebugOverlayHtml.displayName = \"PerformanceDebugOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,IAAa,8BAA8B,MAAM,WAAW;CAC1D,MAAM,CAAC,SAAS,cAAc,SAA6B;EACzD,cAAc;EACd,cAAc;EACd,cAAc;EACd,cAAc;EACf,CAAC;CACF,MAAM,CAAC,OAAO,YAAY,SAAqB;EAC7C,OAAO;EACP,SAAS;EACT,SAAS;EACT,YAAY;EACb,CAAC;CAEF,gBAAgB;EAEd,MAAM,WAAW,kBAAkB;GACjC,MAAM,aAAa,mBAAmB,YAAY;GAClD,WAAW;IACT,cAAc,WAAW;IACzB,cAAc,WAAW;IACzB,cAAc,WAAW;IACzB,cAAc,WAAW;IAC1B,CAAC;GACF,SAAS,iBAAiB,WAAW,CAAC;KACrC,IAAK;EAER,aAAa,cAAc,SAAS;IACnC,EAAE,CAAC;CAIJ,OAAO;EAsMT;AAEF,4BAA4B,cAAc"}