blacktrigram 0.7.47 → 0.7.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioAssetLoader.js.map +1 -1
  3. package/lib/audio/AudioAssetRegistry.js.map +1 -1
  4. package/lib/audio/AudioCache.js.map +1 -1
  5. package/lib/audio/AudioManager.js.map +1 -1
  6. package/lib/audio/AudioMonitor.js.map +1 -1
  7. package/lib/audio/AudioPool.js.map +1 -1
  8. package/lib/audio/AudioProvider.js.map +1 -1
  9. package/lib/audio/AudioUtils.js.map +1 -1
  10. package/lib/audio/BoneImpactAudioMap.js.map +1 -1
  11. package/lib/audio/VariantSelector.js.map +1 -1
  12. package/lib/audio/types.js.map +1 -1
  13. package/lib/components/screens/combat/CombatScreen3D.d.ts.map +1 -1
  14. package/lib/components/screens/combat/CombatScreen3D.js +29 -25
  15. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  18. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  19. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  20. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  21. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  22. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  25. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  26. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  27. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  28. package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
  29. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  30. package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
  31. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  32. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  33. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  34. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  36. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  37. package/lib/components/screens/combat/components/hud/CombatBottomHUD.d.ts.map +1 -1
  38. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js +2 -2
  39. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  40. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  41. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js +1 -1
  42. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  43. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  44. package/lib/components/screens/combat/components/hud/CombatTopHUD.d.ts.map +1 -1
  45. package/lib/components/screens/combat/components/hud/CombatTopHUD.js +2 -1
  46. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  47. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  48. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  49. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  50. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  51. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  52. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  53. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  54. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  55. package/lib/components/screens/combat/helpers/AnimationUpdater.d.ts.map +1 -1
  56. package/lib/components/screens/combat/helpers/AnimationUpdater.js +4 -2
  57. package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
  58. package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
  59. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  60. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  61. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  62. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  63. package/lib/components/screens/combat/hooks/useCombatLayout.d.ts.map +1 -1
  64. package/lib/components/screens/combat/hooks/useCombatLayout.js +11 -5
  65. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  66. package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
  67. package/lib/components/screens/controls/ControlsScreen3D.js +1 -1
  68. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  69. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  72. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  74. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  75. package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
  76. package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
  77. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  78. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  79. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  80. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  81. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  83. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  84. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  85. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  86. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  87. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  88. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  90. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  91. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  92. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  93. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  94. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  95. package/lib/components/screens/philosophy/PhilosophyScreen3D.js +1 -1
  96. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  97. package/lib/components/screens/training/TrainingScreen3D.d.ts.map +1 -1
  98. package/lib/components/screens/training/TrainingScreen3D.js +3 -11
  99. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  100. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  101. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  102. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  103. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  104. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  105. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  106. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  107. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  108. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  109. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  110. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  111. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  112. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  113. package/lib/components/screens/training/components/hud/TrainingBottomHUD.d.ts.map +1 -1
  114. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js +2 -2
  115. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  116. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  117. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  118. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  119. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  120. package/lib/components/screens/training/hooks/useTrainingActions.d.ts +1 -0
  121. package/lib/components/screens/training/hooks/useTrainingActions.d.ts.map +1 -1
  122. package/lib/components/screens/training/hooks/useTrainingActions.js +6 -4
  123. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  124. package/lib/components/screens/training/hooks/useTrainingLayout.d.ts.map +1 -1
  125. package/lib/components/screens/training/hooks/useTrainingLayout.js +11 -5
  126. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  127. package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
  128. package/lib/components/shared/base/BaseButton.js.map +1 -1
  129. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  130. package/lib/components/shared/base/BasePanel.js.map +1 -1
  131. package/lib/components/shared/base/BaseText.js.map +1 -1
  132. package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
  133. package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
  134. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  135. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  136. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  137. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  138. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  139. package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
  140. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  141. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  142. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  143. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  144. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  145. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  146. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  147. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  148. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  149. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  150. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  151. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  152. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  153. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  154. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  155. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  156. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  157. package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
  158. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  159. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  160. package/lib/components/shared/three/models/SkeletalPlayer3D.d.ts.map +1 -1
  161. package/lib/components/shared/three/models/SkeletalPlayer3D.js +7 -5
  162. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  163. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  164. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  165. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  166. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  167. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  168. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  169. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  170. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  171. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  172. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  173. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  174. package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
  175. package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
  176. package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
  177. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  178. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  179. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  180. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  181. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  182. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  183. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  184. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  185. package/lib/components/shared/ui/BackButton.js.map +1 -1
  186. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  187. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  188. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  189. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  190. package/lib/components/shared/ui/SplashScreen.js +2 -2
  191. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  192. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  193. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  194. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  195. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  196. package/lib/constants/bodyDimensions.js.map +1 -1
  197. package/lib/constants/bodyRenderingConstants.js.map +1 -1
  198. package/lib/data/archetypeClothing.js.map +1 -1
  199. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  200. package/lib/data/techniqueMappings.js.map +1 -1
  201. package/lib/data/techniques.js.map +1 -1
  202. package/lib/hooks/useActionFeedback.js.map +1 -1
  203. package/lib/hooks/useBalanceAnimations.js.map +1 -1
  204. package/lib/hooks/useCombatTimer.js.map +1 -1
  205. package/lib/hooks/useDebounce.js.map +1 -1
  206. package/lib/hooks/useHUDLayout.d.ts.map +1 -1
  207. package/lib/hooks/useHUDLayout.js +3 -2
  208. package/lib/hooks/useHUDLayout.js.map +1 -1
  209. package/lib/hooks/useHandPoseTransitions.js.map +1 -1
  210. package/lib/hooks/useKeyboardControls.js.map +1 -1
  211. package/lib/hooks/useMatchCountdown.js.map +1 -1
  212. package/lib/hooks/useMuscleActivation.js.map +1 -1
  213. package/lib/hooks/usePauseMenu.js.map +1 -1
  214. package/lib/hooks/usePlayerAnimation.js.map +1 -1
  215. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  216. package/lib/hooks/useRoundTransition.js.map +1 -1
  217. package/lib/hooks/useSkeletalAnimation.d.ts.map +1 -1
  218. package/lib/hooks/useSkeletalAnimation.js +1 -1
  219. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  220. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  221. package/lib/hooks/useThrottle.js.map +1 -1
  222. package/lib/hooks/useTouchControls.js.map +1 -1
  223. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  224. package/lib/hooks/useWindowSize.js.map +1 -1
  225. package/lib/systems/CombatSystem.js.map +1 -1
  226. package/lib/systems/EffectCalculator.js.map +1 -1
  227. package/lib/systems/LayoutSystem.js.map +1 -1
  228. package/lib/systems/PlayerEffectManager.js.map +1 -1
  229. package/lib/systems/ResponsiveScaling.js.map +1 -1
  230. package/lib/systems/TrigramSystem.js.map +1 -1
  231. package/lib/systems/VitalPointSystem.js.map +1 -1
  232. package/lib/systems/ai/AIPersonality.js.map +1 -1
  233. package/lib/systems/ai/AdaptiveDifficulty.js +16 -16
  234. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  235. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  236. package/lib/systems/ai/ComboSystem.js.map +1 -1
  237. package/lib/systems/ai/DecisionTree.js.map +1 -1
  238. package/lib/systems/ai/TrainingAI.js.map +1 -1
  239. package/lib/systems/ai/types.js.map +1 -1
  240. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  241. package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
  242. package/lib/systems/animation/builders/HandPoses.js.map +1 -1
  243. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  244. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  245. package/lib/systems/animation/builders/KickPhaseApplicator.d.ts +6 -0
  246. package/lib/systems/animation/builders/KickPhaseApplicator.d.ts.map +1 -1
  247. package/lib/systems/animation/builders/KickPhaseApplicator.js +16 -9
  248. package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
  249. package/lib/systems/animation/builders/KoreanGuardPositions.d.ts +4 -4
  250. package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
  251. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts +1 -1
  252. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts.map +1 -1
  253. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js +5 -5
  254. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  255. package/lib/systems/animation/builders/MartialArtsConstants.d.ts +112 -71
  256. package/lib/systems/animation/builders/MartialArtsConstants.d.ts.map +1 -1
  257. package/lib/systems/animation/builders/MartialArtsConstants.js +113 -72
  258. package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
  259. package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
  260. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  261. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  262. package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
  263. package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
  264. package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
  265. package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
  266. package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
  267. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  268. package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
  269. package/lib/systems/animation/catalogs/EnhancedAttackAnimations.js.map +1 -1
  270. package/lib/systems/animation/catalogs/EnhancedElbowKneeAnimations.js.map +1 -1
  271. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  272. package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
  273. package/lib/systems/animation/catalogs/GamStanceAnimations.js +21 -0
  274. package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -0
  275. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js +34 -2
  276. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
  277. package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
  278. package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
  279. package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
  280. package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts +9 -0
  281. package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts.map +1 -1
  282. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +288 -0
  283. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -0
  284. package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
  285. package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
  286. package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
  287. package/lib/systems/animation/catalogs/KickAnimations.d.ts +2 -2
  288. package/lib/systems/animation/catalogs/KickAnimations.js +2 -2
  289. package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
  290. package/lib/systems/animation/catalogs/LiStanceAnimations.js +14 -1
  291. package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
  292. package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
  293. package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
  294. package/lib/systems/animation/catalogs/PunchAnimations.d.ts +1 -1
  295. package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
  296. package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
  297. package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
  298. package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
  299. package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
  300. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
  301. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  302. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  303. package/lib/systems/animation/catalogs/StanceGuardPoses.d.ts +6 -6
  304. package/lib/systems/animation/catalogs/StanceGuardPoses.js +36 -36
  305. package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
  306. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  307. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  308. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  309. package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
  310. package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
  311. package/lib/systems/animation/constants/AnatomicalLimits.js.map +1 -1
  312. package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
  313. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  314. package/lib/systems/animation/core/AnimationPriority.js +15 -15
  315. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  316. package/lib/systems/animation/core/AnimationRegistry.d.ts +30 -0
  317. package/lib/systems/animation/core/AnimationRegistry.d.ts.map +1 -1
  318. package/lib/systems/animation/core/AnimationRegistry.js +74 -12
  319. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  320. package/lib/systems/animation/core/AnimationStateMachine.js +16 -16
  321. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  322. package/lib/systems/animation/core/AnimationTransitions.d.ts.map +1 -1
  323. package/lib/systems/animation/core/AnimationTransitions.js +34 -0
  324. package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
  325. package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
  326. package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
  327. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  328. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  329. package/lib/systems/animation/core/index.d.ts +1 -1
  330. package/lib/systems/animation/core/index.d.ts.map +1 -1
  331. package/lib/systems/animation/core/types.d.ts +24 -0
  332. package/lib/systems/animation/core/types.d.ts.map +1 -1
  333. package/lib/systems/animation/core/types.js +27 -11
  334. package/lib/systems/animation/core/types.js.map +1 -1
  335. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  336. package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
  337. package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
  338. package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
  339. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  340. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  341. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  342. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  343. package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
  344. package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
  345. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  346. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  347. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  348. package/lib/systems/bodypart/types.js.map +1 -1
  349. package/lib/systems/breathing/BreathingDisruptionSystem.js +19 -19
  350. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  351. package/lib/systems/breathing/feedback.js.map +1 -1
  352. package/lib/systems/breathing/integration.js.map +1 -1
  353. package/lib/systems/combat/BalanceSystem.js +19 -19
  354. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  355. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  356. package/lib/systems/combat/CombatStateSystem.js +17 -17
  357. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  358. package/lib/systems/combat/ConsciousnessSystem.js +24 -24
  359. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  360. package/lib/systems/combat/FallIntegration.js.map +1 -1
  361. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  362. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  363. package/lib/systems/combat/PainResponseSystem.js +21 -21
  364. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  365. package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
  366. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  367. package/lib/systems/combat/typeGuards.js.map +1 -1
  368. package/lib/systems/effects.js.map +1 -1
  369. package/lib/systems/game.js.map +1 -1
  370. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  371. package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
  372. package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
  373. package/lib/systems/movement/integration.js.map +1 -1
  374. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  375. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  376. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  377. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  378. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  379. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  380. package/lib/systems/physics/SpeedModifierSystem.js +6 -6
  381. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  382. package/lib/systems/trigram/KoreanCulture.js.map +1 -1
  383. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  384. package/lib/systems/trigram/StanceManager.js.map +1 -1
  385. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  386. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  387. package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
  388. package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
  389. package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
  390. package/lib/systems/trigram/techniques/GeonTechniques.js.map +1 -1
  391. package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
  392. package/lib/systems/trigram/techniques/JinTechniques.js.map +1 -1
  393. package/lib/systems/trigram/techniques/LiTechniques.js.map +1 -1
  394. package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
  395. package/lib/systems/trigram/techniques/TaeTechniques.js.map +1 -1
  396. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  397. package/lib/systems/trigram/techniques/index.js.map +1 -1
  398. package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
  399. package/lib/systems/trigram/types.js.map +1 -1
  400. package/lib/systems/types.js.map +1 -1
  401. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  402. package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
  403. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  404. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  405. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  406. package/lib/systems/vitalpoint/VitalPointsData.js.map +1 -1
  407. package/lib/types/AccessibilityTypes.js.map +1 -1
  408. package/lib/types/LayoutTypes.js.map +1 -1
  409. package/lib/types/PhysicsTypes.js.map +1 -1
  410. package/lib/types/common.js.map +1 -1
  411. package/lib/types/constants/animations.js.map +1 -1
  412. package/lib/types/constants/colors.js.map +1 -1
  413. package/lib/types/constants/designSystem.js.map +1 -1
  414. package/lib/types/constants/index.js.map +1 -1
  415. package/lib/types/constants/layout.d.ts +21 -0
  416. package/lib/types/constants/layout.d.ts.map +1 -1
  417. package/lib/types/constants/layout.js +22 -1
  418. package/lib/types/constants/layout.js.map +1 -1
  419. package/lib/types/constants/performance.js.map +1 -1
  420. package/lib/types/constants/typography.js.map +1 -1
  421. package/lib/types/constants/ui.js.map +1 -1
  422. package/lib/types/facial.js +19 -19
  423. package/lib/types/facial.js.map +1 -1
  424. package/lib/types/hand-animation.js.map +1 -1
  425. package/lib/types/injury.js.map +1 -1
  426. package/lib/types/muscle.js.map +1 -1
  427. package/lib/types/physics.js.map +1 -1
  428. package/lib/types/physicsConstants.js.map +1 -1
  429. package/lib/types/player-visual.d.ts +1 -1
  430. package/lib/types/player-visual.d.ts.map +1 -1
  431. package/lib/types/skeletal.js.map +1 -1
  432. package/lib/types/techniqueId.js.map +1 -1
  433. package/lib/utils/accessibility.js.map +1 -1
  434. package/lib/utils/arenaWorldDimensions.js.map +1 -1
  435. package/lib/utils/assetConfig.js.map +1 -1
  436. package/lib/utils/characterScaling.js.map +1 -1
  437. package/lib/utils/colorHelpers.js.map +1 -1
  438. package/lib/utils/colorUtils.js.map +1 -1
  439. package/lib/utils/combatReadiness.js.map +1 -1
  440. package/lib/utils/controlMapping.js.map +1 -1
  441. package/lib/utils/deviceDetection.js +6 -7
  442. package/lib/utils/deviceDetection.js.map +1 -1
  443. package/lib/utils/effectUtils.js.map +1 -1
  444. package/lib/utils/fabricTextures.js.map +1 -1
  445. package/lib/utils/hapticFeedback.js.map +1 -1
  446. package/lib/utils/haptics.js.map +1 -1
  447. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  448. package/lib/utils/inputSystem.js.map +1 -1
  449. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  450. package/lib/utils/math.js.map +1 -1
  451. package/lib/utils/mobileLayoutHelpers.js.map +1 -1
  452. package/lib/utils/mobileUIUtils.js.map +1 -1
  453. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  454. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  455. package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
  456. package/lib/utils/performanceOptimization.js.map +1 -1
  457. package/lib/utils/player3DHelpers.js.map +1 -1
  458. package/lib/utils/playerUtils.js.map +1 -1
  459. package/lib/utils/responsiveLayout.js.map +1 -1
  460. package/lib/utils/responsiveLayoutHelpers.d.ts +7 -0
  461. package/lib/utils/responsiveLayoutHelpers.d.ts.map +1 -1
  462. package/lib/utils/responsiveLayoutHelpers.js +16 -2
  463. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  464. package/lib/utils/responsiveOrientationConstants.js.map +1 -1
  465. package/lib/utils/safeAreaUtils.js.map +1 -1
  466. package/lib/utils/sharedPhysicsConfig.js.map +1 -1
  467. package/lib/utils/skeletonScaling.js.map +1 -1
  468. package/lib/utils/stanceHelpers.js.map +1 -1
  469. package/lib/utils/threeObjectPool.js.map +1 -1
  470. package/lib/utils/visualEffects.js.map +1 -1
  471. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"inputSystem.js","names":[],"sources":["../../src/utils/inputSystem.ts"],"sourcesContent":["import { COMBAT_CONTROLS } from \"@/systems/types\";\nimport type { Position } from \"@/types/common\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport type { MovementInput } from \"../systems/physics/MovementPhysics\";\nimport { MovementPhysics } from \"../systems/physics/MovementPhysics\";\nimport { TrigramStance } from \"../types/common\";\nimport { calculateArenaBounds, DEFAULT_PHYSICS_ARENA_BOUNDS } from \"../types/PhysicsTypes\";\nimport type { MovementArenaBounds } from \"../types/PhysicsTypes\";\n\n/**\n * Configuration interface for the input system and player movement.\n * Uses physics-first approach: all positions and velocities are in meters.\n *\n * **Korean**: 입력 시스템 설정 (Input System Configuration)\n *\n * ## Physics-First Architecture\n *\n * This interface requires worldWidthMeters and worldDepthMeters to enable\n * the new physics-first coordinate system. Without these properties, the\n * movement system cannot properly convert between physics (meters) and\n * rendering (pixels).\n *\n * ### Migration Guide\n *\n * Existing code must be updated to pass world dimensions:\n *\n * ```typescript\n * // Before (incorrect):\n * const config = { bounds: { x: 0, y: 0, width: 960, height: 480 } };\n *\n * // After (correct):\n * const config = {\n * bounds: {\n * worldWidthMeters: 10, // From layout hook\n * worldDepthMeters: 10 // From layout hook\n * }\n * };\n * ```\n *\n * ### Fallback Behavior\n *\n * If worldWidthMeters/worldDepthMeters are not provided, the system falls back\n * to DEFAULT_PHYSICS_ARENA_BOUNDS (10m × 7.5m) to ensure movement stays bounded.\n * Callers SHOULD provide these values from their layout hooks (useCombatLayout, \n * useTrainingLayout) for proper arena sizing.\n */\nexport interface InputSystemConfig {\n /** Whether the input system is enabled and processing input */\n readonly enabled?: boolean;\n\n /**\n * Arena world dimensions in meters for physics calculations.\n *\n * **REQUIRED for physics-first coordinate system to work.**\n *\n * These values must come from layout hooks:\n * - CombatScreen3D: Use arenaBounds.worldWidthMeters/worldDepthMeters from useCombatLayout()\n * - TrainingScreen3D: Use trainingAreaBounds.worldWidthMeters/worldDepthMeters from useTrainingLayout()\n */\n readonly bounds?: {\n /** Physical arena width in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldWidthMeters: number;\n /** Physical arena depth in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldDepthMeters: number;\n };\n\n /** Callback invoked when player position changes (position in meters) */\n readonly onPositionChange?: (position: Position) => void;\n\n /** Initial player position in METERS (x = lateral, y = forward/backward) */\n readonly initialPositionMeters?: Position;\n\n // Physics-based movement parameters (always enabled)\n /** Current trigram stance affecting movement speed */\n readonly currentStance?: TrigramStance;\n\n /** Leg injury factor (0-1, where 1 is fully injured) affecting movement speed */\n readonly legInjuryFactor?: number;\n\n /** Whether player is running (sprint mode) */\n readonly isRunning?: boolean;\n\n /** Whether to use tactical step mode (30cm grid quantization) */\n readonly useTacticalSteps?: boolean;\n\n // Speed modifier overrides from SpeedModifierSystem\n /** Final calculated maximum speed in meters per second */\n readonly maxSpeedOverride?: number;\n\n /** Final calculated acceleration in meters per second squared */\n readonly accelerationOverride?: number;\n}\n\nexport interface MovementState {\n readonly up: boolean;\n readonly down: boolean;\n readonly left: boolean;\n readonly right: boolean;\n readonly position: Position;\n readonly isMoving: boolean; // Add isMoving to movement state\n}\n\nexport interface PlayerMovementResult {\n /** Player position in METERS (x = lateral, y = forward/backward in arena) */\n readonly playerPosition: Position;\n readonly movementState: MovementState;\n readonly isMoving: boolean;\n readonly isKeyPressed: (key: string) => boolean;\n /** Velocity in m/s (x = lateral, y = forward/backward) */\n readonly velocity?: { x: number; y: number };\n /** Current speed magnitude in m/s */\n readonly speed?: number;\n}\n\n/**\n * Hook for handling player movement with physics-first approach.\n * All positions and velocities are in METERS - no pixel conversions.\n *\n * **Korean**: 플레이어 이동 훅 (Player Movement Hook)\n *\n * @param config - Physics-first configuration with positions in meters\n * @returns Movement state and physics data (all in meters)\n */\nexport function usePlayerMovement(\n config: InputSystemConfig,\n): PlayerMovementResult {\n const {\n enabled = true,\n bounds,\n onPositionChange,\n initialPositionMeters = { x: 0, y: 0 },\n currentStance = TrigramStance.GEON,\n legInjuryFactor = 0,\n isRunning: isRunningProp = false,\n useTacticalSteps = false,\n maxSpeedOverride,\n accelerationOverride,\n } = config;\n\n // Position in METERS (x = lateral position, y = forward/backward position)\n const [playerPosition, setPlayerPosition] = useState<Position>(\n initialPositionMeters,\n );\n const [keyState, setKeyState] = useState({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n // Physics state for render (velocity and speed in m/s)\n const [velocity, setVelocity] = useState<\n { x: number; y: number } | undefined\n >(undefined);\n const [speed, setSpeed] = useState<number | undefined>(undefined);\n\n // Auto-run detection: track how long movement keys have been held\n // After sustained movement, automatically transition from walking to running\n const movementStartTimeRef = useRef<number | null>(null);\n const AUTO_RUN_THRESHOLD_MS = 300; // Transition to run after 300ms of sustained movement\n\n // Physics-based movement state (always initialized for realistic combat)\n const physicsEngineRef = useRef<MovementPhysics | null>(null);\n const physicsStateRef = useRef<{\n position: THREE.Vector3;\n velocity: THREE.Vector3;\n acceleration: number;\n maxSpeed: number;\n currentStance: TrigramStance;\n legInjuryFactor: number;\n } | null>(null);\n\n // Initialize physics engine once on mount (always enabled)\n // All positions are in METERS - no pixel conversion needed\n useEffect(() => {\n if (!physicsEngineRef.current) {\n // Use arena width for physics-aware speed scaling\n // Validate and fall back to default if invalid\n const width = bounds?.worldWidthMeters;\n const arenaWidth =\n width != null && Number.isFinite(width) && width > 0\n ? width\n : DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters;\n physicsEngineRef.current = new MovementPhysics(arenaWidth);\n // Initial position in meters (x = lateral, z = forward/backward)\n physicsStateRef.current = {\n position: new THREE.Vector3(\n initialPositionMeters.x,\n 0,\n initialPositionMeters.y,\n ),\n velocity: new THREE.Vector3(0, 0, 0),\n acceleration: 0,\n maxSpeed: 6.0, // Default to BASE_WALK_SPEED (6.0 m/s for responsive combat)\n currentStance,\n legInjuryFactor: legInjuryFactor ?? 0,\n };\n }\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Compute arena bounds synchronously when bounds dimensions change\n // Uses useMemo to ensure bounds are available immediately (not after effect runs)\n // Falls back to default arena bounds if invalid or missing\n // Depend on the whole `bounds` object so the compiler's inferred property-access\n // dependencies (bounds.worldWidthMeters / bounds.worldDepthMeters) are covered.\n const arenaBoundsResult = useMemo<{\n bounds: MovementArenaBounds | undefined;\n error?: Error;\n }>(() => {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: bounds.worldWidthMeters,\n worldDepthMeters: bounds.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // If validation fails, fall back to default bounds\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }\n\n // Fallback: use default arena bounds to ensure movement stays bounded\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters,\n worldDepthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // Should never happen with default bounds, but handle gracefully\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }, [bounds]);\n\n const arenaBounds = arenaBoundsResult.bounds;\n\n // Log arena bounds calculation errors in an effect (not during render)\n useEffect(() => {\n if (arenaBoundsResult.error) {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n // Custom bounds failed validation\n console.warn(\n \"Failed to calculate arena bounds, using defaults:\",\n arenaBoundsResult.error\n );\n } else {\n // Should never happen with default bounds\n console.error(\n \"Failed to calculate default arena bounds:\",\n arenaBoundsResult.error\n );\n }\n }\n }, [arenaBoundsResult.error, bounds?.worldWidthMeters, bounds?.worldDepthMeters]);\n\n // Update physics engine arena width when bounds change (legacy)\n useEffect(() => {\n if (!physicsEngineRef.current) {\n return;\n }\n\n const width = bounds?.worldWidthMeters;\n if (width == null) {\n return;\n }\n\n // Validate width before applying to physics engine to avoid runtime errors\n if (!Number.isFinite(width) || width <= 0) {\n console.warn(\n \"Ignoring invalid worldWidthMeters when updating arena width:\",\n width,\n );\n return;\n }\n\n try {\n physicsEngineRef.current.setArenaWidth(width);\n } catch (error) {\n console.warn(\"Failed to update physics arena width:\", error);\n }\n }, [bounds?.worldWidthMeters]);\n\n // Track pressed keys for combat system\n const pressedKeys = useRef<Set<string>>(new Set());\n // Use useState lazy initializer for performance.now() to avoid impure function during render\n const [initialTime] = useState(() => performance.now());\n const lastUpdateTime = useRef(initialTime);\n const animationFrameId = useRef<number | null>(null);\n\n // Refs to track last reported position/velocity to avoid useCallback dependency issues\n // This prevents the animation frame from being cancelled every frame due to callback recreation\n const lastReportedPositionRef = useRef<Position>(initialPositionMeters);\n const lastReportedVelocityRef = useRef<{ x: number; y: number } | undefined>(\n undefined,\n );\n const lastReportedSpeedRef = useRef<number | undefined>(undefined);\n\n // Ref to track keyState for physics loop - avoids recreating callback on key changes\n const keyStateRef = useRef({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n\n // Calculate if currently moving\n const isMoving =\n keyState.up || keyState.down || keyState.left || keyState.right;\n\n // Create complete movement state\n const movementState: MovementState = {\n ...keyState,\n position: playerPosition,\n isMoving,\n };\n\n // Key press checker for combat system\n const isKeyPressed = useCallback((key: string): boolean => {\n return pressedKeys.current.has(key);\n }, []);\n\n // Enhanced keyboard event handlers\n const handleKeyDown = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.add(key);\n\n // ✅ FIXED: Add all movement keys including WASD and arrows\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = true;\n setKeyState((prev) => ({ ...prev, up: true }));\n event.preventDefault();\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = true;\n setKeyState((prev) => ({ ...prev, down: true }));\n event.preventDefault();\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = true;\n setKeyState((prev) => ({ ...prev, left: true }));\n event.preventDefault();\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = true;\n setKeyState((prev) => ({ ...prev, right: true }));\n event.preventDefault();\n break;\n }\n },\n [enabled],\n );\n\n const handleKeyUp = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.delete(key);\n\n // ✅ FIXED: Handle key release for all movement keys\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = false;\n setKeyState((prev) => ({ ...prev, up: false }));\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = false;\n setKeyState((prev) => ({ ...prev, down: false }));\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = false;\n setKeyState((prev) => ({ ...prev, left: false }));\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = false;\n setKeyState((prev) => ({ ...prev, right: false }));\n break;\n }\n },\n [enabled],\n );\n\n // ✅ FIXED: Proper movement calculation with correct bounds\n // Use a ref to store the callback to avoid reference before declaration issue\n const updatePositionRef = useRef<(() => void) | null>(null);\n\n const updatePosition = useCallback(() => {\n // Check if any movement keys are pressed using ref (not stale state)\n const keys = keyStateRef.current;\n const isCurrentlyMoving = keys.up || keys.down || keys.left || keys.right;\n\n if (!enabled || !isCurrentlyMoving) {\n animationFrameId.current = null;\n return;\n }\n\n const now = performance.now();\n const deltaTime = Math.min(now - (lastUpdateTime.current ?? now), 50);\n lastUpdateTime.current = now;\n\n if (deltaTime <= 0) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n return;\n }\n\n // Physics-based movement (always enabled for realistic combat)\n if (physicsEngineRef.current && physicsStateRef.current) {\n // Apply speed modifiers if provided by SpeedModifierSystem\n // BUG FIX: Now properly passing maxSpeedOverride to physics engine\n if (maxSpeedOverride !== undefined) {\n physicsEngineRef.current.setMaxSpeed(maxSpeedOverride);\n }\n\n if (accelerationOverride !== undefined) {\n physicsEngineRef.current.setAcceleration(accelerationOverride);\n }\n\n // Convert key state to physics input (using ref to avoid callback recreation)\n // Screen coordinates: UP/W = toward top of screen, DOWN/S = toward bottom\n // Physics Z-axis: negative Z = toward top, positive Z = toward bottom\n const keys = keyStateRef.current;\n const forward = keys.up ? -1 : keys.down ? 1 : 0;\n const lateral = keys.right ? 1 : keys.left ? -1 : 0;\n const isCurrentlyMoving = forward !== 0 || lateral !== 0;\n\n // Auto-run detection: transition to running after sustained movement\n const now = performance.now();\n if (isCurrentlyMoving) {\n movementStartTimeRef.current ??= now;\n } else {\n movementStartTimeRef.current = null;\n }\n\n // Determine if player should be running (auto-run after threshold)\n const movementDuration = movementStartTimeRef.current\n ? now - movementStartTimeRef.current\n : 0;\n const shouldRun =\n isRunningProp || movementDuration > AUTO_RUN_THRESHOLD_MS;\n\n const physicsInput: MovementInput = {\n forward,\n lateral,\n isRunning: shouldRun,\n isMoving: isCurrentlyMoving,\n useTacticalSteps,\n };\n\n // Update physics state\n const state = physicsStateRef.current;\n state.currentStance = currentStance;\n state.legInjuryFactor = legInjuryFactor;\n\n // Clamp delta time to 1/30s (≈33.33ms) to match usePlayerMovement and prevent instability\n const clampedDeltaTimeMs = Math.min(deltaTime, 1000 / 30);\n\n // Use arena bounds computed via useMemo (available synchronously)\n physicsEngineRef.current.updateMovement(\n state,\n physicsInput,\n clampedDeltaTimeMs / 1000,\n arenaBounds, // Use memoized bounds\n );\n\n // Position in meters (x = lateral, y = forward/backward)\n const newPosition = { x: state.position.x, y: state.position.z };\n\n // Velocity in m/s (x = lateral, y = forward/backward)\n const newVelocity = { x: state.velocity.x, y: state.velocity.z };\n const newSpeed = state.velocity.length();\n\n // Use refs for comparison to avoid recreating callback on every frame\n // This prevents the animation frame from being cancelled due to useCallback recreation\n const lastPos = lastReportedPositionRef.current;\n if (newPosition.x !== lastPos.x || newPosition.y !== lastPos.y) {\n lastReportedPositionRef.current = newPosition;\n setPlayerPosition(newPosition);\n onPositionChange?.(newPosition);\n }\n\n // Update velocity and speed if changed (with epsilon tolerance for floating-point stability)\n const EPSILON = 0.001;\n const lastVel = lastReportedVelocityRef.current;\n const velocityChanged =\n !lastVel ||\n Math.abs(lastVel.x - newVelocity.x) > EPSILON ||\n Math.abs(lastVel.y - newVelocity.y) > EPSILON;\n if (velocityChanged) {\n lastReportedVelocityRef.current = newVelocity;\n setVelocity(newVelocity);\n }\n // Initialize speed when undefined, then update only on significant changes\n const lastSpd = lastReportedSpeedRef.current;\n if (lastSpd === undefined || Math.abs(lastSpd - newSpeed) > EPSILON) {\n lastReportedSpeedRef.current = newSpeed;\n setSpeed(newSpeed);\n }\n }\n\n // Continue animation if still moving (check ref, not stale closure)\n const stillMoving =\n keyStateRef.current.up ||\n keyStateRef.current.down ||\n keyStateRef.current.left ||\n keyStateRef.current.right;\n if (stillMoving) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n } else {\n animationFrameId.current = null;\n }\n // NOTE: playerPosition, velocity, speed, keyState, isMoving intentionally excluded from deps\n // Using refs (lastReportedPositionRef, lastReportedVelocityRef, lastReportedSpeedRef, keyStateRef)\n // for comparison to prevent animation frame cancellation on every state update.\n // arenaBounds is computed from bounds and automatically updates when bounds changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n // playerPosition - excluded, using ref\n // keyState - excluded, using keyStateRef\n // isMoving - excluded, using keyStateRef for movement check\n // arenaBounds - excluded, derived from bounds (below)\n bounds,\n onPositionChange,\n currentStance,\n legInjuryFactor,\n isRunningProp,\n useTacticalSteps,\n // velocity - excluded, using ref\n // speed - excluded, using ref\n maxSpeedOverride,\n accelerationOverride,\n ]);\n\n // Keep updatePositionRef in sync via useEffect (not during render)\n useEffect(() => {\n updatePositionRef.current = updatePosition;\n }, [updatePosition]);\n\n // Handle keyboard input\n useEffect(() => {\n if (!enabled) return;\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [enabled, handleKeyDown, handleKeyUp]);\n\n // Start animation loop when movement begins\n useEffect(() => {\n if (isMoving && !animationFrameId.current) {\n lastUpdateTime.current = performance.now();\n // Use ref to avoid dependency on updatePosition callback\n animationFrameId.current = requestAnimationFrame(() => {\n updatePositionRef.current?.();\n });\n } else if (!isMoving && animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n };\n // Only depend on isMoving - updatePositionRef is stable\n }, [isMoving]);\n\n return {\n playerPosition,\n movementState,\n isMoving,\n isKeyPressed,\n velocity,\n speed,\n };\n}\n\nexport interface InputEvent {\n readonly type: \"keydown\" | \"keyup\" | \"click\" | \"touchstart\" | \"touchend\";\n readonly key?: string;\n readonly target?: EventTarget | null;\n readonly timestamp: number;\n}\n\nexport interface CombatInput {\n readonly stanceChange?: TrigramStance;\n readonly attack?: boolean;\n readonly block?: boolean;\n readonly movement?: MovementState;\n readonly timestamp: number;\n}\n\n/**\n * Input system for combat controls\n */\nexport class InputSystem {\n private actionCallbacks = new Map<string, (() => void)[]>();\n private isEnabled = true;\n\n constructor() {\n this.setupEventListeners();\n }\n\n private setupEventListeners() {\n window.addEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.addEventListener(\"keyup\", this.handleKeyUp.bind(this));\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keydown:${key}`);\n this.triggerAction(\"keydown\");\n }\n\n private handleKeyUp(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keyup:${key}`);\n this.triggerAction(\"keyup\");\n }\n\n registerAction(action: string, callback: () => void) {\n if (!this.actionCallbacks.has(action)) {\n this.actionCallbacks.set(action, []);\n }\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.push(callback);\n }\n }\n\n unregisterAction(action: string, callback?: () => void) {\n if (!this.actionCallbacks.has(action)) return;\n\n if (callback) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n const index = callbacks.indexOf(callback);\n if (index > -1) {\n callbacks.splice(index, 1);\n }\n }\n } else {\n this.actionCallbacks.delete(action);\n }\n }\n\n clearActions() {\n this.actionCallbacks.clear();\n }\n\n isActionActive(action: string): boolean {\n return this.actionCallbacks.has(action);\n }\n\n enable() {\n this.isEnabled = true;\n }\n\n disable() {\n this.isEnabled = false;\n }\n\n private triggerAction(action: string) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.forEach((callback) => callback());\n }\n }\n\n destroy() {\n window.removeEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.removeEventListener(\"keyup\", this.handleKeyUp.bind(this));\n this.clearActions();\n }\n}\n\n/**\n * Get stance from keyboard input\n */\nexport function getStanceFromKey(key: string): TrigramStance | null {\n const stanceKey = key as keyof typeof COMBAT_CONTROLS.stanceControls;\n\n if (stanceKey in COMBAT_CONTROLS.stanceControls) {\n return COMBAT_CONTROLS.stanceControls[stanceKey].stance;\n }\n\n return null;\n}\n\n/**\n * Process combat input and return structured combat data\n */\nexport function processCombatInput(event: KeyboardEvent): CombatInput | null {\n const key = event.key;\n const timestamp = performance.now();\n\n // Check for stance change (1-8 keys)\n const stance = getStanceFromKey(key);\n if (stance) {\n return {\n stanceChange: stance,\n timestamp,\n };\n }\n\n // Check for combat actions\n switch (key.toLowerCase()) {\n case \" \": // Space for attack\n return {\n attack: true,\n timestamp,\n };\n case \"shift\":\n return {\n block: true,\n timestamp,\n };\n default:\n return null;\n }\n}\n\n/**\n * Hook for combat input handling\n */\nexport function useCombatInput(onCombatInput: (input: CombatInput) => void) {\n const isEnabled = useRef<boolean>(true);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!isEnabled.current) return;\n\n const combatInput = processCombatInput(event);\n if (combatInput) {\n onCombatInput(combatInput);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onCombatInput]);\n\n return {\n enable: () => {\n isEnabled.current = true;\n },\n disable: () => {\n isEnabled.current = false;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"inputSystem.js","names":[],"sources":["../../src/utils/inputSystem.ts"],"sourcesContent":["import { COMBAT_CONTROLS } from \"@/systems/types\";\nimport type { Position } from \"@/types/common\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport type { MovementInput } from \"../systems/physics/MovementPhysics\";\nimport { MovementPhysics } from \"../systems/physics/MovementPhysics\";\nimport { TrigramStance } from \"../types/common\";\nimport { calculateArenaBounds, DEFAULT_PHYSICS_ARENA_BOUNDS } from \"../types/PhysicsTypes\";\nimport type { MovementArenaBounds } from \"../types/PhysicsTypes\";\n\n/**\n * Configuration interface for the input system and player movement.\n * Uses physics-first approach: all positions and velocities are in meters.\n *\n * **Korean**: 입력 시스템 설정 (Input System Configuration)\n *\n * ## Physics-First Architecture\n *\n * This interface requires worldWidthMeters and worldDepthMeters to enable\n * the new physics-first coordinate system. Without these properties, the\n * movement system cannot properly convert between physics (meters) and\n * rendering (pixels).\n *\n * ### Migration Guide\n *\n * Existing code must be updated to pass world dimensions:\n *\n * ```typescript\n * // Before (incorrect):\n * const config = { bounds: { x: 0, y: 0, width: 960, height: 480 } };\n *\n * // After (correct):\n * const config = {\n * bounds: {\n * worldWidthMeters: 10, // From layout hook\n * worldDepthMeters: 10 // From layout hook\n * }\n * };\n * ```\n *\n * ### Fallback Behavior\n *\n * If worldWidthMeters/worldDepthMeters are not provided, the system falls back\n * to DEFAULT_PHYSICS_ARENA_BOUNDS (10m × 7.5m) to ensure movement stays bounded.\n * Callers SHOULD provide these values from their layout hooks (useCombatLayout, \n * useTrainingLayout) for proper arena sizing.\n */\nexport interface InputSystemConfig {\n /** Whether the input system is enabled and processing input */\n readonly enabled?: boolean;\n\n /**\n * Arena world dimensions in meters for physics calculations.\n *\n * **REQUIRED for physics-first coordinate system to work.**\n *\n * These values must come from layout hooks:\n * - CombatScreen3D: Use arenaBounds.worldWidthMeters/worldDepthMeters from useCombatLayout()\n * - TrainingScreen3D: Use trainingAreaBounds.worldWidthMeters/worldDepthMeters from useTrainingLayout()\n */\n readonly bounds?: {\n /** Physical arena width in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldWidthMeters: number;\n /** Physical arena depth in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldDepthMeters: number;\n };\n\n /** Callback invoked when player position changes (position in meters) */\n readonly onPositionChange?: (position: Position) => void;\n\n /** Initial player position in METERS (x = lateral, y = forward/backward) */\n readonly initialPositionMeters?: Position;\n\n // Physics-based movement parameters (always enabled)\n /** Current trigram stance affecting movement speed */\n readonly currentStance?: TrigramStance;\n\n /** Leg injury factor (0-1, where 1 is fully injured) affecting movement speed */\n readonly legInjuryFactor?: number;\n\n /** Whether player is running (sprint mode) */\n readonly isRunning?: boolean;\n\n /** Whether to use tactical step mode (30cm grid quantization) */\n readonly useTacticalSteps?: boolean;\n\n // Speed modifier overrides from SpeedModifierSystem\n /** Final calculated maximum speed in meters per second */\n readonly maxSpeedOverride?: number;\n\n /** Final calculated acceleration in meters per second squared */\n readonly accelerationOverride?: number;\n}\n\nexport interface MovementState {\n readonly up: boolean;\n readonly down: boolean;\n readonly left: boolean;\n readonly right: boolean;\n readonly position: Position;\n readonly isMoving: boolean; // Add isMoving to movement state\n}\n\nexport interface PlayerMovementResult {\n /** Player position in METERS (x = lateral, y = forward/backward in arena) */\n readonly playerPosition: Position;\n readonly movementState: MovementState;\n readonly isMoving: boolean;\n readonly isKeyPressed: (key: string) => boolean;\n /** Velocity in m/s (x = lateral, y = forward/backward) */\n readonly velocity?: { x: number; y: number };\n /** Current speed magnitude in m/s */\n readonly speed?: number;\n}\n\n/**\n * Hook for handling player movement with physics-first approach.\n * All positions and velocities are in METERS - no pixel conversions.\n *\n * **Korean**: 플레이어 이동 훅 (Player Movement Hook)\n *\n * @param config - Physics-first configuration with positions in meters\n * @returns Movement state and physics data (all in meters)\n */\nexport function usePlayerMovement(\n config: InputSystemConfig,\n): PlayerMovementResult {\n const {\n enabled = true,\n bounds,\n onPositionChange,\n initialPositionMeters = { x: 0, y: 0 },\n currentStance = TrigramStance.GEON,\n legInjuryFactor = 0,\n isRunning: isRunningProp = false,\n useTacticalSteps = false,\n maxSpeedOverride,\n accelerationOverride,\n } = config;\n\n // Position in METERS (x = lateral position, y = forward/backward position)\n const [playerPosition, setPlayerPosition] = useState<Position>(\n initialPositionMeters,\n );\n const [keyState, setKeyState] = useState({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n // Physics state for render (velocity and speed in m/s)\n const [velocity, setVelocity] = useState<\n { x: number; y: number } | undefined\n >(undefined);\n const [speed, setSpeed] = useState<number | undefined>(undefined);\n\n // Auto-run detection: track how long movement keys have been held\n // After sustained movement, automatically transition from walking to running\n const movementStartTimeRef = useRef<number | null>(null);\n const AUTO_RUN_THRESHOLD_MS = 300; // Transition to run after 300ms of sustained movement\n\n // Physics-based movement state (always initialized for realistic combat)\n const physicsEngineRef = useRef<MovementPhysics | null>(null);\n const physicsStateRef = useRef<{\n position: THREE.Vector3;\n velocity: THREE.Vector3;\n acceleration: number;\n maxSpeed: number;\n currentStance: TrigramStance;\n legInjuryFactor: number;\n } | null>(null);\n\n // Initialize physics engine once on mount (always enabled)\n // All positions are in METERS - no pixel conversion needed\n useEffect(() => {\n if (!physicsEngineRef.current) {\n // Use arena width for physics-aware speed scaling\n // Validate and fall back to default if invalid\n const width = bounds?.worldWidthMeters;\n const arenaWidth =\n width != null && Number.isFinite(width) && width > 0\n ? width\n : DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters;\n physicsEngineRef.current = new MovementPhysics(arenaWidth);\n // Initial position in meters (x = lateral, z = forward/backward)\n physicsStateRef.current = {\n position: new THREE.Vector3(\n initialPositionMeters.x,\n 0,\n initialPositionMeters.y,\n ),\n velocity: new THREE.Vector3(0, 0, 0),\n acceleration: 0,\n maxSpeed: 6.0, // Default to BASE_WALK_SPEED (6.0 m/s for responsive combat)\n currentStance,\n legInjuryFactor: legInjuryFactor ?? 0,\n };\n }\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Compute arena bounds synchronously when bounds dimensions change\n // Uses useMemo to ensure bounds are available immediately (not after effect runs)\n // Falls back to default arena bounds if invalid or missing\n // Depend on the whole `bounds` object so the compiler's inferred property-access\n // dependencies (bounds.worldWidthMeters / bounds.worldDepthMeters) are covered.\n const arenaBoundsResult = useMemo<{\n bounds: MovementArenaBounds | undefined;\n error?: Error;\n }>(() => {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: bounds.worldWidthMeters,\n worldDepthMeters: bounds.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // If validation fails, fall back to default bounds\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }\n\n // Fallback: use default arena bounds to ensure movement stays bounded\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters,\n worldDepthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // Should never happen with default bounds, but handle gracefully\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }, [bounds]);\n\n const arenaBounds = arenaBoundsResult.bounds;\n\n // Log arena bounds calculation errors in an effect (not during render)\n useEffect(() => {\n if (arenaBoundsResult.error) {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n // Custom bounds failed validation\n console.warn(\n \"Failed to calculate arena bounds, using defaults:\",\n arenaBoundsResult.error\n );\n } else {\n // Should never happen with default bounds\n console.error(\n \"Failed to calculate default arena bounds:\",\n arenaBoundsResult.error\n );\n }\n }\n }, [arenaBoundsResult.error, bounds?.worldWidthMeters, bounds?.worldDepthMeters]);\n\n // Update physics engine arena width when bounds change (legacy)\n useEffect(() => {\n if (!physicsEngineRef.current) {\n return;\n }\n\n const width = bounds?.worldWidthMeters;\n if (width == null) {\n return;\n }\n\n // Validate width before applying to physics engine to avoid runtime errors\n if (!Number.isFinite(width) || width <= 0) {\n console.warn(\n \"Ignoring invalid worldWidthMeters when updating arena width:\",\n width,\n );\n return;\n }\n\n try {\n physicsEngineRef.current.setArenaWidth(width);\n } catch (error) {\n console.warn(\"Failed to update physics arena width:\", error);\n }\n }, [bounds?.worldWidthMeters]);\n\n // Track pressed keys for combat system\n const pressedKeys = useRef<Set<string>>(new Set());\n // Use useState lazy initializer for performance.now() to avoid impure function during render\n const [initialTime] = useState(() => performance.now());\n const lastUpdateTime = useRef(initialTime);\n const animationFrameId = useRef<number | null>(null);\n\n // Refs to track last reported position/velocity to avoid useCallback dependency issues\n // This prevents the animation frame from being cancelled every frame due to callback recreation\n const lastReportedPositionRef = useRef<Position>(initialPositionMeters);\n const lastReportedVelocityRef = useRef<{ x: number; y: number } | undefined>(\n undefined,\n );\n const lastReportedSpeedRef = useRef<number | undefined>(undefined);\n\n // Ref to track keyState for physics loop - avoids recreating callback on key changes\n const keyStateRef = useRef({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n\n // Calculate if currently moving\n const isMoving =\n keyState.up || keyState.down || keyState.left || keyState.right;\n\n // Create complete movement state\n const movementState: MovementState = {\n ...keyState,\n position: playerPosition,\n isMoving,\n };\n\n // Key press checker for combat system\n const isKeyPressed = useCallback((key: string): boolean => {\n return pressedKeys.current.has(key);\n }, []);\n\n // Enhanced keyboard event handlers\n const handleKeyDown = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.add(key);\n\n // ✅ FIXED: Add all movement keys including WASD and arrows\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = true;\n setKeyState((prev) => ({ ...prev, up: true }));\n event.preventDefault();\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = true;\n setKeyState((prev) => ({ ...prev, down: true }));\n event.preventDefault();\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = true;\n setKeyState((prev) => ({ ...prev, left: true }));\n event.preventDefault();\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = true;\n setKeyState((prev) => ({ ...prev, right: true }));\n event.preventDefault();\n break;\n }\n },\n [enabled],\n );\n\n const handleKeyUp = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.delete(key);\n\n // ✅ FIXED: Handle key release for all movement keys\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = false;\n setKeyState((prev) => ({ ...prev, up: false }));\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = false;\n setKeyState((prev) => ({ ...prev, down: false }));\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = false;\n setKeyState((prev) => ({ ...prev, left: false }));\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = false;\n setKeyState((prev) => ({ ...prev, right: false }));\n break;\n }\n },\n [enabled],\n );\n\n // ✅ FIXED: Proper movement calculation with correct bounds\n // Use a ref to store the callback to avoid reference before declaration issue\n const updatePositionRef = useRef<(() => void) | null>(null);\n\n const updatePosition = useCallback(() => {\n // Check if any movement keys are pressed using ref (not stale state)\n const keys = keyStateRef.current;\n const isCurrentlyMoving = keys.up || keys.down || keys.left || keys.right;\n\n if (!enabled || !isCurrentlyMoving) {\n animationFrameId.current = null;\n return;\n }\n\n const now = performance.now();\n const deltaTime = Math.min(now - (lastUpdateTime.current ?? now), 50);\n lastUpdateTime.current = now;\n\n if (deltaTime <= 0) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n return;\n }\n\n // Physics-based movement (always enabled for realistic combat)\n if (physicsEngineRef.current && physicsStateRef.current) {\n // Apply speed modifiers if provided by SpeedModifierSystem\n // BUG FIX: Now properly passing maxSpeedOverride to physics engine\n if (maxSpeedOverride !== undefined) {\n physicsEngineRef.current.setMaxSpeed(maxSpeedOverride);\n }\n\n if (accelerationOverride !== undefined) {\n physicsEngineRef.current.setAcceleration(accelerationOverride);\n }\n\n // Convert key state to physics input (using ref to avoid callback recreation)\n // Screen coordinates: UP/W = toward top of screen, DOWN/S = toward bottom\n // Physics Z-axis: negative Z = toward top, positive Z = toward bottom\n const keys = keyStateRef.current;\n const forward = keys.up ? -1 : keys.down ? 1 : 0;\n const lateral = keys.right ? 1 : keys.left ? -1 : 0;\n const isCurrentlyMoving = forward !== 0 || lateral !== 0;\n\n // Auto-run detection: transition to running after sustained movement\n const now = performance.now();\n if (isCurrentlyMoving) {\n movementStartTimeRef.current ??= now;\n } else {\n movementStartTimeRef.current = null;\n }\n\n // Determine if player should be running (auto-run after threshold)\n const movementDuration = movementStartTimeRef.current\n ? now - movementStartTimeRef.current\n : 0;\n const shouldRun =\n isRunningProp || movementDuration > AUTO_RUN_THRESHOLD_MS;\n\n const physicsInput: MovementInput = {\n forward,\n lateral,\n isRunning: shouldRun,\n isMoving: isCurrentlyMoving,\n useTacticalSteps,\n };\n\n // Update physics state\n const state = physicsStateRef.current;\n state.currentStance = currentStance;\n state.legInjuryFactor = legInjuryFactor;\n\n // Clamp delta time to 1/30s (≈33.33ms) to match usePlayerMovement and prevent instability\n const clampedDeltaTimeMs = Math.min(deltaTime, 1000 / 30);\n\n // Use arena bounds computed via useMemo (available synchronously)\n physicsEngineRef.current.updateMovement(\n state,\n physicsInput,\n clampedDeltaTimeMs / 1000,\n arenaBounds, // Use memoized bounds\n );\n\n // Position in meters (x = lateral, y = forward/backward)\n const newPosition = { x: state.position.x, y: state.position.z };\n\n // Velocity in m/s (x = lateral, y = forward/backward)\n const newVelocity = { x: state.velocity.x, y: state.velocity.z };\n const newSpeed = state.velocity.length();\n\n // Use refs for comparison to avoid recreating callback on every frame\n // This prevents the animation frame from being cancelled due to useCallback recreation\n const lastPos = lastReportedPositionRef.current;\n if (newPosition.x !== lastPos.x || newPosition.y !== lastPos.y) {\n lastReportedPositionRef.current = newPosition;\n setPlayerPosition(newPosition);\n onPositionChange?.(newPosition);\n }\n\n // Update velocity and speed if changed (with epsilon tolerance for floating-point stability)\n const EPSILON = 0.001;\n const lastVel = lastReportedVelocityRef.current;\n const velocityChanged =\n !lastVel ||\n Math.abs(lastVel.x - newVelocity.x) > EPSILON ||\n Math.abs(lastVel.y - newVelocity.y) > EPSILON;\n if (velocityChanged) {\n lastReportedVelocityRef.current = newVelocity;\n setVelocity(newVelocity);\n }\n // Initialize speed when undefined, then update only on significant changes\n const lastSpd = lastReportedSpeedRef.current;\n if (lastSpd === undefined || Math.abs(lastSpd - newSpeed) > EPSILON) {\n lastReportedSpeedRef.current = newSpeed;\n setSpeed(newSpeed);\n }\n }\n\n // Continue animation if still moving (check ref, not stale closure)\n const stillMoving =\n keyStateRef.current.up ||\n keyStateRef.current.down ||\n keyStateRef.current.left ||\n keyStateRef.current.right;\n if (stillMoving) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n } else {\n animationFrameId.current = null;\n }\n // NOTE: playerPosition, velocity, speed, keyState, isMoving intentionally excluded from deps\n // Using refs (lastReportedPositionRef, lastReportedVelocityRef, lastReportedSpeedRef, keyStateRef)\n // for comparison to prevent animation frame cancellation on every state update.\n // arenaBounds is computed from bounds and automatically updates when bounds changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n // playerPosition - excluded, using ref\n // keyState - excluded, using keyStateRef\n // isMoving - excluded, using keyStateRef for movement check\n // arenaBounds - excluded, derived from bounds (below)\n bounds,\n onPositionChange,\n currentStance,\n legInjuryFactor,\n isRunningProp,\n useTacticalSteps,\n // velocity - excluded, using ref\n // speed - excluded, using ref\n maxSpeedOverride,\n accelerationOverride,\n ]);\n\n // Keep updatePositionRef in sync via useEffect (not during render)\n useEffect(() => {\n updatePositionRef.current = updatePosition;\n }, [updatePosition]);\n\n // Handle keyboard input\n useEffect(() => {\n if (!enabled) return;\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [enabled, handleKeyDown, handleKeyUp]);\n\n // Start animation loop when movement begins\n useEffect(() => {\n if (isMoving && !animationFrameId.current) {\n lastUpdateTime.current = performance.now();\n // Use ref to avoid dependency on updatePosition callback\n animationFrameId.current = requestAnimationFrame(() => {\n updatePositionRef.current?.();\n });\n } else if (!isMoving && animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n };\n // Only depend on isMoving - updatePositionRef is stable\n }, [isMoving]);\n\n return {\n playerPosition,\n movementState,\n isMoving,\n isKeyPressed,\n velocity,\n speed,\n };\n}\n\nexport interface InputEvent {\n readonly type: \"keydown\" | \"keyup\" | \"click\" | \"touchstart\" | \"touchend\";\n readonly key?: string;\n readonly target?: EventTarget | null;\n readonly timestamp: number;\n}\n\nexport interface CombatInput {\n readonly stanceChange?: TrigramStance;\n readonly attack?: boolean;\n readonly block?: boolean;\n readonly movement?: MovementState;\n readonly timestamp: number;\n}\n\n/**\n * Input system for combat controls\n */\nexport class InputSystem {\n private actionCallbacks = new Map<string, (() => void)[]>();\n private isEnabled = true;\n\n constructor() {\n this.setupEventListeners();\n }\n\n private setupEventListeners() {\n window.addEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.addEventListener(\"keyup\", this.handleKeyUp.bind(this));\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keydown:${key}`);\n this.triggerAction(\"keydown\");\n }\n\n private handleKeyUp(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keyup:${key}`);\n this.triggerAction(\"keyup\");\n }\n\n registerAction(action: string, callback: () => void) {\n if (!this.actionCallbacks.has(action)) {\n this.actionCallbacks.set(action, []);\n }\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.push(callback);\n }\n }\n\n unregisterAction(action: string, callback?: () => void) {\n if (!this.actionCallbacks.has(action)) return;\n\n if (callback) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n const index = callbacks.indexOf(callback);\n if (index > -1) {\n callbacks.splice(index, 1);\n }\n }\n } else {\n this.actionCallbacks.delete(action);\n }\n }\n\n clearActions() {\n this.actionCallbacks.clear();\n }\n\n isActionActive(action: string): boolean {\n return this.actionCallbacks.has(action);\n }\n\n enable() {\n this.isEnabled = true;\n }\n\n disable() {\n this.isEnabled = false;\n }\n\n private triggerAction(action: string) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.forEach((callback) => callback());\n }\n }\n\n destroy() {\n window.removeEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.removeEventListener(\"keyup\", this.handleKeyUp.bind(this));\n this.clearActions();\n }\n}\n\n/**\n * Get stance from keyboard input\n */\nexport function getStanceFromKey(key: string): TrigramStance | null {\n const stanceKey = key as keyof typeof COMBAT_CONTROLS.stanceControls;\n\n if (stanceKey in COMBAT_CONTROLS.stanceControls) {\n return COMBAT_CONTROLS.stanceControls[stanceKey].stance;\n }\n\n return null;\n}\n\n/**\n * Process combat input and return structured combat data\n */\nexport function processCombatInput(event: KeyboardEvent): CombatInput | null {\n const key = event.key;\n const timestamp = performance.now();\n\n // Check for stance change (1-8 keys)\n const stance = getStanceFromKey(key);\n if (stance) {\n return {\n stanceChange: stance,\n timestamp,\n };\n }\n\n // Check for combat actions\n switch (key.toLowerCase()) {\n case \" \": // Space for attack\n return {\n attack: true,\n timestamp,\n };\n case \"shift\":\n return {\n block: true,\n timestamp,\n };\n default:\n return null;\n }\n}\n\n/**\n * Hook for combat input handling\n */\nexport function useCombatInput(onCombatInput: (input: CombatInput) => void) {\n const isEnabled = useRef<boolean>(true);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!isEnabled.current) return;\n\n const combatInput = processCombatInput(event);\n if (combatInput) {\n onCombatInput(combatInput);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onCombatInput]);\n\n return {\n enable: () => {\n isEnabled.current = true;\n },\n disable: () => {\n isEnabled.current = false;\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"koreanThemeHelpers.js","names":[],"sources":["../../src/utils/koreanThemeHelpers.ts"],"sourcesContent":["/**\n * Korean Theme Helper Utilities for HTML Overlays\n *\n * Provides consistent Korean martial arts cyberpunk theming across all HTML overlay components.\n * These utilities ensure color consistency, bilingual text formatting, and responsive spacing.\n *\n * @module utils/koreanThemeHelpers\n * @category UI Utilities\n * @korean 한국테마도우미\n */\n\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { SPACING, BORDER_RADIUS } from \"../types/constants/ui\";\nimport { hexToRgbaString } from \"./colorUtils\";\nimport {\n getNeonGlowEffect,\n getNeonTextShadow,\n getLayeredDepthEffect,\n getCyberpunkGradient,\n getSmoothTransition,\n getKoreanFontOptimization,\n getHoverStateStyles,\n getFocusStateStyles,\n getBackdropBlurEffect,\n getTrigramSymbolGlow as getTrigramGlowEffect,\n combineShadowEffects,\n type GlowIntensity,\n type HoverAnimationType,\n} from \"./visualEffects\";\n\n/**\n * Bilingual text format options\n * @korean 이중언어형식\n */\nexport type BilingualFormat = \"pipe\" | \"parentheses\" | \"bracket\" | \"slash\";\n\n/**\n * Button variant types for Korean theme\n * @korean 버튼변형\n */\nexport type KoreanButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"danger\"\n | \"success\"\n | \"warning\";\n\n/**\n * Responsive spacing size\n * @korean 반응형간격크기\n */\nexport type SpacingSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"xxl\";\n\n/**\n * Enhanced overlay styles configuration\n * @korean 향상된오버레이스타일설정\n */\nexport interface EnhancedOverlayConfig {\n readonly opacity?: number;\n readonly glowIntensity?: GlowIntensity;\n readonly includeGradient?: boolean;\n readonly includeBackdropBlur?: boolean;\n readonly depthLayers?: number;\n}\n\n/**\n * Base styles for all Korean-themed overlays\n *\n * Provides consistent dark background with cyan/gold accents\n *\n * @param opacity - Background opacity (0-1), default 0.9\n * @returns React.CSSProperties object with Korean theme\n *\n * @example\n * ```tsx\n * <div style={getKoreanOverlayBaseStyles(0.95)}>\n * Content\n * </div>\n * ```\n *\n * @korean 한국오버레이기본스타일얻기\n */\nexport function getKoreanOverlayBaseStyles(\n opacity: number = 0.9,\n): React.CSSProperties {\n return {\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, opacity),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n borderRadius: `${BORDER_RADIUS.MD}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 4px 20px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean overlay styles with visual effects\n *\n * Provides advanced cyberpunk styling with neon glow, depth effects,\n * gradients, and backdrop blur for Korean-themed overlays.\n *\n * @param config - Enhanced overlay configuration\n * @returns React.CSSProperties with advanced visual effects\n *\n * @example\n * ```tsx\n * const styles = getEnhancedKoreanOverlayStyles({\n * opacity: 0.95,\n * glowIntensity: 'medium',\n * includeGradient: true,\n * includeBackdropBlur: true,\n * depthLayers: 3,\n * });\n * <div style={styles}>Enhanced Content</div>\n * ```\n *\n * @korean 향상된한국오버레이스타일얻기\n */\nexport function getEnhancedKoreanOverlayStyles(\n config: EnhancedOverlayConfig = {},\n): React.CSSProperties {\n const {\n opacity = 0.9,\n glowIntensity = \"medium\",\n includeGradient = false,\n includeBackdropBlur = false,\n depthLayers = 2,\n } = config;\n\n // Base styles\n const baseStyles = getKoreanOverlayBaseStyles(opacity);\n\n // Neon glow effect\n const neonGlow = getNeonGlowEffect(\n KOREAN_COLORS.PRIMARY_CYAN,\n glowIntensity,\n true,\n );\n\n // Depth effect\n const depthShadow = getLayeredDepthEffect({\n layers: depthLayers,\n baseOffset: 2,\n baseBlur: 4,\n color: KOREAN_COLORS.BLACK_SOLID,\n opacity: 0.5,\n });\n\n // Combine shadows\n const boxShadow = combineShadowEffects([neonGlow, depthShadow]);\n\n // Optional gradient background\n let background = baseStyles.backgroundColor;\n if (includeGradient) {\n const gradient = getCyberpunkGradient(\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n 135,\n );\n background = `${gradient}, ${background}`;\n }\n\n // Optional backdrop blur\n const backdropStyles = includeBackdropBlur\n ? getBackdropBlurEffect(10, 1.5)\n : {};\n\n // Combine all styles\n return {\n ...baseStyles,\n ...backdropStyles,\n background,\n boxShadow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n ...getKoreanFontOptimization(16, \"normal\"),\n };\n}\n\n/**\n * Format bilingual text with Korean and English\n *\n * Supports multiple formatting styles:\n * - pipe: \"한글 | English\"\n * - parentheses: \"한글 (English)\"\n * - bracket: \"한글 [English]\"\n * - slash: \"한글 / English\"\n *\n * @param korean - Korean text\n * @param english - English text\n * @param format - Format style, default \"pipe\"\n * @returns Formatted bilingual string\n *\n * @example\n * ```tsx\n * formatBilingualText('공격', 'Attack', 'pipe') // \"공격 | Attack\"\n * formatBilingualText('방어', 'Defense', 'parentheses') // \"방어 (Defense)\"\n * ```\n *\n * @korean 이중언어텍스트형식화\n */\nexport function formatBilingualText(\n korean: string,\n english: string,\n format: BilingualFormat = \"pipe\",\n): string {\n switch (format) {\n case \"pipe\":\n return `${korean} | ${english}`;\n case \"parentheses\":\n return `${korean} (${english})`;\n case \"bracket\":\n return `${korean} [${english}]`;\n case \"slash\":\n return `${korean} / ${english}`;\n default:\n return `${korean} | ${english}`;\n }\n}\n\n/**\n * Get Korean button styles with variant support\n *\n * Returns consistent button styling based on variant:\n * - primary: Cyan border, gold text\n * - secondary: Gold border, white text\n * - danger: Red border, red text\n * - success: Green border, green text\n * - warning: Orange border, orange text\n *\n * @param variant - Button variant type\n * @param isHovered - Whether button is hovered\n * @param isPressed - Whether button is pressed\n * @returns React.CSSProperties for button\n *\n * @example\n * ```tsx\n * <button style={getKoreanButtonStyles('primary', isHovered, isPressed)}>\n * {formatBilingualText('확인', 'Confirm')}\n * </button>\n * ```\n *\n * @korean 한국버튼스타일얻기\n */\nexport function getKoreanButtonStyles(\n variant: KoreanButtonVariant = \"primary\",\n isHovered: boolean = false,\n isPressed: boolean = false,\n): React.CSSProperties {\n // Variant-specific colors\n const variantColors = {\n primary: {\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: KOREAN_COLORS.PRIMARY_CYAN,\n },\n secondary: {\n border: KOREAN_COLORS.ACCENT_GOLD,\n text: KOREAN_COLORS.TEXT_PRIMARY,\n hoverBg: KOREAN_COLORS.ACCENT_GOLD,\n },\n danger: {\n border: KOREAN_COLORS.ACCENT_RED,\n text: KOREAN_COLORS.ACCENT_RED,\n hoverBg: KOREAN_COLORS.ACCENT_RED,\n },\n success: {\n border: KOREAN_COLORS.ACCENT_GREEN,\n text: KOREAN_COLORS.ACCENT_GREEN,\n hoverBg: KOREAN_COLORS.ACCENT_GREEN,\n },\n warning: {\n border: KOREAN_COLORS.WARNING_ORANGE,\n text: KOREAN_COLORS.WARNING_ORANGE,\n hoverBg: KOREAN_COLORS.WARNING_ORANGE,\n },\n };\n\n const colors = variantColors[variant];\n\n let backgroundColor = hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 0.9,\n );\n let borderColor = hexToRgbaString(colors.border, 0.8);\n const textColor = hexToRgbaString(colors.text);\n let boxShadow = \"none\";\n let transform = \"scale(1)\";\n\n if (isPressed) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.2);\n transform = \"scale(0.98)\";\n } else if (isHovered) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.1);\n borderColor = hexToRgbaString(colors.border, 1.0);\n boxShadow = `0 0 10px ${hexToRgbaString(colors.border, 0.5)}`;\n }\n\n return {\n backgroundColor,\n border: `2px solid ${borderColor}`,\n borderRadius: `${BORDER_RADIUS.SM}px`,\n color: textColor,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n padding: `${SPACING.SM}px ${SPACING.MD}px`,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n boxShadow,\n transform,\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean button configuration\n * @korean 향상된한국버튼설정\n */\nexport interface EnhancedButtonConfig {\n readonly variant?: KoreanButtonVariant;\n readonly isHovered?: boolean;\n readonly isPressed?: boolean;\n readonly isFocused?: boolean;\n readonly glowIntensity?: GlowIntensity;\n readonly hoverAnimation?: HoverAnimationType;\n}\n\n/**\n * Get enhanced Korean button styles with neon glow\n *\n * Provides advanced button styling with cyberpunk neon effects,\n * smooth transitions, and Korean font optimization.\n *\n * @param config - Enhanced button configuration\n * @returns React.CSSProperties with neon glow effects\n *\n * @example\n * ```tsx\n * const buttonStyle = getKoreanButtonWithGlow({\n * variant: 'primary',\n * isHovered: true,\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined',\n * });\n * <button style={buttonStyle}>\n * {formatBilingualText('공격', 'Attack')}\n * </button>\n * ```\n *\n * @korean 네온글로우한국버튼스타일얻기\n */\nexport function getKoreanButtonWithGlow(\n config: EnhancedButtonConfig = {},\n): React.CSSProperties {\n const {\n variant = \"primary\",\n isHovered = false,\n isPressed = false,\n isFocused = false,\n glowIntensity = \"medium\",\n hoverAnimation = \"combined\",\n } = config;\n\n // Get base button styles\n const baseStyles = getKoreanButtonStyles(variant, false, isPressed);\n\n // Variant-specific glow colors\n const glowColors = {\n primary: KOREAN_COLORS.PRIMARY_CYAN,\n secondary: KOREAN_COLORS.ACCENT_GOLD,\n danger: KOREAN_COLORS.ACCENT_RED,\n success: KOREAN_COLORS.ACCENT_GREEN,\n warning: KOREAN_COLORS.WARNING_ORANGE,\n };\n\n const glowColor = glowColors[variant];\n\n // Hover state with visual effects\n let hoverStyles: React.CSSProperties = {};\n if (isHovered) {\n hoverStyles = getHoverStateStyles(glowColor, hoverAnimation, glowIntensity);\n }\n\n // Focus state\n let focusStyles: React.CSSProperties = {};\n if (isFocused) {\n focusStyles = getFocusStateStyles(glowColor, true);\n }\n\n // Extract font size from base styles (getKoreanButtonStyles always returns number | string)\n const baseFontSize =\n typeof baseStyles.fontSize === \"number\"\n ? baseStyles.fontSize\n : parseInt(String(baseStyles.fontSize), 10) || 14;\n\n // Neon text glow for button text\n const textGlow = getNeonTextShadow(\n glowColor,\n isHovered ? \"medium\" : \"subtle\",\n );\n\n // Korean font optimization\n const fontOptimization = getKoreanFontOptimization(baseFontSize, \"bold\");\n\n return {\n ...baseStyles,\n ...fontOptimization,\n ...hoverStyles,\n ...focusStyles,\n textShadow: textGlow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n };\n}\n\n/**\n * Get button visual effects only (no color/background/border)\n *\n * Extracts only visual effects (boxShadow, transition, transform, textShadow) from\n * getKoreanButtonWithGlow without color-related properties. This is useful for cases\n * where buttons need custom colors (e.g., menu selection states) but want to preserve\n * the visual effects system.\n *\n * This function provides an explicit contract for visual effects extraction, preventing\n * fragile coupling that could occur with destructuring patterns. If getKoreanButtonWithGlow\n * adds new visual effect properties in the future, they should be explicitly added here.\n *\n * **Intentionally Excluded Properties:**\n * - Layout: fontSize, fontFamily, fontWeight, padding, width, height, cursor\n * - Typography: letterSpacing, lineHeight, textRendering, WebkitFontSmoothing, MozOsxFontSmoothing\n * - Colors: color, background, backgroundColor, border, borderColor\n *\n * **Included Visual Effects:**\n * - boxShadow: Neon glow effects\n * - transition: Smooth state changes (0.2s ease)\n * - transform: Hover/press animations (scale)\n * - textShadow: Text glow effects\n *\n * **Note:** If getKoreanButtonWithGlow adds new visual effect properties (e.g., 'filter',\n * 'opacity', 'willChange', 'backdropFilter'), they must be explicitly added to this function's\n * return type and extraction logic to maintain the explicit contract.\n *\n * @param config - Same configuration as getKoreanButtonWithGlow\n * @returns React.CSSProperties with only visual effects (boxShadow, transition, transform, textShadow)\n *\n * @example\n * ```tsx\n * const visualEffects = getButtonVisualEffectsOnly({\n * variant: 'primary',\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined'\n * });\n *\n * <button style={{\n * ...visualEffects,\n * color: customColor, // Apply custom colors\n * background: customBg,\n * border: customBorder\n * }}>\n * Custom Button\n * </button>\n * ```\n *\n * @korean 버튼시각효과만얻기\n */\nexport function getButtonVisualEffectsOnly(\n config: Parameters<typeof getKoreanButtonWithGlow>[0],\n): Pick<\n React.CSSProperties,\n \"boxShadow\" | \"transition\" | \"transform\" | \"textShadow\"\n> {\n const fullStyles = getKoreanButtonWithGlow(config);\n\n // Explicitly extract only visual effect properties\n // Note: This explicitly excludes layout, typography, and color properties.\n // If new visual effects (filter, opacity, willChange, etc.) are added to\n // getKoreanButtonWithGlow, they must be added here to maintain the contract.\n return {\n boxShadow: fullStyles.boxShadow,\n transition: fullStyles.transition,\n transform: fullStyles.transform,\n textShadow: fullStyles.textShadow,\n };\n}\n\n/**\n * Get responsive spacing value\n *\n * Returns SPACING constant value for consistent spacing across components.\n * Optionally scales for mobile devices.\n *\n * **IMPORTANT**: This function accepts lowercase size parameters ('xs', 'sm', 'md', etc.)\n * to provide a more intuitive API, then internally converts to uppercase to match\n * SPACING constant keys ('XS', 'SM', 'MD', etc.). This design choice prioritizes\n * developer experience while maintaining compatibility with the SPACING constants.\n *\n * @param size - Spacing size constant ('xs', 'sm', 'md', 'lg', 'xl', 'xxl')\n * @param isMobile - Whether to apply mobile scaling (87.5% for mobile devices)\n * @returns Spacing value in pixels\n *\n * @example\n * ```tsx\n * const padding = getResponsiveSpacing('md', isMobile); // 16px desktop, 14px mobile\n * <div style={{ padding: `${padding}px` }}>Content</div>\n * ```\n *\n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n size: SpacingSize,\n isMobile: boolean = false,\n): number {\n // Convert lowercase API parameter to uppercase SPACING constant key\n // This provides a more ergonomic API while maintaining internal consistency\n const spacingKey = size.toUpperCase() as keyof typeof SPACING;\n const spacingValue = SPACING[spacingKey];\n const mobileScale = 0.875; // 87.5% for mobile\n\n // Runtime validation: While TypeScript prevents invalid sizes at compile time,\n // this check provides safety for JavaScript consumers and edge cases where\n // type assertions bypass TypeScript checks (e.g., 'as any', dynamic values)\n if (spacingValue === undefined) {\n const fallback = SPACING.MD;\n console.warn(\n `[koreanThemeHelpers:getResponsiveSpacing] Invalid spacing size \"${String(\n size,\n )}\" provided. Falling back to \"MD\".`,\n );\n return isMobile ? Math.round(fallback * mobileScale) : fallback;\n }\n\n return isMobile ? Math.round(spacingValue * mobileScale) : spacingValue;\n}\n\n/**\n * Get trigram symbol by name\n *\n * Returns Unicode trigram symbol for visual embellishment\n *\n * @param name - Trigram name in Korean\n * @returns Unicode trigram symbol\n *\n * @example\n * ```tsx\n * <div>{getTrigramSymbol('건')} 건 (Heaven)</div>\n * ```\n *\n * @korean 팔괘기호얻기\n */\nexport function getTrigramSymbol(\n name: \"건\" | \"태\" | \"리\" | \"진\" | \"손\" | \"감\" | \"간\" | \"곤\",\n): string {\n const symbols = {\n 건: \"☰\", // Heaven - 乾\n 태: \"☱\", // Lake - 兌\n 리: \"☲\", // Fire - 離\n 진: \"☳\", // Thunder - 震\n 손: \"☴\", // Wind - 巽\n 감: \"☵\", // Water - 坎\n 간: \"☶\", // Mountain - 艮\n 곤: \"☷\", // Earth - 坤\n };\n return symbols[name];\n}\n\n/**\n * Get trigram symbol styles with glow effect\n *\n * Enhances trigram symbols (☰☱☲☳☴☵☶☷) with cyberpunk neon glow\n * based on stance-specific colors and active state.\n *\n * @param config - Configuration object\n * @param config.stance - Trigram stance identifier (\"geon\", \"tae\", etc.)\n * @param config.isActive - Whether the trigram stance is currently active\n * @param config.size - Font size in pixels (optional, defaults based on active state)\n * @returns React.CSSProperties with trigram-specific glow\n *\n * @example\n * ```tsx\n * const trigramStyle = getTrigramSymbolWithGlow({ stance: 'geon', isActive: true });\n * <div style={trigramStyle}>\n * ☰ 건 | Geon\n * </div>\n * ```\n *\n * @korean 팔괘기호네온글로우스타일얻기\n */\nexport function getTrigramSymbolWithGlow(config: {\n readonly stance:\n | \"geon\"\n | \"tae\"\n | \"li\"\n | \"jin\"\n | \"son\"\n | \"gam\"\n | \"gan\"\n | \"gon\";\n readonly isActive?: boolean;\n readonly size?: number;\n}): React.CSSProperties & {\n WebkitUserSelect?: string;\n} {\n const { stance, isActive = false, size } = config;\n\n // Map English stance names to Korean\n const stanceToKorean = {\n geon: \"건\",\n tae: \"태\",\n li: \"리\",\n jin: \"진\",\n son: \"손\",\n gam: \"감\",\n gan: \"간\",\n gon: \"곤\",\n };\n\n const koreanName = stanceToKorean[stance];\n\n // Trigram-specific colors matching KOREAN_COLORS\n // Note: Comments reflect actual hex color values\n // TODO: Fix source constants (colors.ts lines 84, 87) - TRIGRAM_GEON_PRIMARY should say \"Gold\" not \"White\",\n // TRIGRAM_JIN_PRIMARY should say \"Medium Purple\" not \"Yellow\"\n const trigramColors: Record<string, number> = {\n 건: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY, // Heaven - Gold (0xffd700)\n 태: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY, // Lake - Sky Blue\n 리: KOREAN_COLORS.TRIGRAM_LI_PRIMARY, // Fire - Orange Red\n 진: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY, // Thunder - Medium Purple (0x9370db)\n 손: KOREAN_COLORS.TRIGRAM_SON_PRIMARY, // Wind - Light Green\n 감: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY, // Water - Blue\n 간: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY, // Mountain - Brown\n 곤: KOREAN_COLORS.TRIGRAM_GON_PRIMARY, // Earth - Dark Gray\n };\n\n const trigramColor = trigramColors[koreanName];\n\n // Get glow effect from visualEffects\n const glowStyles = getTrigramGlowEffect(trigramColor, isActive);\n\n // Korean font optimization for trigram symbols\n const fontSize = size ?? (isActive ? 32 : 28);\n const fontStyles = getKoreanFontOptimization(fontSize, \"bold\");\n\n return {\n ...fontStyles,\n ...glowStyles,\n fontFamily: FONT_FAMILY.KOREAN,\n display: \"inline-block\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n}\n\n/**\n * Get Korean color name for documentation\n *\n * Maps hex color to Korean color name for better documentation\n *\n * @param hexColor - Hex color value from KOREAN_COLORS\n * @returns Korean name and English translation\n *\n * @internal Used primarily for JSDoc documentation\n * @korean 한국색상이름얻기\n */\nexport function getKoreanColorName(hexColor: number): {\n korean: string;\n english: string;\n} {\n const colorNames: Record<number, { korean: string; english: string }> = {\n [KOREAN_COLORS.PRIMARY_CYAN]: { korean: \"청록\", english: \"Cyan\" },\n [KOREAN_COLORS.ACCENT_GOLD]: { korean: \"금색\", english: \"Gold\" },\n [KOREAN_COLORS.ACCENT_RED]: { korean: \"빨강\", english: \"Red\" },\n [KOREAN_COLORS.ACCENT_GREEN]: { korean: \"초록\", english: \"Green\" },\n [KOREAN_COLORS.WARNING_ORANGE]: { korean: \"주황\", english: \"Orange\" },\n [KOREAN_COLORS.TEXT_PRIMARY]: { korean: \"흰색\", english: \"White\" },\n [KOREAN_COLORS.UI_BACKGROUND_DARK]: {\n korean: \"어두운배경\",\n english: \"Dark Background\",\n },\n };\n\n return colorNames[hexColor] ?? { korean: \"알수없음\", english: \"Unknown\" };\n}\n\n/**\n * Format stat row with bilingual labels\n *\n * Creates consistent stat row styling for training/combat statistics\n *\n * @param korean - Korean label\n * @param english - English label\n * @param value - Stat value\n * @param valueColor - Hex color for value text\n * @param isMobile - Mobile responsive mode\n * @returns Stat row configuration object for React rendering\n *\n * @example\n * ```tsx\n * const statConfig = formatStatRow('점수', 'Score', 1500, KOREAN_COLORS.ACCENT_GOLD, false);\n * ```\n *\n * @korean 통계행형식화\n */\nexport function formatStatRow(\n korean: string,\n english: string,\n value: string | number,\n valueColor: number,\n isMobile: boolean,\n): {\n korean: string;\n english: string;\n value: string | number;\n valueColor: number;\n labelSize: string;\n subLabelSize: string;\n valueSize: string;\n} {\n const labelSize = isMobile ? \"11px\" : \"12px\";\n const subLabelSize = isMobile ? \"8px\" : \"9px\";\n const valueSize = isMobile ? \"16px\" : \"18px\";\n\n return {\n korean,\n english,\n value,\n valueColor,\n labelSize,\n subLabelSize,\n valueSize,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"koreanThemeHelpers.js","names":[],"sources":["../../src/utils/koreanThemeHelpers.ts"],"sourcesContent":["/**\n * Korean Theme Helper Utilities for HTML Overlays\n *\n * Provides consistent Korean martial arts cyberpunk theming across all HTML overlay components.\n * These utilities ensure color consistency, bilingual text formatting, and responsive spacing.\n *\n * @module utils/koreanThemeHelpers\n * @category UI Utilities\n * @korean 한국테마도우미\n */\n\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { SPACING, BORDER_RADIUS } from \"../types/constants/ui\";\nimport { hexToRgbaString } from \"./colorUtils\";\nimport {\n getNeonGlowEffect,\n getNeonTextShadow,\n getLayeredDepthEffect,\n getCyberpunkGradient,\n getSmoothTransition,\n getKoreanFontOptimization,\n getHoverStateStyles,\n getFocusStateStyles,\n getBackdropBlurEffect,\n getTrigramSymbolGlow as getTrigramGlowEffect,\n combineShadowEffects,\n type GlowIntensity,\n type HoverAnimationType,\n} from \"./visualEffects\";\n\n/**\n * Bilingual text format options\n * @korean 이중언어형식\n */\nexport type BilingualFormat = \"pipe\" | \"parentheses\" | \"bracket\" | \"slash\";\n\n/**\n * Button variant types for Korean theme\n * @korean 버튼변형\n */\nexport type KoreanButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"danger\"\n | \"success\"\n | \"warning\";\n\n/**\n * Responsive spacing size\n * @korean 반응형간격크기\n */\nexport type SpacingSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"xxl\";\n\n/**\n * Enhanced overlay styles configuration\n * @korean 향상된오버레이스타일설정\n */\nexport interface EnhancedOverlayConfig {\n readonly opacity?: number;\n readonly glowIntensity?: GlowIntensity;\n readonly includeGradient?: boolean;\n readonly includeBackdropBlur?: boolean;\n readonly depthLayers?: number;\n}\n\n/**\n * Base styles for all Korean-themed overlays\n *\n * Provides consistent dark background with cyan/gold accents\n *\n * @param opacity - Background opacity (0-1), default 0.9\n * @returns React.CSSProperties object with Korean theme\n *\n * @example\n * ```tsx\n * <div style={getKoreanOverlayBaseStyles(0.95)}>\n * Content\n * </div>\n * ```\n *\n * @korean 한국오버레이기본스타일얻기\n */\nexport function getKoreanOverlayBaseStyles(\n opacity: number = 0.9,\n): React.CSSProperties {\n return {\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, opacity),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n borderRadius: `${BORDER_RADIUS.MD}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 4px 20px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean overlay styles with visual effects\n *\n * Provides advanced cyberpunk styling with neon glow, depth effects,\n * gradients, and backdrop blur for Korean-themed overlays.\n *\n * @param config - Enhanced overlay configuration\n * @returns React.CSSProperties with advanced visual effects\n *\n * @example\n * ```tsx\n * const styles = getEnhancedKoreanOverlayStyles({\n * opacity: 0.95,\n * glowIntensity: 'medium',\n * includeGradient: true,\n * includeBackdropBlur: true,\n * depthLayers: 3,\n * });\n * <div style={styles}>Enhanced Content</div>\n * ```\n *\n * @korean 향상된한국오버레이스타일얻기\n */\nexport function getEnhancedKoreanOverlayStyles(\n config: EnhancedOverlayConfig = {},\n): React.CSSProperties {\n const {\n opacity = 0.9,\n glowIntensity = \"medium\",\n includeGradient = false,\n includeBackdropBlur = false,\n depthLayers = 2,\n } = config;\n\n // Base styles\n const baseStyles = getKoreanOverlayBaseStyles(opacity);\n\n // Neon glow effect\n const neonGlow = getNeonGlowEffect(\n KOREAN_COLORS.PRIMARY_CYAN,\n glowIntensity,\n true,\n );\n\n // Depth effect\n const depthShadow = getLayeredDepthEffect({\n layers: depthLayers,\n baseOffset: 2,\n baseBlur: 4,\n color: KOREAN_COLORS.BLACK_SOLID,\n opacity: 0.5,\n });\n\n // Combine shadows\n const boxShadow = combineShadowEffects([neonGlow, depthShadow]);\n\n // Optional gradient background\n let background = baseStyles.backgroundColor;\n if (includeGradient) {\n const gradient = getCyberpunkGradient(\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n 135,\n );\n background = `${gradient}, ${background}`;\n }\n\n // Optional backdrop blur\n const backdropStyles = includeBackdropBlur\n ? getBackdropBlurEffect(10, 1.5)\n : {};\n\n // Combine all styles\n return {\n ...baseStyles,\n ...backdropStyles,\n background,\n boxShadow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n ...getKoreanFontOptimization(16, \"normal\"),\n };\n}\n\n/**\n * Format bilingual text with Korean and English\n *\n * Supports multiple formatting styles:\n * - pipe: \"한글 | English\"\n * - parentheses: \"한글 (English)\"\n * - bracket: \"한글 [English]\"\n * - slash: \"한글 / English\"\n *\n * @param korean - Korean text\n * @param english - English text\n * @param format - Format style, default \"pipe\"\n * @returns Formatted bilingual string\n *\n * @example\n * ```tsx\n * formatBilingualText('공격', 'Attack', 'pipe') // \"공격 | Attack\"\n * formatBilingualText('방어', 'Defense', 'parentheses') // \"방어 (Defense)\"\n * ```\n *\n * @korean 이중언어텍스트형식화\n */\nexport function formatBilingualText(\n korean: string,\n english: string,\n format: BilingualFormat = \"pipe\",\n): string {\n switch (format) {\n case \"pipe\":\n return `${korean} | ${english}`;\n case \"parentheses\":\n return `${korean} (${english})`;\n case \"bracket\":\n return `${korean} [${english}]`;\n case \"slash\":\n return `${korean} / ${english}`;\n default:\n return `${korean} | ${english}`;\n }\n}\n\n/**\n * Get Korean button styles with variant support\n *\n * Returns consistent button styling based on variant:\n * - primary: Cyan border, gold text\n * - secondary: Gold border, white text\n * - danger: Red border, red text\n * - success: Green border, green text\n * - warning: Orange border, orange text\n *\n * @param variant - Button variant type\n * @param isHovered - Whether button is hovered\n * @param isPressed - Whether button is pressed\n * @returns React.CSSProperties for button\n *\n * @example\n * ```tsx\n * <button style={getKoreanButtonStyles('primary', isHovered, isPressed)}>\n * {formatBilingualText('확인', 'Confirm')}\n * </button>\n * ```\n *\n * @korean 한국버튼스타일얻기\n */\nexport function getKoreanButtonStyles(\n variant: KoreanButtonVariant = \"primary\",\n isHovered: boolean = false,\n isPressed: boolean = false,\n): React.CSSProperties {\n // Variant-specific colors\n const variantColors = {\n primary: {\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: KOREAN_COLORS.PRIMARY_CYAN,\n },\n secondary: {\n border: KOREAN_COLORS.ACCENT_GOLD,\n text: KOREAN_COLORS.TEXT_PRIMARY,\n hoverBg: KOREAN_COLORS.ACCENT_GOLD,\n },\n danger: {\n border: KOREAN_COLORS.ACCENT_RED,\n text: KOREAN_COLORS.ACCENT_RED,\n hoverBg: KOREAN_COLORS.ACCENT_RED,\n },\n success: {\n border: KOREAN_COLORS.ACCENT_GREEN,\n text: KOREAN_COLORS.ACCENT_GREEN,\n hoverBg: KOREAN_COLORS.ACCENT_GREEN,\n },\n warning: {\n border: KOREAN_COLORS.WARNING_ORANGE,\n text: KOREAN_COLORS.WARNING_ORANGE,\n hoverBg: KOREAN_COLORS.WARNING_ORANGE,\n },\n };\n\n const colors = variantColors[variant];\n\n let backgroundColor = hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 0.9,\n );\n let borderColor = hexToRgbaString(colors.border, 0.8);\n const textColor = hexToRgbaString(colors.text);\n let boxShadow = \"none\";\n let transform = \"scale(1)\";\n\n if (isPressed) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.2);\n transform = \"scale(0.98)\";\n } else if (isHovered) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.1);\n borderColor = hexToRgbaString(colors.border, 1.0);\n boxShadow = `0 0 10px ${hexToRgbaString(colors.border, 0.5)}`;\n }\n\n return {\n backgroundColor,\n border: `2px solid ${borderColor}`,\n borderRadius: `${BORDER_RADIUS.SM}px`,\n color: textColor,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n padding: `${SPACING.SM}px ${SPACING.MD}px`,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n boxShadow,\n transform,\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean button configuration\n * @korean 향상된한국버튼설정\n */\nexport interface EnhancedButtonConfig {\n readonly variant?: KoreanButtonVariant;\n readonly isHovered?: boolean;\n readonly isPressed?: boolean;\n readonly isFocused?: boolean;\n readonly glowIntensity?: GlowIntensity;\n readonly hoverAnimation?: HoverAnimationType;\n}\n\n/**\n * Get enhanced Korean button styles with neon glow\n *\n * Provides advanced button styling with cyberpunk neon effects,\n * smooth transitions, and Korean font optimization.\n *\n * @param config - Enhanced button configuration\n * @returns React.CSSProperties with neon glow effects\n *\n * @example\n * ```tsx\n * const buttonStyle = getKoreanButtonWithGlow({\n * variant: 'primary',\n * isHovered: true,\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined',\n * });\n * <button style={buttonStyle}>\n * {formatBilingualText('공격', 'Attack')}\n * </button>\n * ```\n *\n * @korean 네온글로우한국버튼스타일얻기\n */\nexport function getKoreanButtonWithGlow(\n config: EnhancedButtonConfig = {},\n): React.CSSProperties {\n const {\n variant = \"primary\",\n isHovered = false,\n isPressed = false,\n isFocused = false,\n glowIntensity = \"medium\",\n hoverAnimation = \"combined\",\n } = config;\n\n // Get base button styles\n const baseStyles = getKoreanButtonStyles(variant, false, isPressed);\n\n // Variant-specific glow colors\n const glowColors = {\n primary: KOREAN_COLORS.PRIMARY_CYAN,\n secondary: KOREAN_COLORS.ACCENT_GOLD,\n danger: KOREAN_COLORS.ACCENT_RED,\n success: KOREAN_COLORS.ACCENT_GREEN,\n warning: KOREAN_COLORS.WARNING_ORANGE,\n };\n\n const glowColor = glowColors[variant];\n\n // Hover state with visual effects\n let hoverStyles: React.CSSProperties = {};\n if (isHovered) {\n hoverStyles = getHoverStateStyles(glowColor, hoverAnimation, glowIntensity);\n }\n\n // Focus state\n let focusStyles: React.CSSProperties = {};\n if (isFocused) {\n focusStyles = getFocusStateStyles(glowColor, true);\n }\n\n // Extract font size from base styles (getKoreanButtonStyles always returns number | string)\n const baseFontSize =\n typeof baseStyles.fontSize === \"number\"\n ? baseStyles.fontSize\n : parseInt(String(baseStyles.fontSize), 10) || 14;\n\n // Neon text glow for button text\n const textGlow = getNeonTextShadow(\n glowColor,\n isHovered ? \"medium\" : \"subtle\",\n );\n\n // Korean font optimization\n const fontOptimization = getKoreanFontOptimization(baseFontSize, \"bold\");\n\n return {\n ...baseStyles,\n ...fontOptimization,\n ...hoverStyles,\n ...focusStyles,\n textShadow: textGlow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n };\n}\n\n/**\n * Get button visual effects only (no color/background/border)\n *\n * Extracts only visual effects (boxShadow, transition, transform, textShadow) from\n * getKoreanButtonWithGlow without color-related properties. This is useful for cases\n * where buttons need custom colors (e.g., menu selection states) but want to preserve\n * the visual effects system.\n *\n * This function provides an explicit contract for visual effects extraction, preventing\n * fragile coupling that could occur with destructuring patterns. If getKoreanButtonWithGlow\n * adds new visual effect properties in the future, they should be explicitly added here.\n *\n * **Intentionally Excluded Properties:**\n * - Layout: fontSize, fontFamily, fontWeight, padding, width, height, cursor\n * - Typography: letterSpacing, lineHeight, textRendering, WebkitFontSmoothing, MozOsxFontSmoothing\n * - Colors: color, background, backgroundColor, border, borderColor\n *\n * **Included Visual Effects:**\n * - boxShadow: Neon glow effects\n * - transition: Smooth state changes (0.2s ease)\n * - transform: Hover/press animations (scale)\n * - textShadow: Text glow effects\n *\n * **Note:** If getKoreanButtonWithGlow adds new visual effect properties (e.g., 'filter',\n * 'opacity', 'willChange', 'backdropFilter'), they must be explicitly added to this function's\n * return type and extraction logic to maintain the explicit contract.\n *\n * @param config - Same configuration as getKoreanButtonWithGlow\n * @returns React.CSSProperties with only visual effects (boxShadow, transition, transform, textShadow)\n *\n * @example\n * ```tsx\n * const visualEffects = getButtonVisualEffectsOnly({\n * variant: 'primary',\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined'\n * });\n *\n * <button style={{\n * ...visualEffects,\n * color: customColor, // Apply custom colors\n * background: customBg,\n * border: customBorder\n * }}>\n * Custom Button\n * </button>\n * ```\n *\n * @korean 버튼시각효과만얻기\n */\nexport function getButtonVisualEffectsOnly(\n config: Parameters<typeof getKoreanButtonWithGlow>[0],\n): Pick<\n React.CSSProperties,\n \"boxShadow\" | \"transition\" | \"transform\" | \"textShadow\"\n> {\n const fullStyles = getKoreanButtonWithGlow(config);\n\n // Explicitly extract only visual effect properties\n // Note: This explicitly excludes layout, typography, and color properties.\n // If new visual effects (filter, opacity, willChange, etc.) are added to\n // getKoreanButtonWithGlow, they must be added here to maintain the contract.\n return {\n boxShadow: fullStyles.boxShadow,\n transition: fullStyles.transition,\n transform: fullStyles.transform,\n textShadow: fullStyles.textShadow,\n };\n}\n\n/**\n * Get responsive spacing value\n *\n * Returns SPACING constant value for consistent spacing across components.\n * Optionally scales for mobile devices.\n *\n * **IMPORTANT**: This function accepts lowercase size parameters ('xs', 'sm', 'md', etc.)\n * to provide a more intuitive API, then internally converts to uppercase to match\n * SPACING constant keys ('XS', 'SM', 'MD', etc.). This design choice prioritizes\n * developer experience while maintaining compatibility with the SPACING constants.\n *\n * @param size - Spacing size constant ('xs', 'sm', 'md', 'lg', 'xl', 'xxl')\n * @param isMobile - Whether to apply mobile scaling (87.5% for mobile devices)\n * @returns Spacing value in pixels\n *\n * @example\n * ```tsx\n * const padding = getResponsiveSpacing('md', isMobile); // 16px desktop, 14px mobile\n * <div style={{ padding: `${padding}px` }}>Content</div>\n * ```\n *\n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n size: SpacingSize,\n isMobile: boolean = false,\n): number {\n // Convert lowercase API parameter to uppercase SPACING constant key\n // This provides a more ergonomic API while maintaining internal consistency\n const spacingKey = size.toUpperCase() as keyof typeof SPACING;\n const spacingValue = SPACING[spacingKey];\n const mobileScale = 0.875; // 87.5% for mobile\n\n // Runtime validation: While TypeScript prevents invalid sizes at compile time,\n // this check provides safety for JavaScript consumers and edge cases where\n // type assertions bypass TypeScript checks (e.g., 'as any', dynamic values)\n if (spacingValue === undefined) {\n const fallback = SPACING.MD;\n console.warn(\n `[koreanThemeHelpers:getResponsiveSpacing] Invalid spacing size \"${String(\n size,\n )}\" provided. Falling back to \"MD\".`,\n );\n return isMobile ? Math.round(fallback * mobileScale) : fallback;\n }\n\n return isMobile ? Math.round(spacingValue * mobileScale) : spacingValue;\n}\n\n/**\n * Get trigram symbol by name\n *\n * Returns Unicode trigram symbol for visual embellishment\n *\n * @param name - Trigram name in Korean\n * @returns Unicode trigram symbol\n *\n * @example\n * ```tsx\n * <div>{getTrigramSymbol('건')} 건 (Heaven)</div>\n * ```\n *\n * @korean 팔괘기호얻기\n */\nexport function getTrigramSymbol(\n name: \"건\" | \"태\" | \"리\" | \"진\" | \"손\" | \"감\" | \"간\" | \"곤\",\n): string {\n const symbols = {\n 건: \"☰\", // Heaven - 乾\n 태: \"☱\", // Lake - 兌\n 리: \"☲\", // Fire - 離\n 진: \"☳\", // Thunder - 震\n 손: \"☴\", // Wind - 巽\n 감: \"☵\", // Water - 坎\n 간: \"☶\", // Mountain - 艮\n 곤: \"☷\", // Earth - 坤\n };\n return symbols[name];\n}\n\n/**\n * Get trigram symbol styles with glow effect\n *\n * Enhances trigram symbols (☰☱☲☳☴☵☶☷) with cyberpunk neon glow\n * based on stance-specific colors and active state.\n *\n * @param config - Configuration object\n * @param config.stance - Trigram stance identifier (\"geon\", \"tae\", etc.)\n * @param config.isActive - Whether the trigram stance is currently active\n * @param config.size - Font size in pixels (optional, defaults based on active state)\n * @returns React.CSSProperties with trigram-specific glow\n *\n * @example\n * ```tsx\n * const trigramStyle = getTrigramSymbolWithGlow({ stance: 'geon', isActive: true });\n * <div style={trigramStyle}>\n * ☰ 건 | Geon\n * </div>\n * ```\n *\n * @korean 팔괘기호네온글로우스타일얻기\n */\nexport function getTrigramSymbolWithGlow(config: {\n readonly stance:\n | \"geon\"\n | \"tae\"\n | \"li\"\n | \"jin\"\n | \"son\"\n | \"gam\"\n | \"gan\"\n | \"gon\";\n readonly isActive?: boolean;\n readonly size?: number;\n}): React.CSSProperties & {\n WebkitUserSelect?: string;\n} {\n const { stance, isActive = false, size } = config;\n\n // Map English stance names to Korean\n const stanceToKorean = {\n geon: \"건\",\n tae: \"태\",\n li: \"리\",\n jin: \"진\",\n son: \"손\",\n gam: \"감\",\n gan: \"간\",\n gon: \"곤\",\n };\n\n const koreanName = stanceToKorean[stance];\n\n // Trigram-specific colors matching KOREAN_COLORS\n // Note: Comments reflect actual hex color values\n // TODO: Fix source constants (colors.ts lines 84, 87) - TRIGRAM_GEON_PRIMARY should say \"Gold\" not \"White\",\n // TRIGRAM_JIN_PRIMARY should say \"Medium Purple\" not \"Yellow\"\n const trigramColors: Record<string, number> = {\n 건: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY, // Heaven - Gold (0xffd700)\n 태: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY, // Lake - Sky Blue\n 리: KOREAN_COLORS.TRIGRAM_LI_PRIMARY, // Fire - Orange Red\n 진: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY, // Thunder - Medium Purple (0x9370db)\n 손: KOREAN_COLORS.TRIGRAM_SON_PRIMARY, // Wind - Light Green\n 감: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY, // Water - Blue\n 간: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY, // Mountain - Brown\n 곤: KOREAN_COLORS.TRIGRAM_GON_PRIMARY, // Earth - Dark Gray\n };\n\n const trigramColor = trigramColors[koreanName];\n\n // Get glow effect from visualEffects\n const glowStyles = getTrigramGlowEffect(trigramColor, isActive);\n\n // Korean font optimization for trigram symbols\n const fontSize = size ?? (isActive ? 32 : 28);\n const fontStyles = getKoreanFontOptimization(fontSize, \"bold\");\n\n return {\n ...fontStyles,\n ...glowStyles,\n fontFamily: FONT_FAMILY.KOREAN,\n display: \"inline-block\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n}\n\n/**\n * Get Korean color name for documentation\n *\n * Maps hex color to Korean color name for better documentation\n *\n * @param hexColor - Hex color value from KOREAN_COLORS\n * @returns Korean name and English translation\n *\n * @internal Used primarily for JSDoc documentation\n * @korean 한국색상이름얻기\n */\nexport function getKoreanColorName(hexColor: number): {\n korean: string;\n english: string;\n} {\n const colorNames: Record<number, { korean: string; english: string }> = {\n [KOREAN_COLORS.PRIMARY_CYAN]: { korean: \"청록\", english: \"Cyan\" },\n [KOREAN_COLORS.ACCENT_GOLD]: { korean: \"금색\", english: \"Gold\" },\n [KOREAN_COLORS.ACCENT_RED]: { korean: \"빨강\", english: \"Red\" },\n [KOREAN_COLORS.ACCENT_GREEN]: { korean: \"초록\", english: \"Green\" },\n [KOREAN_COLORS.WARNING_ORANGE]: { korean: \"주황\", english: \"Orange\" },\n [KOREAN_COLORS.TEXT_PRIMARY]: { korean: \"흰색\", english: \"White\" },\n [KOREAN_COLORS.UI_BACKGROUND_DARK]: {\n korean: \"어두운배경\",\n english: \"Dark Background\",\n },\n };\n\n return colorNames[hexColor] ?? { korean: \"알수없음\", english: \"Unknown\" };\n}\n\n/**\n * Format stat row with bilingual labels\n *\n * Creates consistent stat row styling for training/combat statistics\n *\n * @param korean - Korean label\n * @param english - English label\n * @param value - Stat value\n * @param valueColor - Hex color for value text\n * @param isMobile - Mobile responsive mode\n * @returns Stat row configuration object for React rendering\n *\n * @example\n * ```tsx\n * const statConfig = formatStatRow('점수', 'Score', 1500, KOREAN_COLORS.ACCENT_GOLD, false);\n * ```\n *\n * @korean 통계행형식화\n */\nexport function formatStatRow(\n korean: string,\n english: string,\n value: string | number,\n valueColor: number,\n isMobile: boolean,\n): {\n korean: string;\n english: string;\n value: string | number;\n valueColor: number;\n labelSize: string;\n subLabelSize: string;\n valueSize: string;\n} {\n const labelSize = isMobile ? \"11px\" : \"12px\";\n const subLabelSize = isMobile ? \"8px\" : \"9px\";\n const valueSize = isMobile ? \"16px\" : \"18px\";\n\n return {\n korean,\n english,\n value,\n valueColor,\n labelSize,\n subLabelSize,\n valueSize,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"math.js","names":[],"sources":["../../src/utils/math.ts"],"sourcesContent":["/**\n * Mathematical utility functions for Black Trigram\n * \n * Provides shared mathematical operations used across combat, training,\n * and physics systems to ensure consistency and follow DRY principles.\n * \n * **Korean**: 수학 유틸리티 함수\n * \n * @module utils/math\n * @korean 수학유틸리티\n */\n\n/**\n * Calculate 3D Euclidean distance between two positions\n * \n * Uses the standard 3D distance formula: √(dx² + dy² + dz²)\n * This is used throughout the combat and training systems to calculate\n * distance between combatants, ensuring consistent distance calculations.\n * \n * **Korean**: 3D 유클리드 거리 계산\n * \n * @param pos1 - First position as [x, y, z] tuple\n * @param pos2 - Second position as [x, y, z] tuple\n * @returns The 3D Euclidean distance in meters\n * \n * @example\n * const distance = calculateDistance3D([0, 0, 0], [3, 4, 0]);\n * // Returns: 5.0 (3-4-5 triangle)\n * \n * @category Math Utilities\n * @korean 3D거리계산\n */\nexport function calculateDistance3D(\n pos1: [number, number, number],\n pos2: [number, number, number]\n): number {\n const dx = pos1[0] - pos2[0];\n const dy = pos1[1] - pos2[1];\n const dz = pos1[2] - pos2[2];\n return Math.sqrt(dx * dx + dy * dy + dz * dz);\n}\n\n/**\n * Calculate squared 3D distance between two positions\n * \n * Optimized version that avoids the expensive square root operation.\n * Useful when comparing distances (A > B) where the square root is unnecessary.\n * \n * **Korean**: 3D 거리 제곱 계산\n * \n * @param pos1 - First position as [x, y, z] tuple\n * @param pos2 - Second position as [x, y, z] tuple\n * @returns The squared 3D distance in meters²\n * \n * @example\n * const distSq = calculateDistance3DSquared([0, 0, 0], [3, 4, 0]);\n * // Returns: 25.0\n * \n * @category Math Utilities\n * @korean 3D거리제곱계산\n */\nexport function calculateDistance3DSquared(\n pos1: [number, number, number],\n pos2: [number, number, number]\n): number {\n const dx = pos1[0] - pos2[0];\n const dy = pos1[1] - pos2[1];\n const dz = pos1[2] - pos2[2];\n return dx * dx + dy * dy + dz * dz;\n}\n\n/**\n * Convert degrees to radians\n * \n * Used throughout animation systems for bone rotations where angles are\n * specified in degrees for readability but need to be converted to radians\n * for Three.js rendering.\n * \n * **Korean**: 각도를 라디안으로 변환\n * \n * @param degrees - Angle in degrees (0-360)\n * @returns Angle in radians (0-2π)\n * \n * @example\n * const rightAngle = toRadians(90);\n * // Returns: approximately 1.5708 (π/2)\n * \n * @example\n * const straightAngle = toRadians(180);\n * // Returns: approximately 3.1416 (π)\n * \n * @category Math Utilities\n * @korean 각도변환\n */\nexport function toRadians(degrees: number): number {\n return degrees * (Math.PI / 180);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,oBACd,MACA,MACQ;CACR,MAAM,KAAK,KAAK,KAAK,KAAK;CAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;CAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;CAC1B,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;AAuD/C,SAAgB,UAAU,SAAyB;CACjD,OAAO,WAAW,KAAK,KAAK"}
1
+ {"version":3,"file":"math.js","names":[],"sources":["../../src/utils/math.ts"],"sourcesContent":["/**\n * Mathematical utility functions for Black Trigram\n * \n * Provides shared mathematical operations used across combat, training,\n * and physics systems to ensure consistency and follow DRY principles.\n * \n * **Korean**: 수학 유틸리티 함수\n * \n * @module utils/math\n * @korean 수학유틸리티\n */\n\n/**\n * Calculate 3D Euclidean distance between two positions\n * \n * Uses the standard 3D distance formula: √(dx² + dy² + dz²)\n * This is used throughout the combat and training systems to calculate\n * distance between combatants, ensuring consistent distance calculations.\n * \n * **Korean**: 3D 유클리드 거리 계산\n * \n * @param pos1 - First position as [x, y, z] tuple\n * @param pos2 - Second position as [x, y, z] tuple\n * @returns The 3D Euclidean distance in meters\n * \n * @example\n * const distance = calculateDistance3D([0, 0, 0], [3, 4, 0]);\n * // Returns: 5.0 (3-4-5 triangle)\n * \n * @category Math Utilities\n * @korean 3D거리계산\n */\nexport function calculateDistance3D(\n pos1: [number, number, number],\n pos2: [number, number, number]\n): number {\n const dx = pos1[0] - pos2[0];\n const dy = pos1[1] - pos2[1];\n const dz = pos1[2] - pos2[2];\n return Math.sqrt(dx * dx + dy * dy + dz * dz);\n}\n\n/**\n * Calculate squared 3D distance between two positions\n * \n * Optimized version that avoids the expensive square root operation.\n * Useful when comparing distances (A > B) where the square root is unnecessary.\n * \n * **Korean**: 3D 거리 제곱 계산\n * \n * @param pos1 - First position as [x, y, z] tuple\n * @param pos2 - Second position as [x, y, z] tuple\n * @returns The squared 3D distance in meters²\n * \n * @example\n * const distSq = calculateDistance3DSquared([0, 0, 0], [3, 4, 0]);\n * // Returns: 25.0\n * \n * @category Math Utilities\n * @korean 3D거리제곱계산\n */\nexport function calculateDistance3DSquared(\n pos1: [number, number, number],\n pos2: [number, number, number]\n): number {\n const dx = pos1[0] - pos2[0];\n const dy = pos1[1] - pos2[1];\n const dz = pos1[2] - pos2[2];\n return dx * dx + dy * dy + dz * dz;\n}\n\n/**\n * Convert degrees to radians\n * \n * Used throughout animation systems for bone rotations where angles are\n * specified in degrees for readability but need to be converted to radians\n * for Three.js rendering.\n * \n * **Korean**: 각도를 라디안으로 변환\n * \n * @param degrees - Angle in degrees (0-360)\n * @returns Angle in radians (0-2π)\n * \n * @example\n * const rightAngle = toRadians(90);\n * // Returns: approximately 1.5708 (π/2)\n * \n * @example\n * const straightAngle = toRadians(180);\n * // Returns: approximately 3.1416 (π)\n * \n * @category Math Utilities\n * @korean 각도변환\n */\nexport function toRadians(degrees: number): number {\n return degrees * (Math.PI / 180);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,oBACd,MACA,MACQ;CACR,MAAM,KAAK,KAAK,KAAK,KAAK;CAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;CAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;CAC1B,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE;AAC9C;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,SAAgB,UAAU,SAAyB;CACjD,OAAO,WAAW,KAAK,KAAK;AAC9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"mobileLayoutHelpers.js","names":[],"sources":["../../src/utils/mobileLayoutHelpers.ts"],"sourcesContent":["/**\n * Mobile Layout Helpers\n *\n * Shared utilities for calculating mobile area bounds with consistent aspect ratios\n * and device-specific sizing. Used by both combat and training layout hooks.\n *\n * @module utils/mobileLayoutHelpers\n * @category Layout\n * @korean 모바일레이아웃도우미\n */\n\nimport { calculateArenaWorldDimensions } from \"./arenaWorldDimensions\";\n\n/**\n * Mobile area bounds with world dimensions.\n *\n */\nexport interface MobileAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number;\n readonly worldWidthMeters: number;\n readonly worldDepthMeters: number;\n}\n\n/**\n * Device orientation for mobile area calculation.\n *\n * - `landscape`: width > height, the arena is rendered in 4:3 (wider than tall)\n * so both fighters remain visible from a side view.\n * - `portrait`: height ≥ width, the arena is rendered in 3:4 (taller than wide)\n * so both fighters plus the horizontal breathing-room between them fit\n * inside a narrow viewport without being occluded by the bottom HUD.\n *\n */\nexport type MobileOrientation = \"portrait\" | \"landscape\";\n\n/**\n * Calculate mobile area bounds, orientation-aware.\n *\n * Implements consistent mobile area sizing logic shared between combat and training screens.\n * Adapts to different device resolutions while maintaining an orientation-appropriate\n * aspect ratio (4:3 in landscape, 3:4 in portrait).\n *\n * The caller is responsible for passing `bottomClearance` that already includes\n * the bottom HUD (technique bar), the mobile controls (D-Pad / action buttons)\n * and any safe-area insets. This function will never let the arena overflow\n * `height - topClearance - bottomClearance`.\n *\n * @param width - Screen width in pixels\n * @param height - Screen height in pixels\n * @param topClearance - Minimum space to reserve at top (for HUD/header + safe area)\n * @param bottomClearance - Minimum space to reserve at bottom (controls + footer + safe area)\n * @param yOffset - Y position offset (typically header height + padding)\n * @param orientation - `\"portrait\"` for tall-narrow viewports, otherwise `\"landscape\"` (default)\n * @returns Mobile area bounds with position, dimensions, scale, and world dimensions\n *\n * @example\n * ```typescript\n * // Landscape phone: 4:3 arena\n * const landscape = calculateMobileAreaBounds(667, 375, 60, 120, 70, \"landscape\");\n *\n * // Portrait phone: 3:4 arena (taller than wide)\n * const portrait = calculateMobileAreaBounds(375, 667, 80, 220, 90, \"portrait\");\n * ```\n *\n * @korean 모바일영역경계계산\n */\nexport function calculateMobileAreaBounds(\n width: number,\n height: number,\n topClearance: number,\n bottomClearance: number,\n yOffset: number,\n orientation: MobileOrientation = \"landscape\",\n): MobileAreaBounds {\n const isPortrait = orientation === \"portrait\";\n\n // Calculate available space for the area.\n // Extra-small devices (<380px) use tighter margins for more screen real estate.\n //\n // Height reservation: the arena starts at `yOffset`, so the available\n // vertical space is bounded by `height - yOffset - bottomClearance`.\n // Falling back to `height - topClearance - bottomClearance` when a caller\n // passes `yOffset < topClearance` keeps the legacy behaviour, but we must\n // never let the arena overflow when `yOffset > topClearance` (which is the\n // case for the updated combat/training layout hooks).\n const horizontalMargin = width < 380 ? 30 : 40; // 15px vs 20px per side\n const effectiveTopReservation = Math.max(topClearance, yOffset);\n const availableHeight = Math.max(\n 0,\n height - effectiveTopReservation - bottomClearance,\n );\n const availableWidth = Math.max(0, width - horizontalMargin);\n\n // Determine max width based on device resolution\n // Device-specific sizing with extra-small support:\n // - 4K mobile landscape (≥2160px): up to 1100px\n // - QHD+/4K portrait (≥1440px): up to 960px\n // - 2K (1200-1439px): up to 760px\n // - Large phones/tablets (768-1199px): up to 560px\n // - Standard phones (380-767px): up to 400px\n // - Extra-small phones (<380px): up to 320px\n let maxMobileWidth: number;\n if (width >= 2160) {\n maxMobileWidth = Math.min(availableWidth, 1100);\n } else if (width >= 1440) {\n maxMobileWidth = Math.min(availableWidth, 960);\n } else if (width >= 1200) {\n maxMobileWidth = Math.min(availableWidth, 760);\n } else if (width >= 768) {\n maxMobileWidth = Math.min(availableWidth, 560);\n } else if (width >= 380) {\n maxMobileWidth = Math.min(availableWidth, 400);\n } else {\n // Extra-small devices (iPhone SE, old Android, budget phones)\n maxMobileWidth = Math.min(availableWidth, 320);\n }\n\n // In portrait we want the arena to consume more vertical real estate,\n // so the per-device max-height cap only applies in landscape.\n const maxMobileHeight = isPortrait\n ? availableHeight\n : Math.min(availableHeight, width < 380 ? 240 : 800);\n\n // Aspect ratio depends on orientation:\n // landscape → 4:3 (height = width × 3/4)\n // portrait → 3:4 (height = width × 4/3)\n // In portrait, width is constrained by height × 3/4 (not height × 4/3).\n const widthFromHeight = isPortrait\n ? maxMobileHeight * (3 / 4)\n : maxMobileHeight * (4 / 3);\n\n // Minimum arena width so the 3D scene stays legible, but never above\n // what the viewport can actually host:\n // - `availableWidth` caps absolute width of the arena element\n // - in portrait, `availableHeight × 3/4` caps arena width so the arena\n // height never exceeds `availableHeight` (preventing the arena from\n // being drawn behind the bottom HUD / D-Pad).\n // - in landscape, `availableHeight × 4/3` caps arena width so the 4:3\n // arena height never exceeds `availableHeight` on short mobile screens.\n const hardWidthCap = isPortrait\n ? Math.min(availableWidth, availableHeight * (3 / 4))\n : Math.min(availableWidth, availableHeight * (4 / 3));\n const minArenaWidth = Math.min(280, hardWidthCap);\n\n const areaWidth = Math.max(\n Math.min(maxMobileWidth, widthFromHeight, hardWidthCap),\n Math.max(0, minArenaWidth),\n );\n const areaHeight = isPortrait ? areaWidth * (4 / 3) : areaWidth * (3 / 4);\n\n // Calculate world dimensions based on RENDERED arena width (not screen width)\n // This ensures correct pixels-per-meter ratio for the actual visible arena\n const worldDimensions = calculateArenaWorldDimensions(areaWidth);\n\n // Calculate 3D scale factor based on reference arena\n // Reference: 10m arena at 1000px = 100 px/m\n const pixelsPerMeter = areaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - areaWidth) / 2, // Centered horizontally\n y: yOffset,\n width: areaWidth,\n height: areaHeight,\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsEA,SAAgB,0BACd,OACA,QACA,cACA,iBACA,SACA,cAAiC,aACf;CAClB,MAAM,aAAa,gBAAgB;CAWnC,MAAM,mBAAmB,QAAQ,MAAM,KAAK;CAE5C,MAAM,kBAAkB,KAAK,IAC3B,GACA,SAH8B,KAAK,IAAI,cAAc,QAG5C,GAA0B,gBACpC;CACD,MAAM,iBAAiB,KAAK,IAAI,GAAG,QAAQ,iBAAiB;CAU5D,IAAI;CACJ,IAAI,SAAS,MACX,iBAAiB,KAAK,IAAI,gBAAgB,KAAK;MAC1C,IAAI,SAAS,MAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MACzC,IAAI,SAAS,MAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MACzC,IAAI,SAAS,KAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MACzC,IAAI,SAAS,KAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MAG9C,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;CAKhD,MAAM,kBAAkB,aACpB,kBACA,KAAK,IAAI,iBAAiB,QAAQ,MAAM,MAAM,IAAI;CAMtD,MAAM,kBAAkB,aACpB,mBAAmB,IAAI,KACvB,mBAAmB,IAAI;CAU3B,MAAM,eAAe,aACjB,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,GAAG,GACnD,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,GAAG;CAGvD,MAAM,YAAY,KAAK,IACrB,KAAK,IAAI,gBAAgB,iBAAiB,aAAa,EACvD,KAAK,IAAI,GAJW,KAAK,IAAI,KAAK,aAItB,CAAc,CAC3B;CACD,MAAM,aAAa,aAAa,aAAa,IAAI,KAAK,aAAa,IAAI;CAIvE,MAAM,kBAAkB,8BAA8B,UAAU;CAMhE,MAAM,QAFiB,YAAY,gBAAgB,cAEpB;CAE/B,OAAO;EACL,IAAI,QAAQ,aAAa;EACzB,GAAG;EACH,OAAO;EACP,QAAQ;EACR;EACA,kBAAkB,gBAAgB;EAClC,kBAAkB,gBAAgB;EACnC"}
1
+ {"version":3,"file":"mobileLayoutHelpers.js","names":[],"sources":["../../src/utils/mobileLayoutHelpers.ts"],"sourcesContent":["/**\n * Mobile Layout Helpers\n *\n * Shared utilities for calculating mobile area bounds with consistent aspect ratios\n * and device-specific sizing. Used by both combat and training layout hooks.\n *\n * @module utils/mobileLayoutHelpers\n * @category Layout\n * @korean 모바일레이아웃도우미\n */\n\nimport { calculateArenaWorldDimensions } from \"./arenaWorldDimensions\";\n\n/**\n * Mobile area bounds with world dimensions.\n *\n */\nexport interface MobileAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number;\n readonly worldWidthMeters: number;\n readonly worldDepthMeters: number;\n}\n\n/**\n * Device orientation for mobile area calculation.\n *\n * - `landscape`: width > height, the arena is rendered in 4:3 (wider than tall)\n * so both fighters remain visible from a side view.\n * - `portrait`: height ≥ width, the arena is rendered in 3:4 (taller than wide)\n * so both fighters plus the horizontal breathing-room between them fit\n * inside a narrow viewport without being occluded by the bottom HUD.\n *\n */\nexport type MobileOrientation = \"portrait\" | \"landscape\";\n\n/**\n * Calculate mobile area bounds, orientation-aware.\n *\n * Implements consistent mobile area sizing logic shared between combat and training screens.\n * Adapts to different device resolutions while maintaining an orientation-appropriate\n * aspect ratio (4:3 in landscape, 3:4 in portrait).\n *\n * The caller is responsible for passing `bottomClearance` that already includes\n * the bottom HUD (technique bar), the mobile controls (D-Pad / action buttons)\n * and any safe-area insets. This function will never let the arena overflow\n * `height - topClearance - bottomClearance`.\n *\n * @param width - Screen width in pixels\n * @param height - Screen height in pixels\n * @param topClearance - Minimum space to reserve at top (for HUD/header + safe area)\n * @param bottomClearance - Minimum space to reserve at bottom (controls + footer + safe area)\n * @param yOffset - Y position offset (typically header height + padding)\n * @param orientation - `\"portrait\"` for tall-narrow viewports, otherwise `\"landscape\"` (default)\n * @returns Mobile area bounds with position, dimensions, scale, and world dimensions\n *\n * @example\n * ```typescript\n * // Landscape phone: 4:3 arena\n * const landscape = calculateMobileAreaBounds(667, 375, 60, 120, 70, \"landscape\");\n *\n * // Portrait phone: 3:4 arena (taller than wide)\n * const portrait = calculateMobileAreaBounds(375, 667, 80, 220, 90, \"portrait\");\n * ```\n *\n * @korean 모바일영역경계계산\n */\nexport function calculateMobileAreaBounds(\n width: number,\n height: number,\n topClearance: number,\n bottomClearance: number,\n yOffset: number,\n orientation: MobileOrientation = \"landscape\",\n): MobileAreaBounds {\n const isPortrait = orientation === \"portrait\";\n\n // Calculate available space for the area.\n // Extra-small devices (<380px) use tighter margins for more screen real estate.\n //\n // Height reservation: the arena starts at `yOffset`, so the available\n // vertical space is bounded by `height - yOffset - bottomClearance`.\n // Falling back to `height - topClearance - bottomClearance` when a caller\n // passes `yOffset < topClearance` keeps the legacy behaviour, but we must\n // never let the arena overflow when `yOffset > topClearance` (which is the\n // case for the updated combat/training layout hooks).\n const horizontalMargin = width < 380 ? 30 : 40; // 15px vs 20px per side\n const effectiveTopReservation = Math.max(topClearance, yOffset);\n const availableHeight = Math.max(\n 0,\n height - effectiveTopReservation - bottomClearance,\n );\n const availableWidth = Math.max(0, width - horizontalMargin);\n\n // Determine max width based on device resolution\n // Device-specific sizing with extra-small support:\n // - 4K mobile landscape (≥2160px): up to 1100px\n // - QHD+/4K portrait (≥1440px): up to 960px\n // - 2K (1200-1439px): up to 760px\n // - Large phones/tablets (768-1199px): up to 560px\n // - Standard phones (380-767px): up to 400px\n // - Extra-small phones (<380px): up to 320px\n let maxMobileWidth: number;\n if (width >= 2160) {\n maxMobileWidth = Math.min(availableWidth, 1100);\n } else if (width >= 1440) {\n maxMobileWidth = Math.min(availableWidth, 960);\n } else if (width >= 1200) {\n maxMobileWidth = Math.min(availableWidth, 760);\n } else if (width >= 768) {\n maxMobileWidth = Math.min(availableWidth, 560);\n } else if (width >= 380) {\n maxMobileWidth = Math.min(availableWidth, 400);\n } else {\n // Extra-small devices (iPhone SE, old Android, budget phones)\n maxMobileWidth = Math.min(availableWidth, 320);\n }\n\n // In portrait we want the arena to consume more vertical real estate,\n // so the per-device max-height cap only applies in landscape.\n const maxMobileHeight = isPortrait\n ? availableHeight\n : Math.min(availableHeight, width < 380 ? 240 : 800);\n\n // Aspect ratio depends on orientation:\n // landscape → 4:3 (height = width × 3/4)\n // portrait → 3:4 (height = width × 4/3)\n // In portrait, width is constrained by height × 3/4 (not height × 4/3).\n const widthFromHeight = isPortrait\n ? maxMobileHeight * (3 / 4)\n : maxMobileHeight * (4 / 3);\n\n // Minimum arena width so the 3D scene stays legible, but never above\n // what the viewport can actually host:\n // - `availableWidth` caps absolute width of the arena element\n // - in portrait, `availableHeight × 3/4` caps arena width so the arena\n // height never exceeds `availableHeight` (preventing the arena from\n // being drawn behind the bottom HUD / D-Pad).\n // - in landscape, `availableHeight × 4/3` caps arena width so the 4:3\n // arena height never exceeds `availableHeight` on short mobile screens.\n const hardWidthCap = isPortrait\n ? Math.min(availableWidth, availableHeight * (3 / 4))\n : Math.min(availableWidth, availableHeight * (4 / 3));\n const minArenaWidth = Math.min(280, hardWidthCap);\n\n const areaWidth = Math.max(\n Math.min(maxMobileWidth, widthFromHeight, hardWidthCap),\n Math.max(0, minArenaWidth),\n );\n const areaHeight = isPortrait ? areaWidth * (4 / 3) : areaWidth * (3 / 4);\n\n // Calculate world dimensions based on RENDERED arena width (not screen width)\n // This ensures correct pixels-per-meter ratio for the actual visible arena\n const worldDimensions = calculateArenaWorldDimensions(areaWidth);\n\n // Calculate 3D scale factor based on reference arena\n // Reference: 10m arena at 1000px = 100 px/m\n const pixelsPerMeter = areaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - areaWidth) / 2, // Centered horizontally\n y: yOffset,\n width: areaWidth,\n height: areaHeight,\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsEA,SAAgB,0BACd,OACA,QACA,cACA,iBACA,SACA,cAAiC,aACf;CAClB,MAAM,aAAa,gBAAgB;CAWnC,MAAM,mBAAmB,QAAQ,MAAM,KAAK;CAE5C,MAAM,kBAAkB,KAAK,IAC3B,GACA,SAH8B,KAAK,IAAI,cAAc,OAG5C,IAA0B,eACrC;CACA,MAAM,iBAAiB,KAAK,IAAI,GAAG,QAAQ,gBAAgB;CAU3D,IAAI;CACJ,IAAI,SAAS,MACX,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MACzC,IAAI,SAAS,MAClB,iBAAiB,KAAK,IAAI,gBAAgB,GAAG;MACxC,IAAI,SAAS,MAClB,iBAAiB,KAAK,IAAI,gBAAgB,GAAG;MACxC,IAAI,SAAS,KAClB,iBAAiB,KAAK,IAAI,gBAAgB,GAAG;MACxC,IAAI,SAAS,KAClB,iBAAiB,KAAK,IAAI,gBAAgB,GAAG;MAG7C,iBAAiB,KAAK,IAAI,gBAAgB,GAAG;CAK/C,MAAM,kBAAkB,aACpB,kBACA,KAAK,IAAI,iBAAiB,QAAQ,MAAM,MAAM,GAAG;CAMrD,MAAM,kBAAkB,aACpB,mBAAmB,IAAI,KACvB,mBAAmB,IAAI;CAU3B,MAAM,eAAe,aACjB,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,EAAE,IAClD,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,EAAE;CAGtD,MAAM,YAAY,KAAK,IACrB,KAAK,IAAI,gBAAgB,iBAAiB,YAAY,GACtD,KAAK,IAAI,GAJW,KAAK,IAAI,KAAK,YAItB,CAAa,CAC3B;CACA,MAAM,aAAa,aAAa,aAAa,IAAI,KAAK,aAAa,IAAI;CAIvE,MAAM,kBAAkB,8BAA8B,SAAS;CAM/D,MAAM,QAFiB,YAAY,gBAAgB,cAEpB;CAE/B,OAAO;EACL,IAAI,QAAQ,aAAa;EACzB,GAAG;EACH,OAAO;EACP,QAAQ;EACR;EACA,kBAAkB,gBAAgB;EAClC,kBAAkB,gBAAgB;CACpC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"mobileUIUtils.js","names":[],"sources":["../../src/utils/mobileUIUtils.ts"],"sourcesContent":["/**\n * Mobile UI utilities for responsive touch-optimized interfaces\n * \n * Provides helpers for touch target sizing, responsive font scaling,\n * and mobile-specific layout calculations following iOS/Android guidelines.\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are top priority and get:\n * - Enhanced touch targets (56px vs 48px)\n * - Larger Korean fonts (+10% for better readability)\n * - Optimized spacing and layout\n * - Full feature parity with desktop\n * \n * Supported Devices (Priority Order):\n * 1. Super HD Mobile (≥768px): Motorola Edge 60 Pro, Galaxy S-series\n * 2. Standard Mobile (375-767px): iPhone 12/13/14, standard Android\n * 3. Small Mobile (<375px): iPhone SE, budget devices\n * \n * @module utils/mobileUIUtils\n * @category Mobile Utilities\n * @korean 모바일UI유틸리티\n */\n\nimport { UI_DIMENSIONS, SPACING } from \"../types/constants/ui\";\nimport {\n KOREAN_MOBILE_FONT_SIZES,\n getKoreanFontSize,\n} from \"../types/constants/typography\";\n\n/**\n * Viewport size category for responsive design\n * \n * @category Mobile UI\n * @korean 뷰포트크기범주\n */\nexport type ViewportSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\n/**\n * Touch target size configuration\n * \n * @category Mobile UI\n * @korean 터치타겟크기설정\n */\nexport interface TouchTargetSize {\n readonly minWidth: number;\n readonly minHeight: number;\n readonly padding: number;\n readonly spacing: number;\n}\n\n/**\n * Get viewport size category from width\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are treated as 'md' for optimal layout\n * \n * @param width - Viewport width in pixels\n * @returns Viewport size category\n * \n * @korean 뷰포트크기얻기\n */\nexport function getViewportSize(width: number): ViewportSize {\n if (width < 375) return \"xs\"; // Extra small phones\n if (width < 768) return \"sm\"; // Standard phones\n if (width < 1024) return \"md\"; // High-end mobile (Super HD), tablets\n if (width < 1440) return \"lg\"; // Small desktop\n return \"xl\"; // Large desktop\n}\n\n/**\n * Get touch-optimized button size configuration\n * Ensures minimum 48px touch targets per iOS/Android guidelines\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) get enhanced touch targets (56px)\n * for better precision on high-resolution displays\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width for fine-tuning\n * @returns Touch target size configuration\n * \n * @korean 터치타겟크기얻기\n */\nexport function getTouchTargetSize(\n isMobile: boolean,\n viewportWidth?: number\n): TouchTargetSize {\n if (!isMobile) {\n return {\n minWidth: 120,\n minHeight: 44,\n padding: SPACING.MD,\n spacing: SPACING.MD,\n };\n }\n\n const width = viewportWidth ?? 375;\n const isExtraSmall = width < 360;\n const isSuperHD = width >= 768; // High-end mobile (Motorola Edge 60 Pro, etc.)\n\n // Super HD devices get enhanced touch targets for better precision\n if (isSuperHD) {\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n padding: SPACING.MD,\n spacing: SPACING.COMPACT,\n };\n }\n\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n padding: isExtraSmall ? SPACING.SM : SPACING.COMPACT,\n spacing: UI_DIMENSIONS.TOUCH_TARGET_SPACING,\n };\n}\n\n/**\n * Get responsive Korean font size for mobile\n * Ensures minimum 16px for body text on mobile\n * \n * PRIORITY: High-end mobile (Super HD, 2K+) get enhanced font sizes for better readability\n * \n * @param size - Size category ('SMALL', 'MEDIUM', 'LARGE')\n * @param viewportWidth - Viewport width in pixels\n * @returns Font size in pixels\n * \n * @korean 모바일한글글꼴크기얻기\n */\nexport function getMobileKoreanFontSize(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n viewportWidth: number\n): number {\n const fontSize = getKoreanFontSize(size, viewportWidth);\n \n // Super HD mobile devices (≥768px) get enhanced font sizes\n // for better readability on high-resolution displays\n if (viewportWidth >= 768) {\n return Math.ceil(fontSize * 1.1); // 10% larger for Super HD\n }\n \n return fontSize;\n}\n\n/**\n * Get responsive spacing value\n * Scales spacing based on viewport size\n * \n * @param baseSpacing - Base spacing value from SPACING constant\n * @param isMobile - Whether on mobile device\n * @returns Scaled spacing in pixels\n * \n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n baseSpacing: number,\n isMobile: boolean\n): number {\n return isMobile ? Math.max(SPACING.SM, baseSpacing * 0.75) : baseSpacing;\n}\n\n/**\n * Calculate responsive panel width\n * Ensures panels fit within viewport with proper margins\n * \n * @param viewportWidth - Viewport width in pixels\n * @param isMobile - Whether on mobile device\n * @param minMargin - Minimum margin on each side (default: 20px)\n * @returns Panel width in pixels\n * \n * @korean 반응형패널폭얻기\n */\nexport function getResponsivePanelWidth(\n viewportWidth: number,\n isMobile: boolean,\n minMargin = 20\n): number {\n if (!isMobile) {\n return Math.min(400, viewportWidth - minMargin * 2);\n }\n\n // Mobile: use most of screen width\n return Math.min(viewportWidth - minMargin * 2, 360);\n}\n\n/**\n * Check if viewport is in landscape orientation\n * \n * @param viewportWidth - Viewport width in pixels\n * @param viewportHeight - Viewport height in pixels\n * @returns Whether in landscape orientation\n * \n * @korean 가로모드여부\n */\nexport function isLandscape(\n viewportWidth: number,\n viewportHeight: number\n): boolean {\n return viewportWidth > viewportHeight;\n}\n\n/**\n * Get responsive button styles for touch optimization\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width\n * @returns CSS properties for button\n * \n * @korean 반응형버튼스타일얻기\n */\nexport function getResponsiveButtonStyles(\n isMobile: boolean,\n viewportWidth?: number\n): React.CSSProperties {\n const touchTarget = getTouchTargetSize(isMobile, viewportWidth);\n const fontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", viewportWidth ?? 375)\n : 16;\n\n return {\n minWidth: `${touchTarget.minWidth}px`,\n minHeight: `${touchTarget.minHeight}px`,\n padding: `${touchTarget.padding}px`,\n fontSize: `${fontSize}px`,\n lineHeight: \"1.4\",\n cursor: \"pointer\",\n userSelect: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n };\n}\n\n/**\n * Get responsive text styles with proper Korean font sizing\n * \n * @param size - Font size category\n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Viewport width in pixels\n * @returns CSS properties for text\n * \n * @korean 반응형텍스트스타일얻기\n */\nexport function getResponsiveTextStyles(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n isMobile: boolean,\n viewportWidth: number\n): React.CSSProperties {\n const fontSize = isMobile\n ? getMobileKoreanFontSize(size, viewportWidth)\n : KOREAN_MOBILE_FONT_SIZES[size].regular;\n\n return {\n fontSize: `${fontSize}px`,\n lineHeight: isMobile ? \"1.5\" : \"1.4\",\n letterSpacing: \"0.02em\",\n };\n}\n\n/**\n * Calculate minimum spacing between interactive elements\n * Ensures adequate spacing for touch accuracy\n * \n * @param isMobile - Whether on mobile device\n * @returns Minimum spacing in pixels\n * \n * @korean 최소상호작용간격얻기\n */\nexport function getMinimumInteractiveSpacing(isMobile: boolean): number {\n return isMobile\n ? UI_DIMENSIONS.TOUCH_TARGET_SPACING\n : SPACING.COMPACT;\n}\n\n/**\n * Viewport detection utilities\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are explicitly supported\n * \n * @category Mobile UI\n * @korean 뷰포트감지\n */\nexport const ViewportDetection = {\n /** Check if iPhone SE or similar small device @korean iPhone SE여부 */\n isSmallMobile: (width: number) => width <= 375,\n\n /** Check if standard mobile device (excluding high-end) @korean 표준모바일여부 */\n isMobile: (width: number) => width < 768,\n\n /**\n * Check if high-end mobile device (Super HD, 2K+)\n * Uses height + DPR to distinguish phones from tablets\n * High-end phones: short side 400-600px, long side >=800px, DPR >=2\n * @korean 고급모바일여부\n */\n isSuperHDMobile: (width: number, height: number = window.innerHeight) => {\n const shortSide = Math.min(width, height);\n const longSide = Math.max(width, height);\n const dpr = window.devicePixelRatio || 1;\n return shortSide >= 400 && shortSide <= 600 && longSide >= 800 && dpr >= 2;\n },\n\n /** Check if tablet device @korean 태블릿여부 */\n isTablet: (width: number) => width >= 768 && width < 1024,\n\n /** Check if desktop device @korean 데스크톱여부 */\n isDesktop: (width: number) => width >= 1024,\n\n /** Check if device has notch (iPhone X+) @korean 노치여부 */\n hasNotch: (width: number, height: number) =>\n (width === 375 && height === 812) || // iPhone X, XS, 11 Pro\n (width === 414 && height === 896) || // iPhone XR, XS Max, 11, 11 Pro Max\n (width === 390 && height === 844) || // iPhone 12, 12 Pro, 13, 13 Pro, 14\n (width === 393 && height === 852) || // iPhone 14 Pro\n (width === 428 && height === 926), // iPhone 12/13/14 Pro Max\n} as const;\n"],"mappings":";;;;;;;;;;;;;;AA+HA,SAAgB,wBACd,MACA,eACQ;CACR,MAAM,WAAW,kBAAkB,MAAM,cAAc;CAIvD,IAAI,iBAAiB,KACnB,OAAO,KAAK,KAAK,WAAW,IAAI;CAGlC,OAAO"}
1
+ {"version":3,"file":"mobileUIUtils.js","names":[],"sources":["../../src/utils/mobileUIUtils.ts"],"sourcesContent":["/**\n * Mobile UI utilities for responsive touch-optimized interfaces\n * \n * Provides helpers for touch target sizing, responsive font scaling,\n * and mobile-specific layout calculations following iOS/Android guidelines.\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are top priority and get:\n * - Enhanced touch targets (56px vs 48px)\n * - Larger Korean fonts (+10% for better readability)\n * - Optimized spacing and layout\n * - Full feature parity with desktop\n * \n * Supported Devices (Priority Order):\n * 1. Super HD Mobile (≥768px): Motorola Edge 60 Pro, Galaxy S-series\n * 2. Standard Mobile (375-767px): iPhone 12/13/14, standard Android\n * 3. Small Mobile (<375px): iPhone SE, budget devices\n * \n * @module utils/mobileUIUtils\n * @category Mobile Utilities\n * @korean 모바일UI유틸리티\n */\n\nimport { UI_DIMENSIONS, SPACING } from \"../types/constants/ui\";\nimport {\n KOREAN_MOBILE_FONT_SIZES,\n getKoreanFontSize,\n} from \"../types/constants/typography\";\n\n/**\n * Viewport size category for responsive design\n * \n * @category Mobile UI\n * @korean 뷰포트크기범주\n */\nexport type ViewportSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\n/**\n * Touch target size configuration\n * \n * @category Mobile UI\n * @korean 터치타겟크기설정\n */\nexport interface TouchTargetSize {\n readonly minWidth: number;\n readonly minHeight: number;\n readonly padding: number;\n readonly spacing: number;\n}\n\n/**\n * Get viewport size category from width\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are treated as 'md' for optimal layout\n * \n * @param width - Viewport width in pixels\n * @returns Viewport size category\n * \n * @korean 뷰포트크기얻기\n */\nexport function getViewportSize(width: number): ViewportSize {\n if (width < 375) return \"xs\"; // Extra small phones\n if (width < 768) return \"sm\"; // Standard phones\n if (width < 1024) return \"md\"; // High-end mobile (Super HD), tablets\n if (width < 1440) return \"lg\"; // Small desktop\n return \"xl\"; // Large desktop\n}\n\n/**\n * Get touch-optimized button size configuration\n * Ensures minimum 48px touch targets per iOS/Android guidelines\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) get enhanced touch targets (56px)\n * for better precision on high-resolution displays\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width for fine-tuning\n * @returns Touch target size configuration\n * \n * @korean 터치타겟크기얻기\n */\nexport function getTouchTargetSize(\n isMobile: boolean,\n viewportWidth?: number\n): TouchTargetSize {\n if (!isMobile) {\n return {\n minWidth: 120,\n minHeight: 44,\n padding: SPACING.MD,\n spacing: SPACING.MD,\n };\n }\n\n const width = viewportWidth ?? 375;\n const isExtraSmall = width < 360;\n const isSuperHD = width >= 768; // High-end mobile (Motorola Edge 60 Pro, etc.)\n\n // Super HD devices get enhanced touch targets for better precision\n if (isSuperHD) {\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n padding: SPACING.MD,\n spacing: SPACING.COMPACT,\n };\n }\n\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n padding: isExtraSmall ? SPACING.SM : SPACING.COMPACT,\n spacing: UI_DIMENSIONS.TOUCH_TARGET_SPACING,\n };\n}\n\n/**\n * Get responsive Korean font size for mobile\n * Ensures minimum 16px for body text on mobile\n * \n * PRIORITY: High-end mobile (Super HD, 2K+) get enhanced font sizes for better readability\n * \n * @param size - Size category ('SMALL', 'MEDIUM', 'LARGE')\n * @param viewportWidth - Viewport width in pixels\n * @returns Font size in pixels\n * \n * @korean 모바일한글글꼴크기얻기\n */\nexport function getMobileKoreanFontSize(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n viewportWidth: number\n): number {\n const fontSize = getKoreanFontSize(size, viewportWidth);\n \n // Super HD mobile devices (≥768px) get enhanced font sizes\n // for better readability on high-resolution displays\n if (viewportWidth >= 768) {\n return Math.ceil(fontSize * 1.1); // 10% larger for Super HD\n }\n \n return fontSize;\n}\n\n/**\n * Get responsive spacing value\n * Scales spacing based on viewport size\n * \n * @param baseSpacing - Base spacing value from SPACING constant\n * @param isMobile - Whether on mobile device\n * @returns Scaled spacing in pixels\n * \n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n baseSpacing: number,\n isMobile: boolean\n): number {\n return isMobile ? Math.max(SPACING.SM, baseSpacing * 0.75) : baseSpacing;\n}\n\n/**\n * Calculate responsive panel width\n * Ensures panels fit within viewport with proper margins\n * \n * @param viewportWidth - Viewport width in pixels\n * @param isMobile - Whether on mobile device\n * @param minMargin - Minimum margin on each side (default: 20px)\n * @returns Panel width in pixels\n * \n * @korean 반응형패널폭얻기\n */\nexport function getResponsivePanelWidth(\n viewportWidth: number,\n isMobile: boolean,\n minMargin = 20\n): number {\n if (!isMobile) {\n return Math.min(400, viewportWidth - minMargin * 2);\n }\n\n // Mobile: use most of screen width\n return Math.min(viewportWidth - minMargin * 2, 360);\n}\n\n/**\n * Check if viewport is in landscape orientation\n * \n * @param viewportWidth - Viewport width in pixels\n * @param viewportHeight - Viewport height in pixels\n * @returns Whether in landscape orientation\n * \n * @korean 가로모드여부\n */\nexport function isLandscape(\n viewportWidth: number,\n viewportHeight: number\n): boolean {\n return viewportWidth > viewportHeight;\n}\n\n/**\n * Get responsive button styles for touch optimization\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width\n * @returns CSS properties for button\n * \n * @korean 반응형버튼스타일얻기\n */\nexport function getResponsiveButtonStyles(\n isMobile: boolean,\n viewportWidth?: number\n): React.CSSProperties {\n const touchTarget = getTouchTargetSize(isMobile, viewportWidth);\n const fontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", viewportWidth ?? 375)\n : 16;\n\n return {\n minWidth: `${touchTarget.minWidth}px`,\n minHeight: `${touchTarget.minHeight}px`,\n padding: `${touchTarget.padding}px`,\n fontSize: `${fontSize}px`,\n lineHeight: \"1.4\",\n cursor: \"pointer\",\n userSelect: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n };\n}\n\n/**\n * Get responsive text styles with proper Korean font sizing\n * \n * @param size - Font size category\n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Viewport width in pixels\n * @returns CSS properties for text\n * \n * @korean 반응형텍스트스타일얻기\n */\nexport function getResponsiveTextStyles(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n isMobile: boolean,\n viewportWidth: number\n): React.CSSProperties {\n const fontSize = isMobile\n ? getMobileKoreanFontSize(size, viewportWidth)\n : KOREAN_MOBILE_FONT_SIZES[size].regular;\n\n return {\n fontSize: `${fontSize}px`,\n lineHeight: isMobile ? \"1.5\" : \"1.4\",\n letterSpacing: \"0.02em\",\n };\n}\n\n/**\n * Calculate minimum spacing between interactive elements\n * Ensures adequate spacing for touch accuracy\n * \n * @param isMobile - Whether on mobile device\n * @returns Minimum spacing in pixels\n * \n * @korean 최소상호작용간격얻기\n */\nexport function getMinimumInteractiveSpacing(isMobile: boolean): number {\n return isMobile\n ? UI_DIMENSIONS.TOUCH_TARGET_SPACING\n : SPACING.COMPACT;\n}\n\n/**\n * Viewport detection utilities\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are explicitly supported\n * \n * @category Mobile UI\n * @korean 뷰포트감지\n */\nexport const ViewportDetection = {\n /** Check if iPhone SE or similar small device @korean iPhone SE여부 */\n isSmallMobile: (width: number) => width <= 375,\n\n /** Check if standard mobile device (excluding high-end) @korean 표준모바일여부 */\n isMobile: (width: number) => width < 768,\n\n /**\n * Check if high-end mobile device (Super HD, 2K+)\n * Uses height + DPR to distinguish phones from tablets\n * High-end phones: short side 400-600px, long side >=800px, DPR >=2\n * @korean 고급모바일여부\n */\n isSuperHDMobile: (width: number, height: number = window.innerHeight) => {\n const shortSide = Math.min(width, height);\n const longSide = Math.max(width, height);\n const dpr = window.devicePixelRatio || 1;\n return shortSide >= 400 && shortSide <= 600 && longSide >= 800 && dpr >= 2;\n },\n\n /** Check if tablet device @korean 태블릿여부 */\n isTablet: (width: number) => width >= 768 && width < 1024,\n\n /** Check if desktop device @korean 데스크톱여부 */\n isDesktop: (width: number) => width >= 1024,\n\n /** Check if device has notch (iPhone X+) @korean 노치여부 */\n hasNotch: (width: number, height: number) =>\n (width === 375 && height === 812) || // iPhone X, XS, 11 Pro\n (width === 414 && height === 896) || // iPhone XR, XS Max, 11, 11 Pro Max\n (width === 390 && height === 844) || // iPhone 12, 12 Pro, 13, 13 Pro, 14\n (width === 393 && height === 852) || // iPhone 14 Pro\n (width === 428 && height === 926), // iPhone 12/13/14 Pro Max\n} as const;\n"],"mappings":";;;;;;;;;;;;;;AA+HA,SAAgB,wBACd,MACA,eACQ;CACR,MAAM,WAAW,kBAAkB,MAAM,aAAa;CAItD,IAAI,iBAAiB,KACnB,OAAO,KAAK,KAAK,WAAW,GAAG;CAGjC,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"PerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/PerformanceMonitor.ts"],"sourcesContent":["/**\n * PerformanceMonitor - Real-time FPS and performance tracking for Three.js\n *\n * Monitors frame rate, memory usage, and draw calls to maintain 60fps target.\n * Provides warnings in development mode when performance degrades.\n */\n\nimport * as THREE from \"three\";\n\nexport interface PerformanceMetrics {\n readonly fps: number;\n readonly avgFps: number;\n readonly minFps: number;\n readonly maxFps: number;\n readonly frameTime: number;\n readonly memoryMB: number;\n readonly drawCalls: number;\n readonly triangles: number;\n}\n\nexport interface PerformanceThresholds {\n readonly targetFps: number;\n readonly minAcceptableFps: number;\n readonly maxMemoryMB: number;\n readonly maxDrawCalls: number;\n}\n\nconst DEFAULT_THRESHOLDS: PerformanceThresholds = {\n targetFps: 60,\n minAcceptableFps: 55,\n maxMemoryMB: 300,\n maxDrawCalls: 100,\n};\n\n// Prevent unrealistic spikes when frame deltas are extremely small (e.g., mocked timers)\nconst MAX_SAMPLE_FPS = 180;\nconst MIN_FRAME_TIME_MS = 1000 / MAX_SAMPLE_FPS;\n\n/**\n * PerformanceMonitor class for real-time performance tracking\n */\nexport class PerformanceMonitor {\n private frames: number[] = [];\n private lastTime = performance.now();\n private frameCount = 0;\n private readonly maxFrameSamples = 60; // Track last 60 frames (1 second at 60fps)\n\n private minFps = Infinity;\n private maxFps = 0;\n\n private readonly thresholds: PerformanceThresholds;\n private performanceWarnings: string[] = [];\n\n constructor(thresholds: Partial<PerformanceThresholds> = {}) {\n this.thresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update performance metrics (call once per frame)\n * @param renderer Optional Three.js renderer for draw call tracking\n * @returns Current FPS\n */\n update(renderer?: THREE.WebGLRenderer): number {\n const now = performance.now();\n const delta = now - this.lastTime;\n this.lastTime = now;\n\n if (delta <= 0) return 0; // Skip invalid frames\n\n const clampedDelta = Math.max(delta, MIN_FRAME_TIME_MS);\n const fps = Math.min(1000 / clampedDelta, MAX_SAMPLE_FPS);\n this.frames.push(fps);\n\n // Track min/max FPS\n if (fps < this.minFps) this.minFps = fps;\n if (fps > this.maxFps) this.maxFps = fps;\n\n // Keep frames array bounded\n if (this.frames.length > this.maxFrameSamples) {\n this.frames.shift();\n }\n\n this.frameCount++;\n\n // Check performance thresholds (only in dev mode)\n if (import.meta.env.DEV && this.frameCount % 60 === 0) {\n this.checkPerformanceThresholds(renderer);\n }\n\n return fps;\n }\n\n /**\n * Get current FPS\n */\n getCurrentFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames[this.frames.length - 1];\n }\n\n /**\n * Get average FPS over the sampling window\n */\n getAverageFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames.reduce((a, b) => a + b, 0) / this.frames.length;\n }\n\n /**\n * Get minimum FPS recorded\n */\n getMinFPS(): number {\n return this.minFps === Infinity ? 0 : this.minFps;\n }\n\n /**\n * Get maximum FPS recorded\n */\n getMaxFPS(): number {\n return this.maxFps;\n }\n\n /**\n * Check if performance is good (above minimum threshold)\n */\n isPerformanceGood(): boolean {\n const avgFps = this.getAverageFPS();\n return avgFps >= this.thresholds.minAcceptableFps;\n }\n\n /**\n * Get comprehensive performance metrics\n */\n getMetrics(renderer?: THREE.WebGLRenderer): PerformanceMetrics {\n const avgFps = this.getAverageFPS();\n const currentFps = this.getCurrentFPS();\n\n return {\n fps: currentFps,\n avgFps,\n minFps: this.getMinFPS(),\n maxFps: this.getMaxFPS(),\n frameTime: currentFps > 0 ? 1000 / currentFps : 0,\n memoryMB: this.getMemoryUsageMB(),\n drawCalls: renderer?.info?.render?.calls ?? 0,\n triangles: renderer?.info?.render?.triangles ?? 0,\n };\n }\n\n /**\n * Get current memory usage in MB (Chrome only)\n */\n private getMemoryUsageMB(): number {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Chrome-specific performance.memory API not in standard TS types\n const perf = performance as any;\n if (perf.memory) {\n return perf.memory.usedJSHeapSize / 1024 / 1024;\n }\n return 0;\n }\n\n /**\n * Check performance thresholds and log warnings\n */\n private checkPerformanceThresholds(renderer?: THREE.WebGLRenderer): void {\n this.performanceWarnings = [];\n\n const avgFps = this.getAverageFPS();\n const memoryMB = this.getMemoryUsageMB();\n const drawCalls = renderer?.info?.render?.calls ?? 0;\n\n // Check FPS\n if (avgFps < this.thresholds.minAcceptableFps) {\n const warning = `⚠️ FPS below threshold: ${avgFps.toFixed(1)} < ${\n this.thresholds.minAcceptableFps\n }`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check memory\n if (memoryMB > 0 && memoryMB > this.thresholds.maxMemoryMB) {\n const warning = `⚠️ Memory usage high: ${memoryMB.toFixed(1)}MB > ${\n this.thresholds.maxMemoryMB\n }MB`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check draw calls\n if (drawCalls > this.thresholds.maxDrawCalls) {\n const warning = `⚠️ Draw calls high: ${drawCalls} > ${this.thresholds.maxDrawCalls}`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n }\n\n /**\n * Get current performance warnings\n */\n getWarnings(): readonly string[] {\n return this.performanceWarnings;\n }\n\n /**\n * Reset all metrics\n */\n reset(): void {\n this.frames = [];\n this.lastTime = performance.now();\n this.frameCount = 0;\n this.minFps = Infinity;\n this.maxFps = 0;\n this.performanceWarnings = [];\n }\n\n /**\n * Get formatted performance summary string\n */\n getSummary(renderer?: THREE.WebGLRenderer): string {\n const metrics = this.getMetrics(renderer);\n return (\n `FPS: ${metrics.fps.toFixed(1)} | ` +\n `Avg: ${metrics.avgFps.toFixed(1)} | ` +\n `Min: ${metrics.minFps.toFixed(1)} | ` +\n `Max: ${metrics.maxFps.toFixed(1)} | ` +\n `Frame: ${metrics.frameTime.toFixed(2)}ms | ` +\n `Mem: ${metrics.memoryMB.toFixed(1)}MB | ` +\n `Draws: ${metrics.drawCalls} | ` +\n `Tris: ${(metrics.triangles / 1000).toFixed(1)}k`\n );\n }\n}\n\n/**\n * Create a performance monitor with optional custom thresholds\n */\nexport function createPerformanceMonitor(\n thresholds?: Partial<PerformanceThresholds>,\n): PerformanceMonitor {\n return new PerformanceMonitor(thresholds);\n}\n\nexport default PerformanceMonitor;\n"],"mappings":"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"}
1
+ {"version":3,"file":"PerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/PerformanceMonitor.ts"],"sourcesContent":["/**\n * PerformanceMonitor - Real-time FPS and performance tracking for Three.js\n *\n * Monitors frame rate, memory usage, and draw calls to maintain 60fps target.\n * Provides warnings in development mode when performance degrades.\n */\n\nimport * as THREE from \"three\";\n\nexport interface PerformanceMetrics {\n readonly fps: number;\n readonly avgFps: number;\n readonly minFps: number;\n readonly maxFps: number;\n readonly frameTime: number;\n readonly memoryMB: number;\n readonly drawCalls: number;\n readonly triangles: number;\n}\n\nexport interface PerformanceThresholds {\n readonly targetFps: number;\n readonly minAcceptableFps: number;\n readonly maxMemoryMB: number;\n readonly maxDrawCalls: number;\n}\n\nconst DEFAULT_THRESHOLDS: PerformanceThresholds = {\n targetFps: 60,\n minAcceptableFps: 55,\n maxMemoryMB: 300,\n maxDrawCalls: 100,\n};\n\n// Prevent unrealistic spikes when frame deltas are extremely small (e.g., mocked timers)\nconst MAX_SAMPLE_FPS = 180;\nconst MIN_FRAME_TIME_MS = 1000 / MAX_SAMPLE_FPS;\n\n/**\n * PerformanceMonitor class for real-time performance tracking\n */\nexport class PerformanceMonitor {\n private frames: number[] = [];\n private lastTime = performance.now();\n private frameCount = 0;\n private readonly maxFrameSamples = 60; // Track last 60 frames (1 second at 60fps)\n\n private minFps = Infinity;\n private maxFps = 0;\n\n private readonly thresholds: PerformanceThresholds;\n private performanceWarnings: string[] = [];\n\n constructor(thresholds: Partial<PerformanceThresholds> = {}) {\n this.thresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update performance metrics (call once per frame)\n * @param renderer Optional Three.js renderer for draw call tracking\n * @returns Current FPS\n */\n update(renderer?: THREE.WebGLRenderer): number {\n const now = performance.now();\n const delta = now - this.lastTime;\n this.lastTime = now;\n\n if (delta <= 0) return 0; // Skip invalid frames\n\n const clampedDelta = Math.max(delta, MIN_FRAME_TIME_MS);\n const fps = Math.min(1000 / clampedDelta, MAX_SAMPLE_FPS);\n this.frames.push(fps);\n\n // Track min/max FPS\n if (fps < this.minFps) this.minFps = fps;\n if (fps > this.maxFps) this.maxFps = fps;\n\n // Keep frames array bounded\n if (this.frames.length > this.maxFrameSamples) {\n this.frames.shift();\n }\n\n this.frameCount++;\n\n // Check performance thresholds (only in dev mode)\n if (import.meta.env.DEV && this.frameCount % 60 === 0) {\n this.checkPerformanceThresholds(renderer);\n }\n\n return fps;\n }\n\n /**\n * Get current FPS\n */\n getCurrentFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames[this.frames.length - 1];\n }\n\n /**\n * Get average FPS over the sampling window\n */\n getAverageFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames.reduce((a, b) => a + b, 0) / this.frames.length;\n }\n\n /**\n * Get minimum FPS recorded\n */\n getMinFPS(): number {\n return this.minFps === Infinity ? 0 : this.minFps;\n }\n\n /**\n * Get maximum FPS recorded\n */\n getMaxFPS(): number {\n return this.maxFps;\n }\n\n /**\n * Check if performance is good (above minimum threshold)\n */\n isPerformanceGood(): boolean {\n const avgFps = this.getAverageFPS();\n return avgFps >= this.thresholds.minAcceptableFps;\n }\n\n /**\n * Get comprehensive performance metrics\n */\n getMetrics(renderer?: THREE.WebGLRenderer): PerformanceMetrics {\n const avgFps = this.getAverageFPS();\n const currentFps = this.getCurrentFPS();\n\n return {\n fps: currentFps,\n avgFps,\n minFps: this.getMinFPS(),\n maxFps: this.getMaxFPS(),\n frameTime: currentFps > 0 ? 1000 / currentFps : 0,\n memoryMB: this.getMemoryUsageMB(),\n drawCalls: renderer?.info?.render?.calls ?? 0,\n triangles: renderer?.info?.render?.triangles ?? 0,\n };\n }\n\n /**\n * Get current memory usage in MB (Chrome only)\n */\n private getMemoryUsageMB(): number {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Chrome-specific performance.memory API not in standard TS types\n const perf = performance as any;\n if (perf.memory) {\n return perf.memory.usedJSHeapSize / 1024 / 1024;\n }\n return 0;\n }\n\n /**\n * Check performance thresholds and log warnings\n */\n private checkPerformanceThresholds(renderer?: THREE.WebGLRenderer): void {\n this.performanceWarnings = [];\n\n const avgFps = this.getAverageFPS();\n const memoryMB = this.getMemoryUsageMB();\n const drawCalls = renderer?.info?.render?.calls ?? 0;\n\n // Check FPS\n if (avgFps < this.thresholds.minAcceptableFps) {\n const warning = `⚠️ FPS below threshold: ${avgFps.toFixed(1)} < ${\n this.thresholds.minAcceptableFps\n }`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check memory\n if (memoryMB > 0 && memoryMB > this.thresholds.maxMemoryMB) {\n const warning = `⚠️ Memory usage high: ${memoryMB.toFixed(1)}MB > ${\n this.thresholds.maxMemoryMB\n }MB`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check draw calls\n if (drawCalls > this.thresholds.maxDrawCalls) {\n const warning = `⚠️ Draw calls high: ${drawCalls} > ${this.thresholds.maxDrawCalls}`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n }\n\n /**\n * Get current performance warnings\n */\n getWarnings(): readonly string[] {\n return this.performanceWarnings;\n }\n\n /**\n * Reset all metrics\n */\n reset(): void {\n this.frames = [];\n this.lastTime = performance.now();\n this.frameCount = 0;\n this.minFps = Infinity;\n this.maxFps = 0;\n this.performanceWarnings = [];\n }\n\n /**\n * Get formatted performance summary string\n */\n getSummary(renderer?: THREE.WebGLRenderer): string {\n const metrics = this.getMetrics(renderer);\n return (\n `FPS: ${metrics.fps.toFixed(1)} | ` +\n `Avg: ${metrics.avgFps.toFixed(1)} | ` +\n `Min: ${metrics.minFps.toFixed(1)} | ` +\n `Max: ${metrics.maxFps.toFixed(1)} | ` +\n `Frame: ${metrics.frameTime.toFixed(2)}ms | ` +\n `Mem: ${metrics.memoryMB.toFixed(1)}MB | ` +\n `Draws: ${metrics.drawCalls} | ` +\n `Tris: ${(metrics.triangles / 1000).toFixed(1)}k`\n );\n }\n}\n\n/**\n * Create a performance monitor with optional custom thresholds\n */\nexport function createPerformanceMonitor(\n thresholds?: Partial<PerformanceThresholds>,\n): PerformanceMonitor {\n return new PerformanceMonitor(thresholds);\n}\n\nexport default PerformanceMonitor;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PerformanceOverlay3D.js","names":[],"sources":["../../../src/utils/performance/PerformanceOverlay3D.tsx"],"sourcesContent":["/**\n * PerformanceOverlay3D - Development-only performance stats overlay for Three.js\n *\n * Displays real-time FPS, memory, and draw call statistics\n * Only visible in development mode\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React from \"react\";\nimport { usePerformanceMonitor } from \"./usePerformanceMonitor\";\n\nexport interface PerformanceOverlay3DProps {\n readonly visible?: boolean;\n}\n\n/**\n * 3D Performance overlay component for development\n * Shows FPS, memory, draw calls, and warnings\n * Uses screen-space positioning (bottom-left corner)\n */\nexport const PerformanceOverlay3D: React.FC<PerformanceOverlay3DProps> = ({\n visible = import.meta.env.DEV,\n}) => {\n const { metrics, isGood, warnings } = usePerformanceMonitor({\n enabled: visible,\n updateInterval: 500, // Update UI every 500ms\n });\n\n if (!visible) {\n return null;\n }\n\n const fpsColor = isGood\n ? \"#00ff88\"\n : metrics.avgFps < 30\n ? \"#ff4444\"\n : \"#ffaa00\";\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n bottom: \"20px\",\n left: \"20px\",\n backgroundColor: \"rgba(0, 0, 0, 0.85)\",\n border: \"2px solid #00ffff\",\n borderRadius: \"8px\",\n padding: \"12px 16px\",\n fontFamily: \"monospace\",\n fontSize: \"12px\",\n color: \"#00ffff\",\n minWidth: \"220px\",\n maxWidth: \"280px\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n zIndex: 200,\n }}\n data-testid=\"performance-overlay\"\n >\n {/* Title */}\n <div\n style={{\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n borderBottom: \"1px solid #00ffff\",\n paddingBottom: \"4px\",\n }}\n >\n ⚡ Performance Monitor\n </div>\n\n {/* FPS Stats */}\n <div style={{ marginBottom: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>FPS:</span>\n <span style={{ color: fpsColor, fontWeight: \"bold\" }}>\n {metrics.fps.toFixed(1)}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Avg FPS:</span>\n <span style={{ color: fpsColor }}>{metrics.avgFps.toFixed(1)}</span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Min/Max:</span>\n <span>\n {metrics.minFps.toFixed(1)} / {metrics.maxFps.toFixed(1)}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Frame Time:</span>\n <span>{metrics.frameTime.toFixed(2)}ms</span>\n </div>\n </div>\n\n {/* System Stats */}\n <div\n style={{\n marginBottom: \"8px\",\n borderTop: \"1px solid #444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Memory:</span>\n <span\n style={{\n color:\n metrics.memoryMB > 300\n ? \"#ff4444\"\n : metrics.memoryMB > 250\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.memoryMB > 0\n ? `${metrics.memoryMB.toFixed(1)}MB`\n : \"N/A\"}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Draw Calls:</span>\n <span\n style={{\n color:\n metrics.drawCalls > 150\n ? \"#ff4444\"\n : metrics.drawCalls > 100\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.drawCalls}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Triangles:</span>\n <span>{(metrics.triangles / 1000).toFixed(1)}k</span>\n </div>\n </div>\n\n {/* Performance Status */}\n <div style={{ borderTop: \"1px solid #444\", paddingTop: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n }}\n >\n <span\n style={{\n width: \"12px\",\n height: \"12px\",\n borderRadius: \"50%\",\n backgroundColor: isGood ? \"#00ff88\" : \"#ff4444\",\n display: \"inline-block\",\n }}\n />\n <span style={{ fontWeight: \"bold\" }}>\n {isGood ? \"Performance Good\" : \"Performance Degraded\"}\n </span>\n </div>\n </div>\n\n {/* Warnings */}\n {warnings.length > 0 && (\n <div\n style={{\n marginTop: \"8px\",\n borderTop: \"1px solid #ff4444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n color: \"#ff4444\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n >\n ⚠️ Warnings:\n </div>\n {warnings.map((warning, index) => (\n <div\n key={index}\n style={{\n fontSize: \"11px\",\n color: \"#ffaa00\",\n marginBottom: \"2px\",\n }}\n >\n {warning}\n </div>\n ))}\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nexport default PerformanceOverlay3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"PerformanceOverlay3D.js","names":[],"sources":["../../../src/utils/performance/PerformanceOverlay3D.tsx"],"sourcesContent":["/**\n * PerformanceOverlay3D - Development-only performance stats overlay for Three.js\n *\n * Displays real-time FPS, memory, and draw call statistics\n * Only visible in development mode\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React from \"react\";\nimport { usePerformanceMonitor } from \"./usePerformanceMonitor\";\n\nexport interface PerformanceOverlay3DProps {\n readonly visible?: boolean;\n}\n\n/**\n * 3D Performance overlay component for development\n * Shows FPS, memory, draw calls, and warnings\n * Uses screen-space positioning (bottom-left corner)\n */\nexport const PerformanceOverlay3D: React.FC<PerformanceOverlay3DProps> = ({\n visible = import.meta.env.DEV,\n}) => {\n const { metrics, isGood, warnings } = usePerformanceMonitor({\n enabled: visible,\n updateInterval: 500, // Update UI every 500ms\n });\n\n if (!visible) {\n return null;\n }\n\n const fpsColor = isGood\n ? \"#00ff88\"\n : metrics.avgFps < 30\n ? \"#ff4444\"\n : \"#ffaa00\";\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n bottom: \"20px\",\n left: \"20px\",\n backgroundColor: \"rgba(0, 0, 0, 0.85)\",\n border: \"2px solid #00ffff\",\n borderRadius: \"8px\",\n padding: \"12px 16px\",\n fontFamily: \"monospace\",\n fontSize: \"12px\",\n color: \"#00ffff\",\n minWidth: \"220px\",\n maxWidth: \"280px\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n zIndex: 200,\n }}\n data-testid=\"performance-overlay\"\n >\n {/* Title */}\n <div\n style={{\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n borderBottom: \"1px solid #00ffff\",\n paddingBottom: \"4px\",\n }}\n >\n ⚡ Performance Monitor\n </div>\n\n {/* FPS Stats */}\n <div style={{ marginBottom: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>FPS:</span>\n <span style={{ color: fpsColor, fontWeight: \"bold\" }}>\n {metrics.fps.toFixed(1)}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Avg FPS:</span>\n <span style={{ color: fpsColor }}>{metrics.avgFps.toFixed(1)}</span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Min/Max:</span>\n <span>\n {metrics.minFps.toFixed(1)} / {metrics.maxFps.toFixed(1)}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Frame Time:</span>\n <span>{metrics.frameTime.toFixed(2)}ms</span>\n </div>\n </div>\n\n {/* System Stats */}\n <div\n style={{\n marginBottom: \"8px\",\n borderTop: \"1px solid #444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Memory:</span>\n <span\n style={{\n color:\n metrics.memoryMB > 300\n ? \"#ff4444\"\n : metrics.memoryMB > 250\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.memoryMB > 0\n ? `${metrics.memoryMB.toFixed(1)}MB`\n : \"N/A\"}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Draw Calls:</span>\n <span\n style={{\n color:\n metrics.drawCalls > 150\n ? \"#ff4444\"\n : metrics.drawCalls > 100\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.drawCalls}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Triangles:</span>\n <span>{(metrics.triangles / 1000).toFixed(1)}k</span>\n </div>\n </div>\n\n {/* Performance Status */}\n <div style={{ borderTop: \"1px solid #444\", paddingTop: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n }}\n >\n <span\n style={{\n width: \"12px\",\n height: \"12px\",\n borderRadius: \"50%\",\n backgroundColor: isGood ? \"#00ff88\" : \"#ff4444\",\n display: \"inline-block\",\n }}\n />\n <span style={{ fontWeight: \"bold\" }}>\n {isGood ? \"Performance Good\" : \"Performance Degraded\"}\n </span>\n </div>\n </div>\n\n {/* Warnings */}\n {warnings.length > 0 && (\n <div\n style={{\n marginTop: \"8px\",\n borderTop: \"1px solid #ff4444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n color: \"#ff4444\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n >\n ⚠️ Warnings:\n </div>\n {warnings.map((warning, index) => (\n <div\n key={index}\n style={{\n fontSize: \"11px\",\n color: \"#ffaa00\",\n marginBottom: \"2px\",\n }}\n >\n {warning}\n </div>\n ))}\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nexport default PerformanceOverlay3D;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"usePerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/usePerformanceMonitor.ts"],"sourcesContent":["/**\n * React hook for Three.js performance monitoring\n * \n * Integrates PerformanceMonitor with useFrame for real-time FPS tracking\n */\n\nimport { useFrame, useThree } from '@react-three/fiber';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { PerformanceMonitor, PerformanceMetrics, PerformanceThresholds } from './PerformanceMonitor';\n\nexport interface UsePerformanceMonitorOptions {\n readonly enabled?: boolean;\n readonly thresholds?: Partial<PerformanceThresholds>;\n readonly updateInterval?: number; // Update interval in ms for React state\n}\n\nexport interface PerformanceMonitorState {\n readonly metrics: PerformanceMetrics;\n readonly isGood: boolean;\n readonly warnings: readonly string[];\n}\n\n/**\n * Hook for monitoring Three.js performance in real-time\n * \n * @param options Configuration options\n * @returns Current performance state\n * \n * @example\n * ```tsx\n * function CombatScene() {\n * const { metrics, isGood, warnings } = usePerformanceMonitor({\n * enabled: import.meta.env.DEV,\n * thresholds: { minAcceptableFps: 55 }\n * });\n * \n * return (\n * <>\n * {import.meta.env.DEV && (\n * <Html position={[0, 5, 0]}>\n * <div>FPS: {metrics.fps.toFixed(1)}</div>\n * </Html>\n * )}\n * {/* 3D content */}\n * </>\n * );\n * }\n * ```\n */\nexport function usePerformanceMonitor(\n options: UsePerformanceMonitorOptions = {}\n): PerformanceMonitorState {\n const { enabled = true, thresholds, updateInterval = 1000 } = options;\n\n const gl = useThree((state) => state.gl);\n \n // Memoize thresholds to prevent unnecessary monitor recreation\n const stableThresholds = useMemo(\n () => thresholds,\n [thresholds]\n );\n\n const monitor = useMemo(\n () => new PerformanceMonitor(stableThresholds),\n [stableThresholds]\n );\n\n const [state, setState] = useState<PerformanceMonitorState>(() => ({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n }));\n\n const lastUpdateRef = useRef(0);\n\n // Update performance monitor every frame\n useFrame(() => {\n if (!enabled) return;\n\n monitor.update(gl);\n\n // Update React state at specified interval\n const now = performance.now();\n if (now - lastUpdateRef.current >= updateInterval) {\n lastUpdateRef.current = now;\n \n setState({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n });\n }\n });\n\n // Reset monitor on unmount\n useEffect(() => {\n return () => {\n monitor.reset();\n };\n }, [monitor]);\n\n return state;\n}\n\nexport default usePerformanceMonitor;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,sBACd,UAAwC,EAAE,EACjB;CACzB,MAAM,EAAE,UAAU,MAAM,YAAY,iBAAiB,QAAS;CAE9D,MAAM,KAAK,UAAU,UAAU,MAAM,GAAG;CAGxC,MAAM,mBAAmB,cACjB,YACN,CAAC,WAAW,CACb;CAED,MAAM,UAAU,cACR,IAAI,mBAAmB,iBAAiB,EAC9C,CAAC,iBAAiB,CACnB;CAED,MAAM,CAAC,OAAO,YAAY,gBAAyC;EACjE,SAAS,QAAQ,WAAW,GAAG;EAC/B,QAAQ,QAAQ,mBAAmB;EACnC,UAAU,QAAQ,aAAa;EAChC,EAAE;CAEH,MAAM,gBAAgB,OAAO,EAAE;CAG/B,eAAe;EACb,IAAI,CAAC,SAAS;EAEd,QAAQ,OAAO,GAAG;EAGlB,MAAM,MAAM,YAAY,KAAK;EAC7B,IAAI,MAAM,cAAc,WAAW,gBAAgB;GACjD,cAAc,UAAU;GAExB,SAAS;IACP,SAAS,QAAQ,WAAW,GAAG;IAC/B,QAAQ,QAAQ,mBAAmB;IACnC,UAAU,QAAQ,aAAa;IAChC,CAAC;;GAEJ;CAGF,gBAAgB;EACd,aAAa;GACX,QAAQ,OAAO;;IAEhB,CAAC,QAAQ,CAAC;CAEb,OAAO"}
1
+ {"version":3,"file":"usePerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/usePerformanceMonitor.ts"],"sourcesContent":["/**\n * React hook for Three.js performance monitoring\n * \n * Integrates PerformanceMonitor with useFrame for real-time FPS tracking\n */\n\nimport { useFrame, useThree } from '@react-three/fiber';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { PerformanceMonitor, PerformanceMetrics, PerformanceThresholds } from './PerformanceMonitor';\n\nexport interface UsePerformanceMonitorOptions {\n readonly enabled?: boolean;\n readonly thresholds?: Partial<PerformanceThresholds>;\n readonly updateInterval?: number; // Update interval in ms for React state\n}\n\nexport interface PerformanceMonitorState {\n readonly metrics: PerformanceMetrics;\n readonly isGood: boolean;\n readonly warnings: readonly string[];\n}\n\n/**\n * Hook for monitoring Three.js performance in real-time\n * \n * @param options Configuration options\n * @returns Current performance state\n * \n * @example\n * ```tsx\n * function CombatScene() {\n * const { metrics, isGood, warnings } = usePerformanceMonitor({\n * enabled: import.meta.env.DEV,\n * thresholds: { minAcceptableFps: 55 }\n * });\n * \n * return (\n * <>\n * {import.meta.env.DEV && (\n * <Html position={[0, 5, 0]}>\n * <div>FPS: {metrics.fps.toFixed(1)}</div>\n * </Html>\n * )}\n * {/* 3D content */}\n * </>\n * );\n * }\n * ```\n */\nexport function usePerformanceMonitor(\n options: UsePerformanceMonitorOptions = {}\n): PerformanceMonitorState {\n const { enabled = true, thresholds, updateInterval = 1000 } = options;\n\n const gl = useThree((state) => state.gl);\n \n // Memoize thresholds to prevent unnecessary monitor recreation\n const stableThresholds = useMemo(\n () => thresholds,\n [thresholds]\n );\n\n const monitor = useMemo(\n () => new PerformanceMonitor(stableThresholds),\n [stableThresholds]\n );\n\n const [state, setState] = useState<PerformanceMonitorState>(() => ({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n }));\n\n const lastUpdateRef = useRef(0);\n\n // Update performance monitor every frame\n useFrame(() => {\n if (!enabled) return;\n\n monitor.update(gl);\n\n // Update React state at specified interval\n const now = performance.now();\n if (now - lastUpdateRef.current >= updateInterval) {\n lastUpdateRef.current = now;\n \n setState({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n });\n }\n });\n\n // Reset monitor on unmount\n useEffect(() => {\n return () => {\n monitor.reset();\n };\n }, [monitor]);\n\n return state;\n}\n\nexport default usePerformanceMonitor;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,sBACd,UAAwC,CAAC,GAChB;CACzB,MAAM,EAAE,UAAU,MAAM,YAAY,iBAAiB,QAAS;CAE9D,MAAM,KAAK,UAAU,UAAU,MAAM,EAAE;CAGvC,MAAM,mBAAmB,cACjB,YACN,CAAC,UAAU,CACb;CAEA,MAAM,UAAU,cACR,IAAI,mBAAmB,gBAAgB,GAC7C,CAAC,gBAAgB,CACnB;CAEA,MAAM,CAAC,OAAO,YAAY,gBAAyC;EACjE,SAAS,QAAQ,WAAW,EAAE;EAC9B,QAAQ,QAAQ,kBAAkB;EAClC,UAAU,QAAQ,YAAY;CAChC,EAAE;CAEF,MAAM,gBAAgB,OAAO,CAAC;CAG9B,eAAe;EACb,IAAI,CAAC,SAAS;EAEd,QAAQ,OAAO,EAAE;EAGjB,MAAM,MAAM,YAAY,IAAI;EAC5B,IAAI,MAAM,cAAc,WAAW,gBAAgB;GACjD,cAAc,UAAU;GAExB,SAAS;IACP,SAAS,QAAQ,WAAW,EAAE;IAC9B,QAAQ,QAAQ,kBAAkB;IAClC,UAAU,QAAQ,YAAY;GAChC,CAAC;EACH;CACF,CAAC;CAGD,gBAAgB;EACd,aAAa;GACX,QAAQ,MAAM;EAChB;CACF,GAAG,CAAC,OAAO,CAAC;CAEZ,OAAO;AACT"}