blacktrigram 0.7.47 → 0.7.48

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 (450) 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 +22 -11
  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.js.map +1 -1
  38. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  39. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  40. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  41. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  42. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  43. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  44. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  45. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  46. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  47. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  48. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  49. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  50. package/lib/components/screens/combat/helpers/AnimationUpdater.d.ts.map +1 -1
  51. package/lib/components/screens/combat/helpers/AnimationUpdater.js +4 -2
  52. package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
  53. package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
  54. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  55. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  56. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  57. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  58. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  59. package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
  60. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  61. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  62. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  63. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  64. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  65. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  66. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  67. package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
  68. package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
  69. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  70. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  71. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  72. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  74. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  75. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  76. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  77. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  78. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  79. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  80. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  81. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  83. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  84. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  85. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  86. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  87. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  88. package/lib/components/screens/training/TrainingScreen3D.d.ts.map +1 -1
  89. package/lib/components/screens/training/TrainingScreen3D.js +1 -0
  90. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  91. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  92. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  93. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  94. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  95. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  96. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  97. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  98. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  99. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  100. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  101. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  102. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  103. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  104. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  105. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  106. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  107. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  108. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  109. package/lib/components/screens/training/hooks/useTrainingActions.d.ts +1 -0
  110. package/lib/components/screens/training/hooks/useTrainingActions.d.ts.map +1 -1
  111. package/lib/components/screens/training/hooks/useTrainingActions.js +6 -4
  112. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  113. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  114. package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
  115. package/lib/components/shared/base/BaseButton.js.map +1 -1
  116. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  117. package/lib/components/shared/base/BasePanel.js.map +1 -1
  118. package/lib/components/shared/base/BaseText.js.map +1 -1
  119. package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
  120. package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
  121. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  122. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  123. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  124. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  125. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  126. package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
  127. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  128. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  129. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  130. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  131. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  132. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  133. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  134. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  135. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  136. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  137. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  138. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  139. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  140. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  141. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  142. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  143. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  144. package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
  145. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  146. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  147. package/lib/components/shared/three/models/SkeletalPlayer3D.d.ts.map +1 -1
  148. package/lib/components/shared/three/models/SkeletalPlayer3D.js +7 -5
  149. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  150. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  151. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  152. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  153. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  154. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  155. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  156. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  157. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  158. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  159. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  160. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  161. package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
  162. package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
  163. package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
  164. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  165. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  166. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  167. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  168. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  169. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  170. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  171. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  172. package/lib/components/shared/ui/BackButton.js.map +1 -1
  173. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  174. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  175. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  176. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  177. package/lib/components/shared/ui/SplashScreen.js +2 -2
  178. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  179. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  180. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  181. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  182. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  183. package/lib/constants/bodyDimensions.js.map +1 -1
  184. package/lib/constants/bodyRenderingConstants.js.map +1 -1
  185. package/lib/data/archetypeClothing.js.map +1 -1
  186. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  187. package/lib/data/techniqueMappings.js.map +1 -1
  188. package/lib/data/techniques.js.map +1 -1
  189. package/lib/hooks/useActionFeedback.js.map +1 -1
  190. package/lib/hooks/useBalanceAnimations.js.map +1 -1
  191. package/lib/hooks/useCombatTimer.js.map +1 -1
  192. package/lib/hooks/useDebounce.js.map +1 -1
  193. package/lib/hooks/useHUDLayout.js.map +1 -1
  194. package/lib/hooks/useHandPoseTransitions.js.map +1 -1
  195. package/lib/hooks/useKeyboardControls.js.map +1 -1
  196. package/lib/hooks/useMatchCountdown.js.map +1 -1
  197. package/lib/hooks/useMuscleActivation.js.map +1 -1
  198. package/lib/hooks/usePauseMenu.js.map +1 -1
  199. package/lib/hooks/usePlayerAnimation.js.map +1 -1
  200. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  201. package/lib/hooks/useRoundTransition.js.map +1 -1
  202. package/lib/hooks/useSkeletalAnimation.d.ts.map +1 -1
  203. package/lib/hooks/useSkeletalAnimation.js +1 -1
  204. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  205. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  206. package/lib/hooks/useThrottle.js.map +1 -1
  207. package/lib/hooks/useTouchControls.js.map +1 -1
  208. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  209. package/lib/hooks/useWindowSize.js.map +1 -1
  210. package/lib/systems/CombatSystem.js.map +1 -1
  211. package/lib/systems/EffectCalculator.js.map +1 -1
  212. package/lib/systems/LayoutSystem.js.map +1 -1
  213. package/lib/systems/PlayerEffectManager.js.map +1 -1
  214. package/lib/systems/ResponsiveScaling.js.map +1 -1
  215. package/lib/systems/TrigramSystem.js.map +1 -1
  216. package/lib/systems/VitalPointSystem.js.map +1 -1
  217. package/lib/systems/ai/AIPersonality.js.map +1 -1
  218. package/lib/systems/ai/AdaptiveDifficulty.js +16 -16
  219. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  220. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  221. package/lib/systems/ai/ComboSystem.js.map +1 -1
  222. package/lib/systems/ai/DecisionTree.js.map +1 -1
  223. package/lib/systems/ai/TrainingAI.js.map +1 -1
  224. package/lib/systems/ai/types.js.map +1 -1
  225. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  226. package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
  227. package/lib/systems/animation/builders/HandPoses.js.map +1 -1
  228. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  229. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  230. package/lib/systems/animation/builders/KickPhaseApplicator.d.ts +6 -0
  231. package/lib/systems/animation/builders/KickPhaseApplicator.d.ts.map +1 -1
  232. package/lib/systems/animation/builders/KickPhaseApplicator.js +16 -9
  233. package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
  234. package/lib/systems/animation/builders/KoreanGuardPositions.d.ts +4 -4
  235. package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
  236. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts +1 -1
  237. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts.map +1 -1
  238. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js +5 -5
  239. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  240. package/lib/systems/animation/builders/MartialArtsConstants.d.ts +112 -71
  241. package/lib/systems/animation/builders/MartialArtsConstants.d.ts.map +1 -1
  242. package/lib/systems/animation/builders/MartialArtsConstants.js +113 -72
  243. package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
  244. package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
  245. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  246. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  247. package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
  248. package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
  249. package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
  250. package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
  251. package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
  252. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  253. package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
  254. package/lib/systems/animation/catalogs/EnhancedAttackAnimations.js.map +1 -1
  255. package/lib/systems/animation/catalogs/EnhancedElbowKneeAnimations.js.map +1 -1
  256. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  257. package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
  258. package/lib/systems/animation/catalogs/GamStanceAnimations.js +21 -0
  259. package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -0
  260. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js +34 -2
  261. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
  262. package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
  263. package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
  264. package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
  265. package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts +9 -0
  266. package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts.map +1 -1
  267. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +288 -0
  268. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -0
  269. package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
  270. package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
  271. package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
  272. package/lib/systems/animation/catalogs/KickAnimations.d.ts +2 -2
  273. package/lib/systems/animation/catalogs/KickAnimations.js +2 -2
  274. package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
  275. package/lib/systems/animation/catalogs/LiStanceAnimations.js +14 -1
  276. package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
  277. package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
  278. package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
  279. package/lib/systems/animation/catalogs/PunchAnimations.d.ts +1 -1
  280. package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
  281. package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
  282. package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
  283. package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
  284. package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
  285. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
  286. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  287. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  288. package/lib/systems/animation/catalogs/StanceGuardPoses.d.ts +6 -6
  289. package/lib/systems/animation/catalogs/StanceGuardPoses.js +36 -36
  290. package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
  291. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  292. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  293. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  294. package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
  295. package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
  296. package/lib/systems/animation/constants/AnatomicalLimits.js.map +1 -1
  297. package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
  298. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  299. package/lib/systems/animation/core/AnimationPriority.js +15 -15
  300. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  301. package/lib/systems/animation/core/AnimationRegistry.d.ts +30 -0
  302. package/lib/systems/animation/core/AnimationRegistry.d.ts.map +1 -1
  303. package/lib/systems/animation/core/AnimationRegistry.js +74 -12
  304. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  305. package/lib/systems/animation/core/AnimationStateMachine.js +16 -16
  306. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  307. package/lib/systems/animation/core/AnimationTransitions.d.ts.map +1 -1
  308. package/lib/systems/animation/core/AnimationTransitions.js +34 -0
  309. package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
  310. package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
  311. package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
  312. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  313. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  314. package/lib/systems/animation/core/index.d.ts +1 -1
  315. package/lib/systems/animation/core/index.d.ts.map +1 -1
  316. package/lib/systems/animation/core/types.d.ts +24 -0
  317. package/lib/systems/animation/core/types.d.ts.map +1 -1
  318. package/lib/systems/animation/core/types.js +27 -11
  319. package/lib/systems/animation/core/types.js.map +1 -1
  320. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  321. package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
  322. package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
  323. package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
  324. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  325. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  326. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  327. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  328. package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
  329. package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
  330. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  331. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  332. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  333. package/lib/systems/bodypart/types.js.map +1 -1
  334. package/lib/systems/breathing/BreathingDisruptionSystem.js +19 -19
  335. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  336. package/lib/systems/breathing/feedback.js.map +1 -1
  337. package/lib/systems/breathing/integration.js.map +1 -1
  338. package/lib/systems/combat/BalanceSystem.js +19 -19
  339. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  340. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  341. package/lib/systems/combat/CombatStateSystem.js +17 -17
  342. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  343. package/lib/systems/combat/ConsciousnessSystem.js +24 -24
  344. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  345. package/lib/systems/combat/FallIntegration.js.map +1 -1
  346. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  347. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  348. package/lib/systems/combat/PainResponseSystem.js +21 -21
  349. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  350. package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
  351. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  352. package/lib/systems/combat/typeGuards.js.map +1 -1
  353. package/lib/systems/effects.js.map +1 -1
  354. package/lib/systems/game.js.map +1 -1
  355. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  356. package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
  357. package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
  358. package/lib/systems/movement/integration.js.map +1 -1
  359. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  360. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  361. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  362. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  363. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  364. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  365. package/lib/systems/physics/SpeedModifierSystem.js +6 -6
  366. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  367. package/lib/systems/trigram/KoreanCulture.js.map +1 -1
  368. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  369. package/lib/systems/trigram/StanceManager.js.map +1 -1
  370. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  371. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  372. package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
  373. package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
  374. package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
  375. package/lib/systems/trigram/techniques/GeonTechniques.js.map +1 -1
  376. package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
  377. package/lib/systems/trigram/techniques/JinTechniques.js.map +1 -1
  378. package/lib/systems/trigram/techniques/LiTechniques.js.map +1 -1
  379. package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
  380. package/lib/systems/trigram/techniques/TaeTechniques.js.map +1 -1
  381. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  382. package/lib/systems/trigram/techniques/index.js.map +1 -1
  383. package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
  384. package/lib/systems/trigram/types.js.map +1 -1
  385. package/lib/systems/types.js.map +1 -1
  386. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  387. package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
  388. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  389. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  390. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  391. package/lib/systems/vitalpoint/VitalPointsData.js.map +1 -1
  392. package/lib/types/AccessibilityTypes.js.map +1 -1
  393. package/lib/types/LayoutTypes.js.map +1 -1
  394. package/lib/types/PhysicsTypes.js.map +1 -1
  395. package/lib/types/common.js.map +1 -1
  396. package/lib/types/constants/animations.js.map +1 -1
  397. package/lib/types/constants/colors.js.map +1 -1
  398. package/lib/types/constants/designSystem.js.map +1 -1
  399. package/lib/types/constants/index.js.map +1 -1
  400. package/lib/types/constants/layout.js.map +1 -1
  401. package/lib/types/constants/performance.js.map +1 -1
  402. package/lib/types/constants/typography.js.map +1 -1
  403. package/lib/types/constants/ui.js.map +1 -1
  404. package/lib/types/facial.js +19 -19
  405. package/lib/types/facial.js.map +1 -1
  406. package/lib/types/hand-animation.js.map +1 -1
  407. package/lib/types/injury.js.map +1 -1
  408. package/lib/types/muscle.js.map +1 -1
  409. package/lib/types/physics.js.map +1 -1
  410. package/lib/types/physicsConstants.js.map +1 -1
  411. package/lib/types/player-visual.d.ts +1 -1
  412. package/lib/types/player-visual.d.ts.map +1 -1
  413. package/lib/types/skeletal.js.map +1 -1
  414. package/lib/types/techniqueId.js.map +1 -1
  415. package/lib/utils/accessibility.js.map +1 -1
  416. package/lib/utils/arenaWorldDimensions.js.map +1 -1
  417. package/lib/utils/assetConfig.js.map +1 -1
  418. package/lib/utils/characterScaling.js.map +1 -1
  419. package/lib/utils/colorHelpers.js.map +1 -1
  420. package/lib/utils/colorUtils.js.map +1 -1
  421. package/lib/utils/combatReadiness.js.map +1 -1
  422. package/lib/utils/controlMapping.js.map +1 -1
  423. package/lib/utils/deviceDetection.js +6 -7
  424. package/lib/utils/deviceDetection.js.map +1 -1
  425. package/lib/utils/effectUtils.js.map +1 -1
  426. package/lib/utils/fabricTextures.js.map +1 -1
  427. package/lib/utils/hapticFeedback.js.map +1 -1
  428. package/lib/utils/haptics.js.map +1 -1
  429. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  430. package/lib/utils/inputSystem.js.map +1 -1
  431. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  432. package/lib/utils/math.js.map +1 -1
  433. package/lib/utils/mobileLayoutHelpers.js.map +1 -1
  434. package/lib/utils/mobileUIUtils.js.map +1 -1
  435. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  436. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  437. package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
  438. package/lib/utils/performanceOptimization.js.map +1 -1
  439. package/lib/utils/player3DHelpers.js.map +1 -1
  440. package/lib/utils/playerUtils.js.map +1 -1
  441. package/lib/utils/responsiveLayout.js.map +1 -1
  442. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  443. package/lib/utils/responsiveOrientationConstants.js.map +1 -1
  444. package/lib/utils/safeAreaUtils.js.map +1 -1
  445. package/lib/utils/sharedPhysicsConfig.js.map +1 -1
  446. package/lib/utils/skeletonScaling.js.map +1 -1
  447. package/lib/utils/stanceHelpers.js.map +1 -1
  448. package/lib/utils/threeObjectPool.js.map +1 -1
  449. package/lib/utils/visualEffects.js.map +1 -1
  450. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"CombatButtons.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/CombatButtons.tsx"],"sourcesContent":["/**\n * CombatButtons - Reusable button components for CombatScreen\n * \n * Provides return-to-menu button with combat-specific styling.\n * Extracted from CombatScreen3D to reduce code duplication.\n * \n * @module components/screens/combat\n * @category Combat UI\n * @korean 전투버튼\n */\n\nimport React, { useMemo } from \"react\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { BaseButtonOverlayHtml } from \"../../../../shared/base/BaseButtonOverlayHtml\";\n\nexport interface CombatReturnToMenuButtonProps {\n /** Callback when button is clicked */\n readonly onClick: () => void;\n /** Callback when mouse enters button */\n readonly onMouseEnter?: () => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * CombatReturnToMenuButton Component\n * \n * Bilingual button to return to main menu from combat screen.\n * Uses BaseButtonOverlayHtml with custom container for combat-specific styling.\n * \n * Refactored to use BaseButtonOverlayHtml for better consistency.\n * \n * @example\n * ```tsx\n * <CombatReturnToMenuButton\n * onClick={() => navigate('/menu')}\n * onMouseEnter={() => playSound()}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReturnToMenuButton: React.FC<\n CombatReturnToMenuButtonProps\n> = ({ onClick, onMouseEnter, isMobile }) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"md\", isMobile });\n\n const containerStyle = useMemo(() => ({\n background: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.85),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n }), [theme.colors.UI_BACKGROUND_DARK, theme.colors.PRIMARY_CYAN]);\n\n return (\n <div\n style={{\n textAlign: \"center\",\n background: containerStyle.background,\n border: containerStyle.border,\n borderRadius: \"8px\",\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n }}\n >\n <BaseButtonOverlayHtml\n korean={isMobile ? \"메뉴\" : \"메뉴로\"}\n english={isMobile ? \"Menu\" : \"Return to Menu\"}\n onClick={onClick}\n onMouseEnter={onMouseEnter}\n variant=\"primary\"\n size=\"md\"\n isMobile={isMobile}\n testId=\"return-to-menu-button\"\n />\n </div>\n );\n};\n\nexport default CombatReturnToMenuButton;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAa,4BAER,EAAE,SAAS,cAAc,eAAe;CAC3C,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;EAAU,CAAC;CAE1E,MAAM,iBAAiB,eAAe;EACpC,YAAY,gBAAgB,MAAM,OAAO,oBAAoB,IAAK;EAClE,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,GAAI;EACrE,GAAG,CAAC,MAAM,OAAO,oBAAoB,MAAM,OAAO,aAAa,CAAC;CAEjE,OACE,oBAAC,OAAD;EACE,OAAO;GACL,WAAW;GACX,YAAY,eAAe;GAC3B,QAAQ,eAAe;GACvB,cAAc;GACd,SAAS,WAAW,aAAa;GAClC;YAED,oBAAC,uBAAD;GACE,QAAQ,WAAW,OAAO;GAC1B,SAAS,WAAW,SAAS;GACpB;GACK;GACd,SAAQ;GACR,MAAK;GACK;GACV,QAAO;GACP,CAAA;EACE,CAAA"}
1
+ {"version":3,"file":"CombatButtons.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/CombatButtons.tsx"],"sourcesContent":["/**\n * CombatButtons - Reusable button components for CombatScreen\n * \n * Provides return-to-menu button with combat-specific styling.\n * Extracted from CombatScreen3D to reduce code duplication.\n * \n * @module components/screens/combat\n * @category Combat UI\n * @korean 전투버튼\n */\n\nimport React, { useMemo } from \"react\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { BaseButtonOverlayHtml } from \"../../../../shared/base/BaseButtonOverlayHtml\";\n\nexport interface CombatReturnToMenuButtonProps {\n /** Callback when button is clicked */\n readonly onClick: () => void;\n /** Callback when mouse enters button */\n readonly onMouseEnter?: () => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * CombatReturnToMenuButton Component\n * \n * Bilingual button to return to main menu from combat screen.\n * Uses BaseButtonOverlayHtml with custom container for combat-specific styling.\n * \n * Refactored to use BaseButtonOverlayHtml for better consistency.\n * \n * @example\n * ```tsx\n * <CombatReturnToMenuButton\n * onClick={() => navigate('/menu')}\n * onMouseEnter={() => playSound()}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReturnToMenuButton: React.FC<\n CombatReturnToMenuButtonProps\n> = ({ onClick, onMouseEnter, isMobile }) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"md\", isMobile });\n\n const containerStyle = useMemo(() => ({\n background: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.85),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n }), [theme.colors.UI_BACKGROUND_DARK, theme.colors.PRIMARY_CYAN]);\n\n return (\n <div\n style={{\n textAlign: \"center\",\n background: containerStyle.background,\n border: containerStyle.border,\n borderRadius: \"8px\",\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n }}\n >\n <BaseButtonOverlayHtml\n korean={isMobile ? \"메뉴\" : \"메뉴로\"}\n english={isMobile ? \"Menu\" : \"Return to Menu\"}\n onClick={onClick}\n onMouseEnter={onMouseEnter}\n variant=\"primary\"\n size=\"md\"\n isMobile={isMobile}\n testId=\"return-to-menu-button\"\n />\n </div>\n );\n};\n\nexport default CombatReturnToMenuButton;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAa,4BAER,EAAE,SAAS,cAAc,eAAe;CAC3C,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;CAAS,CAAC;CAEzE,MAAM,iBAAiB,eAAe;EACpC,YAAY,gBAAgB,MAAM,OAAO,oBAAoB,GAAI;EACjE,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,EAAG;CACrE,IAAI,CAAC,MAAM,OAAO,oBAAoB,MAAM,OAAO,YAAY,CAAC;CAEhE,OACE,oBAAC,OAAD;EACE,OAAO;GACL,WAAW;GACX,YAAY,eAAe;GAC3B,QAAQ,eAAe;GACvB,cAAc;GACd,SAAS,WAAW,aAAa;EACnC;YAEA,oBAAC,uBAAD;GACE,QAAQ,WAAW,OAAO;GAC1B,SAAS,WAAW,SAAS;GACpB;GACK;GACd,SAAQ;GACR,MAAK;GACK;GACV,QAAO;EACR,CAAA;CACE,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"CombatControlsPanel.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/CombatControlsPanel.tsx"],"sourcesContent":["/**\n * CombatControlsPanel - Displays controls guide and combat messages log\n *\n * Shows keyboard/touch controls on the left and scrolling combat log on the right.\n * Positioned at the bottom of the combat screen above the back button.\n *\n * Refactored to use useKoreanTheme for consistent theming.\n *\n * @module components/combat/components/CombatControlsPanel\n * @category Combat UI\n * @korean 전투컨트롤패널\n */\n\nimport React from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\n/** Ratio of screen height used for message log max height */\nconst MESSAGE_LOG_HEIGHT_RATIO = 0.18;\n\nexport interface CombatControlsPanelProps {\n /** Combat message log (most recent messages) */\n readonly combatMessages: readonly string[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n /** Screen height for responsive sizing */\n readonly height?: number;\n}\n\n/**\n * CombatControlsPanel - Controls guide and combat log display\n * \n * Uses useKoreanTheme for consistent styling.\n *\n * @example\n * ```tsx\n * <CombatControlsPanel\n * combatMessages={combatState.combatMessages}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatControlsPanel: React.FC<CombatControlsPanelProps> = ({\n combatMessages,\n isMobile,\n height,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"sm\", isMobile });\n const panelBackground = hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.8);\n \n return (\n <div\n data-testid=\"combat-controls-panel\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"90px\" : \"100px\",\n left: isMobile ? \"5px\" : \"15px\",\n right: isMobile ? \"5px\" : \"15px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n pointerEvents: \"auto\",\n zIndex: 50,\n }}\n >\n {/* Controls Guide */}\n <div\n data-testid=\"combat-controls-guide\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n }}\n >\n <div style={{ fontSize: isMobile ? \"10px\" : \"12px\" }}>\n 조작법 | Controls: A/D - Attack/Defend | 1-8 - Stances\n </div>\n </div>\n\n {/* Combat Message Log */}\n <div\n data-testid=\"combat-message-log\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n maxHeight: height ? `${Math.round(height * MESSAGE_LOG_HEIGHT_RATIO)}px` : \"18vh\",\n overflow: \"auto\",\n }}\n >\n {combatMessages.slice(-5).map((msg, idx) => (\n <div\n key={`msg-${idx}`}\n style={{ fontSize: \"12px\", marginBottom: \"4px\" }}\n >\n {msg}\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nexport default CombatControlsPanel;\n"],"mappings":";;;;;;AAkBA,IAAM,2BAA2B;;;;;;;;;;;;;;AAwBjC,IAAa,uBAA2D,EACtE,gBACA,UACA,aACI;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;EAAU,CAAC;CAC1E,MAAM,kBAAkB,gBAAgB,MAAM,OAAO,oBAAoB,GAAI;CAE7E,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,UAAU;GACV,QAAQ,WAAW,SAAS;GAC5B,MAAM,WAAW,QAAQ;GACzB,OAAO,WAAW,QAAQ;GAC1B,SAAS;GACT,gBAAgB;GAChB,eAAe;GACf,QAAQ;GACT;YAXH,CAcE,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,EAAE;IAClE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,EAAE;IACpD,YAAY,MAAM,WAAW;IAC9B;aAED,oBAAC,OAAD;IAAK,OAAO,EAAE,UAAU,WAAW,SAAS,QAAQ;cAAE;IAEhD,CAAA;GACF,CAAA,EAGN,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,EAAE;IAClE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,EAAE;IACpD,YAAY,MAAM,WAAW;IAC7B,WAAW,SAAS,GAAG,KAAK,MAAM,SAAS,yBAAyB,CAAC,MAAM;IAC3E,UAAU;IACX;aAEA,eAAe,MAAM,GAAG,CAAC,KAAK,KAAK,QAClC,oBAAC,OAAD;IAEE,OAAO;KAAE,UAAU;KAAQ,cAAc;KAAO;cAE/C;IACG,EAJC,OAAO,MAIR,CACN;GACE,CAAA,CACF"}
1
+ {"version":3,"file":"CombatControlsPanel.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/CombatControlsPanel.tsx"],"sourcesContent":["/**\n * CombatControlsPanel - Displays controls guide and combat messages log\n *\n * Shows keyboard/touch controls on the left and scrolling combat log on the right.\n * Positioned at the bottom of the combat screen above the back button.\n *\n * Refactored to use useKoreanTheme for consistent theming.\n *\n * @module components/combat/components/CombatControlsPanel\n * @category Combat UI\n * @korean 전투컨트롤패널\n */\n\nimport React from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\n/** Ratio of screen height used for message log max height */\nconst MESSAGE_LOG_HEIGHT_RATIO = 0.18;\n\nexport interface CombatControlsPanelProps {\n /** Combat message log (most recent messages) */\n readonly combatMessages: readonly string[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n /** Screen height for responsive sizing */\n readonly height?: number;\n}\n\n/**\n * CombatControlsPanel - Controls guide and combat log display\n * \n * Uses useKoreanTheme for consistent styling.\n *\n * @example\n * ```tsx\n * <CombatControlsPanel\n * combatMessages={combatState.combatMessages}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatControlsPanel: React.FC<CombatControlsPanelProps> = ({\n combatMessages,\n isMobile,\n height,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"sm\", isMobile });\n const panelBackground = hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.8);\n \n return (\n <div\n data-testid=\"combat-controls-panel\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"90px\" : \"100px\",\n left: isMobile ? \"5px\" : \"15px\",\n right: isMobile ? \"5px\" : \"15px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n pointerEvents: \"auto\",\n zIndex: 50,\n }}\n >\n {/* Controls Guide */}\n <div\n data-testid=\"combat-controls-guide\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n }}\n >\n <div style={{ fontSize: isMobile ? \"10px\" : \"12px\" }}>\n 조작법 | Controls: A/D - Attack/Defend | 1-8 - Stances\n </div>\n </div>\n\n {/* Combat Message Log */}\n <div\n data-testid=\"combat-message-log\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n maxHeight: height ? `${Math.round(height * MESSAGE_LOG_HEIGHT_RATIO)}px` : \"18vh\",\n overflow: \"auto\",\n }}\n >\n {combatMessages.slice(-5).map((msg, idx) => (\n <div\n key={`msg-${idx}`}\n style={{ fontSize: \"12px\", marginBottom: \"4px\" }}\n >\n {msg}\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nexport default CombatControlsPanel;\n"],"mappings":";;;;;;AAkBA,IAAM,2BAA2B;;;;;;;;;;;;;;AAwBjC,IAAa,uBAA2D,EACtE,gBACA,UACA,aACI;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;CAAS,CAAC;CACzE,MAAM,kBAAkB,gBAAgB,MAAM,OAAO,oBAAoB,EAAG;CAE5E,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,UAAU;GACV,QAAQ,WAAW,SAAS;GAC5B,MAAM,WAAW,QAAQ;GACzB,OAAO,WAAW,QAAQ;GAC1B,SAAS;GACT,gBAAgB;GAChB,eAAe;GACf,QAAQ;EACV;YAXF,CAcE,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACjE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACnD,YAAY,MAAM,WAAW;GAC/B;aAEA,oBAAC,OAAD;IAAK,OAAO,EAAE,UAAU,WAAW,SAAS,OAAO;cAAG;GAEjD,CAAA;EACF,CAAA,GAGL,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACjE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACnD,YAAY,MAAM,WAAW;IAC7B,WAAW,SAAS,GAAG,KAAK,MAAM,SAAS,wBAAwB,EAAE,MAAM;IAC3E,UAAU;GACZ;aAEC,eAAe,MAAM,EAAE,EAAE,KAAK,KAAK,QAClC,oBAAC,OAAD;IAEE,OAAO;KAAE,UAAU;KAAQ,cAAc;IAAM;cAE9C;GACE,GAJE,OAAO,KAIT,CACN;EACE,CAAA,CACF;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"ControlsGuide.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/ControlsGuide.tsx"],"sourcesContent":["/**\n * ControlsGuide Component - In-game controls reference\n * \n * Features:\n * - Complete combat controls listing\n * - Korean/English bilingual labels\n * - Cyberpunk Korean theming via useKoreanTheme\n * - Organized by action type\n * \n * Refactored to use useKoreanTheme for consistent theming.\n */\n\nimport React from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\nexport interface ControlsGuideProps {\n readonly onClose: () => void;\n readonly isMobile: boolean;\n}\n\ninterface ControlMapping {\n readonly key: string;\n readonly actionKorean: string;\n readonly actionEnglish: string;\n}\n\nconst CONTROL_MAPPINGS: ControlMapping[] = [\n { key: \"WASD / ←↑↓→\", actionKorean: \"이동\", actionEnglish: \"Movement\" },\n { key: \"1-8\", actionKorean: \"팔괘 자세\", actionEnglish: \"Trigram Stances\" },\n { key: \"Space\", actionKorean: \"공격\", actionEnglish: \"Attack\" },\n { key: \"Shift\", actionKorean: \"방어\", actionEnglish: \"Defend\" },\n { key: \"Tab\", actionKorean: \"원형 전환\", actionEnglish: \"Switch Archetype\" },\n { key: \"ESC\", actionKorean: \"일시정지\", actionEnglish: \"Pause\" },\n { key: \"M\", actionKorean: \"음소거\", actionEnglish: \"Mute\" },\n];\n\n/**\n * ControlsGuide - In-game controls reference overlay\n * Uses useKoreanTheme for consistent styling.\n */\nexport const ControlsGuide: React.FC<ControlsGuideProps> = ({\n onClose,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"md\", isMobile });\n\n return (\n <div\n data-testid=\"controls-guide\"\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n padding: isMobile ? \"24px\" : \"32px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"12px\",\n minWidth: isMobile ? \"280px\" : \"400px\",\n maxHeight: isMobile ? \"70vh\" : \"80vh\",\n overflow: \"auto\",\n boxShadow: `0 0 30px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n zIndex: 1000,\n }}\n >\n {/* Title */}\n <h2\n data-testid=\"controls-title\"\n style={{\n fontSize: isMobile ? \"20px\" : \"24px\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 24px 0\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.4)}`,\n }}\n >\n 조작법 | Controls\n </h2>\n\n {/* Controls List */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n }}\n >\n {CONTROL_MAPPINGS.map((control, index) => (\n <div\n key={index}\n data-testid={`control-item-${index}`}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: isMobile ? \"10px\" : \"12px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_MEDIUM, 0.5),\n border: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n borderRadius: \"6px\",\n }}\n >\n <div\n style={{\n flex: 1,\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n }}\n >\n {control.actionKorean}\n <br />\n <span\n style={{\n fontSize: isMobile ? \"11px\" : \"13px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontWeight: \"normal\",\n }}\n >\n {control.actionEnglish}\n </span>\n </div>\n <div\n style={{\n padding: isMobile ? \"6px 12px\" : \"8px 16px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1),\n backgroundColor: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.MONO,\n fontWeight: \"bold\",\n borderRadius: \"4px\",\n textAlign: \"center\",\n minWidth: isMobile ? \"80px\" : \"100px\",\n }}\n >\n {control.key}\n </div>\n </div>\n ))}\n </div>\n\n {/* Additional Tips */}\n <div\n data-testid=\"controls-tips\"\n style={{\n marginTop: isMobile ? \"20px\" : \"24px\",\n padding: isMobile ? \"12px\" : \"16px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_MEDIUM, 0.3),\n border: `1px solid ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.3)}`,\n borderRadius: \"6px\",\n }}\n >\n <h3\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 8px 0\",\n }}\n >\n 💡 팁 | Tips\n </h3>\n <p\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontFamily: theme.fontFamily.KOREAN,\n margin: 0,\n lineHeight: \"1.6\",\n }}\n >\n • 각 팔괘 자세는 고유한 기술과 장점이 있습니다\n <br />\n • Each trigram stance has unique techniques and advantages\n <br />\n <br />\n • 적절한 타이밍에 방어하면 반격 기회를 얻습니다\n <br />\n • Perfect timing on defense creates counter opportunities\n </p>\n </div>\n\n {/* Close Button */}\n <button\n onClick={() => {\n audio.playSFX(\"menu_back\");\n onClose();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n data-testid=\"controls-close-button\"\n style={{\n marginTop: isMobile ? \"20px\" : \"24px\",\n width: \"100%\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n color: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1),\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.ACCENT_GOLD,\n 1\n );\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1\n );\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n >\n 닫기 | Close\n </button>\n </div>\n );\n};\n\nexport default ControlsGuide;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ControlsGuide.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/ControlsGuide.tsx"],"sourcesContent":["/**\n * ControlsGuide Component - In-game controls reference\n * \n * Features:\n * - Complete combat controls listing\n * - Korean/English bilingual labels\n * - Cyberpunk Korean theming via useKoreanTheme\n * - Organized by action type\n * \n * Refactored to use useKoreanTheme for consistent theming.\n */\n\nimport React from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\nexport interface ControlsGuideProps {\n readonly onClose: () => void;\n readonly isMobile: boolean;\n}\n\ninterface ControlMapping {\n readonly key: string;\n readonly actionKorean: string;\n readonly actionEnglish: string;\n}\n\nconst CONTROL_MAPPINGS: ControlMapping[] = [\n { key: \"WASD / ←↑↓→\", actionKorean: \"이동\", actionEnglish: \"Movement\" },\n { key: \"1-8\", actionKorean: \"팔괘 자세\", actionEnglish: \"Trigram Stances\" },\n { key: \"Space\", actionKorean: \"공격\", actionEnglish: \"Attack\" },\n { key: \"Shift\", actionKorean: \"방어\", actionEnglish: \"Defend\" },\n { key: \"Tab\", actionKorean: \"원형 전환\", actionEnglish: \"Switch Archetype\" },\n { key: \"ESC\", actionKorean: \"일시정지\", actionEnglish: \"Pause\" },\n { key: \"M\", actionKorean: \"음소거\", actionEnglish: \"Mute\" },\n];\n\n/**\n * ControlsGuide - In-game controls reference overlay\n * Uses useKoreanTheme for consistent styling.\n */\nexport const ControlsGuide: React.FC<ControlsGuideProps> = ({\n onClose,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"md\", isMobile });\n\n return (\n <div\n data-testid=\"controls-guide\"\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n padding: isMobile ? \"24px\" : \"32px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"12px\",\n minWidth: isMobile ? \"280px\" : \"400px\",\n maxHeight: isMobile ? \"70vh\" : \"80vh\",\n overflow: \"auto\",\n boxShadow: `0 0 30px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n zIndex: 1000,\n }}\n >\n {/* Title */}\n <h2\n data-testid=\"controls-title\"\n style={{\n fontSize: isMobile ? \"20px\" : \"24px\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 24px 0\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.4)}`,\n }}\n >\n 조작법 | Controls\n </h2>\n\n {/* Controls List */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n }}\n >\n {CONTROL_MAPPINGS.map((control, index) => (\n <div\n key={index}\n data-testid={`control-item-${index}`}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: isMobile ? \"10px\" : \"12px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_MEDIUM, 0.5),\n border: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n borderRadius: \"6px\",\n }}\n >\n <div\n style={{\n flex: 1,\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n }}\n >\n {control.actionKorean}\n <br />\n <span\n style={{\n fontSize: isMobile ? \"11px\" : \"13px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontWeight: \"normal\",\n }}\n >\n {control.actionEnglish}\n </span>\n </div>\n <div\n style={{\n padding: isMobile ? \"6px 12px\" : \"8px 16px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1),\n backgroundColor: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.MONO,\n fontWeight: \"bold\",\n borderRadius: \"4px\",\n textAlign: \"center\",\n minWidth: isMobile ? \"80px\" : \"100px\",\n }}\n >\n {control.key}\n </div>\n </div>\n ))}\n </div>\n\n {/* Additional Tips */}\n <div\n data-testid=\"controls-tips\"\n style={{\n marginTop: isMobile ? \"20px\" : \"24px\",\n padding: isMobile ? \"12px\" : \"16px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_MEDIUM, 0.3),\n border: `1px solid ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.3)}`,\n borderRadius: \"6px\",\n }}\n >\n <h3\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 8px 0\",\n }}\n >\n 💡 팁 | Tips\n </h3>\n <p\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontFamily: theme.fontFamily.KOREAN,\n margin: 0,\n lineHeight: \"1.6\",\n }}\n >\n • 각 팔괘 자세는 고유한 기술과 장점이 있습니다\n <br />\n • Each trigram stance has unique techniques and advantages\n <br />\n <br />\n • 적절한 타이밍에 방어하면 반격 기회를 얻습니다\n <br />\n • Perfect timing on defense creates counter opportunities\n </p>\n </div>\n\n {/* Close Button */}\n <button\n onClick={() => {\n audio.playSFX(\"menu_back\");\n onClose();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n data-testid=\"controls-close-button\"\n style={{\n marginTop: isMobile ? \"20px\" : \"24px\",\n width: \"100%\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n color: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1),\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.ACCENT_GOLD,\n 1\n );\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1\n );\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n >\n 닫기 | Close\n </button>\n </div>\n );\n};\n\nexport default ControlsGuide;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"KeyboardHints.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/KeyboardHints.tsx"],"sourcesContent":["/**\n * KeyboardHints - On-screen control overlay\n * Displays keyboard bindings for trigram stances and combat actions\n * \n * Refactored to use useKoreanTheme for consistent theming\n * \n * @module components/combat/components/KeyboardHints\n * @category Combat UI\n * @korean 키보드힌트\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { ControlBinding } from \"../../../../../utils/controlMapping\";\n\n/**\n * Props for KeyboardHints component\n */\nexport interface KeyboardHintsProps {\n /** Whether hints are visible */\n readonly visible: boolean;\n /** Current stance index (0-7) for highlighting */\n readonly currentStance: number;\n /** Mobile layout flag */\n readonly isMobile?: boolean;\n /** Custom key bindings */\n readonly customBindings?: ControlBinding;\n}\n\n/**\n * KeyboardHints Component\n * \n * Displays an overlay of keyboard controls for stance switching and combat actions.\n * Highlights the currently active stance and adapts layout for mobile devices.\n * \n * Features:\n * - 8 trigram stance keys displayed in a grid\n * - Current stance highlighted in gold\n * - Combat action keys (attack, block, movement)\n * - Responsive mobile layout\n * - Korean cyberpunk styling\n * - Toggle with F1 key\n * \n * @example\n * ```tsx\n * <KeyboardHints\n * visible={showHints}\n * currentStance={player.stance}\n * isMobile={isMobile}\n * customBindings={controlMapper.getBindings()}\n * />\n * ```\n * \n * @korean 키보드힌트\n */\nexport const KeyboardHints: React.FC<KeyboardHintsProps> = ({\n visible,\n currentStance,\n isMobile = false,\n customBindings,\n}) => {\n const theme = useKoreanTheme({ variant: \"bordered\", size: \"md\", isMobile });\n \n const stanceKeys = useMemo(() => {\n if (customBindings?.stances) {\n return customBindings.stances;\n }\n return [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"];\n }, [customBindings]);\n\n const layout = useMemo(() => {\n const keySize = isMobile ? 32 : 48;\n const gap = isMobile ? 4 : 8;\n const fontSize = isMobile ? 12 : 16;\n const labelFontSize = isMobile ? 10 : 12;\n const padding = isMobile ? 8 : 12;\n\n return { keySize, gap, fontSize, labelFontSize, padding };\n }, [isMobile]);\n\n if (!visible) return null;\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"keyboard-hints\"\n role=\"dialog\"\n aria-label=\"Keyboard control hints\"\n aria-describedby=\"hints-description\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"20px\" : \"40px\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n pointerEvents: \"none\",\n zIndex: 999,\n }}\n >\n {/* Hidden description for screen readers */}\n <div\n id=\"hints-description\"\n style={{\n position: \"absolute\",\n left: \"-9999px\",\n width: \"1px\",\n height: \"1px\",\n overflow: \"hidden\",\n }}\n >\n Keyboard controls for combat. Press F1 to toggle this overlay.\n </div>\n\n {/* Main hints container */}\n <div\n style={{\n background: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.9),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"8px\",\n padding: `${layout.padding}px`,\n boxShadow: `0 0 20px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n }}\n >\n {/* Section title */}\n <div\n style={{\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: `${layout.labelFontSize}px`,\n textAlign: \"center\",\n marginBottom: \"8px\",\n fontWeight: \"bold\",\n }}\n >\n Trigram Stances (1-8)\n </div>\n\n {/* Stance keys grid */}\n <div\n style={{\n display: \"flex\",\n gap: `${layout.gap}px`,\n marginBottom: \"12px\",\n }}\n >\n {stanceKeys.map((key, index) => {\n const isActive = index === currentStance;\n const keyColor = isActive\n ? theme.colors.ACCENT_GOLD\n : theme.colors.UI_STEEL_GRAY;\n const bgColor = isActive\n ? hexToRgbaString(theme.colors.ACCENT_GOLD, 0.2)\n : \"transparent\";\n\n return (\n <div\n key={index}\n data-testid={`stance-key-${index}`}\n style={{\n width: `${layout.keySize}px`,\n height: `${layout.keySize}px`,\n border: `2px solid ${hexToRgbaString(keyColor, isActive ? 1 : 0.6)}`,\n borderRadius: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: `${layout.fontSize}px`,\n color: hexToRgbaString(keyColor, 1),\n background: bgColor,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 0 10px ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.5)}`\n : \"none\",\n transition: \"all 0.2s ease\",\n }}\n >\n {key.toUpperCase()}\n </div>\n );\n })}\n </div>\n\n {/* Combat actions */}\n <div\n style={{\n borderTop: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n paddingTop: \"8px\",\n marginBottom: \"8px\",\n display: \"grid\",\n gridTemplateColumns: isMobile ? \"1fr\" : \"1fr 1fr\",\n gap: \"4px\",\n fontSize: `${layout.labelFontSize}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.9),\n fontFamily: theme.fontFamily.KOREAN,\n }}\n >\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Space\n </span>{\" \"}\n - Attack\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n B\n </span>{\" \"}\n - Block\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n V\n </span>{\" \"}\n - Toggle vital points overlay\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n WASD/Arrows\n </span>{\" \"}\n - Move\n </div>\n </div>\n\n {/* Technique keys section */}\n <div\n style={{\n borderTop: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n paddingTop: \"8px\",\n marginBottom: \"8px\",\n }}\n >\n <div\n style={{\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: `${layout.labelFontSize}px`,\n textAlign: \"center\",\n marginBottom: \"6px\",\n fontWeight: \"bold\",\n }}\n >\n Techniques (기술)\n </div>\n <div\n style={{\n fontSize: `${layout.labelFontSize}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.9),\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Q-E-R-T-Y-F-G-Z-X-C\n </span>\n {\" \"}\n - Execute techniques\n </div>\n </div>\n\n {/* Advanced footwork section */}\n <div\n style={{\n borderTop: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: `${layout.labelFontSize}px`,\n textAlign: \"center\",\n marginBottom: \"6px\",\n fontWeight: \"bold\",\n }}\n >\n Advanced Footwork (보법)\n </div>\n <div\n style={{\n fontSize: `${layout.labelFontSize - 1}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.9),\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n lineHeight: 1.4,\n }}\n >\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Shift+WASD\n </span>\n {\" \"}\n - Tactical steps (30cm)\n </div>\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Ctrl+WASD\n </span>\n {\" \"}\n - Footwork patterns\n </div>\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Shift+Ctrl+A/D\n </span>\n {\" \"}\n - Pivot rotation\n </div>\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Shift+Ctrl+W/S\n </span>\n {\" \"}\n - Shuffle step\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n H\n </span>\n {\" \"}\n - Switch front foot\n </div>\n </div>\n </div>\n\n {/* Toggle hint */}\n <div\n style={{\n marginTop: \"8px\",\n textAlign: \"center\",\n fontSize: `${layout.labelFontSize - 2}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.7),\n fontStyle: \"italic\",\n }}\n >\n Press F1 to toggle • ESC/M for pause\n </div>\n </div>\n </div>\n </Html>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"KeyboardHints.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/KeyboardHints.tsx"],"sourcesContent":["/**\n * KeyboardHints - On-screen control overlay\n * Displays keyboard bindings for trigram stances and combat actions\n * \n * Refactored to use useKoreanTheme for consistent theming\n * \n * @module components/combat/components/KeyboardHints\n * @category Combat UI\n * @korean 키보드힌트\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { ControlBinding } from \"../../../../../utils/controlMapping\";\n\n/**\n * Props for KeyboardHints component\n */\nexport interface KeyboardHintsProps {\n /** Whether hints are visible */\n readonly visible: boolean;\n /** Current stance index (0-7) for highlighting */\n readonly currentStance: number;\n /** Mobile layout flag */\n readonly isMobile?: boolean;\n /** Custom key bindings */\n readonly customBindings?: ControlBinding;\n}\n\n/**\n * KeyboardHints Component\n * \n * Displays an overlay of keyboard controls for stance switching and combat actions.\n * Highlights the currently active stance and adapts layout for mobile devices.\n * \n * Features:\n * - 8 trigram stance keys displayed in a grid\n * - Current stance highlighted in gold\n * - Combat action keys (attack, block, movement)\n * - Responsive mobile layout\n * - Korean cyberpunk styling\n * - Toggle with F1 key\n * \n * @example\n * ```tsx\n * <KeyboardHints\n * visible={showHints}\n * currentStance={player.stance}\n * isMobile={isMobile}\n * customBindings={controlMapper.getBindings()}\n * />\n * ```\n * \n * @korean 키보드힌트\n */\nexport const KeyboardHints: React.FC<KeyboardHintsProps> = ({\n visible,\n currentStance,\n isMobile = false,\n customBindings,\n}) => {\n const theme = useKoreanTheme({ variant: \"bordered\", size: \"md\", isMobile });\n \n const stanceKeys = useMemo(() => {\n if (customBindings?.stances) {\n return customBindings.stances;\n }\n return [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"];\n }, [customBindings]);\n\n const layout = useMemo(() => {\n const keySize = isMobile ? 32 : 48;\n const gap = isMobile ? 4 : 8;\n const fontSize = isMobile ? 12 : 16;\n const labelFontSize = isMobile ? 10 : 12;\n const padding = isMobile ? 8 : 12;\n\n return { keySize, gap, fontSize, labelFontSize, padding };\n }, [isMobile]);\n\n if (!visible) return null;\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"keyboard-hints\"\n role=\"dialog\"\n aria-label=\"Keyboard control hints\"\n aria-describedby=\"hints-description\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"20px\" : \"40px\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n pointerEvents: \"none\",\n zIndex: 999,\n }}\n >\n {/* Hidden description for screen readers */}\n <div\n id=\"hints-description\"\n style={{\n position: \"absolute\",\n left: \"-9999px\",\n width: \"1px\",\n height: \"1px\",\n overflow: \"hidden\",\n }}\n >\n Keyboard controls for combat. Press F1 to toggle this overlay.\n </div>\n\n {/* Main hints container */}\n <div\n style={{\n background: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.9),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"8px\",\n padding: `${layout.padding}px`,\n boxShadow: `0 0 20px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n }}\n >\n {/* Section title */}\n <div\n style={{\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: `${layout.labelFontSize}px`,\n textAlign: \"center\",\n marginBottom: \"8px\",\n fontWeight: \"bold\",\n }}\n >\n Trigram Stances (1-8)\n </div>\n\n {/* Stance keys grid */}\n <div\n style={{\n display: \"flex\",\n gap: `${layout.gap}px`,\n marginBottom: \"12px\",\n }}\n >\n {stanceKeys.map((key, index) => {\n const isActive = index === currentStance;\n const keyColor = isActive\n ? theme.colors.ACCENT_GOLD\n : theme.colors.UI_STEEL_GRAY;\n const bgColor = isActive\n ? hexToRgbaString(theme.colors.ACCENT_GOLD, 0.2)\n : \"transparent\";\n\n return (\n <div\n key={index}\n data-testid={`stance-key-${index}`}\n style={{\n width: `${layout.keySize}px`,\n height: `${layout.keySize}px`,\n border: `2px solid ${hexToRgbaString(keyColor, isActive ? 1 : 0.6)}`,\n borderRadius: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: `${layout.fontSize}px`,\n color: hexToRgbaString(keyColor, 1),\n background: bgColor,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 0 10px ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.5)}`\n : \"none\",\n transition: \"all 0.2s ease\",\n }}\n >\n {key.toUpperCase()}\n </div>\n );\n })}\n </div>\n\n {/* Combat actions */}\n <div\n style={{\n borderTop: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n paddingTop: \"8px\",\n marginBottom: \"8px\",\n display: \"grid\",\n gridTemplateColumns: isMobile ? \"1fr\" : \"1fr 1fr\",\n gap: \"4px\",\n fontSize: `${layout.labelFontSize}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.9),\n fontFamily: theme.fontFamily.KOREAN,\n }}\n >\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Space\n </span>{\" \"}\n - Attack\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n B\n </span>{\" \"}\n - Block\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n V\n </span>{\" \"}\n - Toggle vital points overlay\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n WASD/Arrows\n </span>{\" \"}\n - Move\n </div>\n </div>\n\n {/* Technique keys section */}\n <div\n style={{\n borderTop: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n paddingTop: \"8px\",\n marginBottom: \"8px\",\n }}\n >\n <div\n style={{\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: `${layout.labelFontSize}px`,\n textAlign: \"center\",\n marginBottom: \"6px\",\n fontWeight: \"bold\",\n }}\n >\n Techniques (기술)\n </div>\n <div\n style={{\n fontSize: `${layout.labelFontSize}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.9),\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Q-E-R-T-Y-F-G-Z-X-C\n </span>\n {\" \"}\n - Execute techniques\n </div>\n </div>\n\n {/* Advanced footwork section */}\n <div\n style={{\n borderTop: `1px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: `${layout.labelFontSize}px`,\n textAlign: \"center\",\n marginBottom: \"6px\",\n fontWeight: \"bold\",\n }}\n >\n Advanced Footwork (보법)\n </div>\n <div\n style={{\n fontSize: `${layout.labelFontSize - 1}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.9),\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n lineHeight: 1.4,\n }}\n >\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Shift+WASD\n </span>\n {\" \"}\n - Tactical steps (30cm)\n </div>\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Ctrl+WASD\n </span>\n {\" \"}\n - Footwork patterns\n </div>\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Shift+Ctrl+A/D\n </span>\n {\" \"}\n - Pivot rotation\n </div>\n <div style={{ marginBottom: \"4px\" }}>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n Shift+Ctrl+W/S\n </span>\n {\" \"}\n - Shuffle step\n </div>\n <div>\n <span\n style={{\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n }}\n >\n H\n </span>\n {\" \"}\n - Switch front foot\n </div>\n </div>\n </div>\n\n {/* Toggle hint */}\n <div\n style={{\n marginTop: \"8px\",\n textAlign: \"center\",\n fontSize: `${layout.labelFontSize - 2}px`,\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.7),\n fontStyle: \"italic\",\n }}\n >\n Press F1 to toggle • ESC/M for pause\n </div>\n </div>\n </div>\n </Html>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PauseMenu.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenu.tsx"],"sourcesContent":["/**\n * PauseMenu Component - Main pause menu overlay\n *\n * Features:\n * - Resume combat\n * - Restart match\n * - View controls\n * - Adjust settings\n * - Return to menu (with confirmation)\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - WCAG 2.1 Level AA compliant\n * - Keyboard navigation (Arrow keys, Enter, Escape)\n * - Focus indicators with high contrast\n * - ARIA labels for screen readers\n * \n * Refactored to use useKoreanTheme for consistent theming\n */\n\nimport React, { useCallback, useMemo, useState, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport ConfirmDialog from \"../../../../shared/ui/shared/ConfirmDialog\";\nimport ControlsGuide from \"./ControlsGuide\";\nimport QuickSettings from \"./QuickSettings\";\nimport { handleKeyboardNav } from \"../../../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../../../types/AccessibilityTypes\";\nimport { PauseMenuButton } from \"./PauseMenuButton\";\n\nexport interface PauseMenuProps {\n readonly onResume: () => void;\n readonly onRestart: () => void;\n readonly onReturnToMenu: () => void;\n readonly isMobile: boolean;\n}\n\ninterface MenuItem {\n readonly key: string;\n readonly labelKorean: string;\n readonly labelEnglish: string;\n readonly testId: string;\n readonly onClick: () => void;\n readonly icon?: string;\n}\n\n/**\n * PauseMenu - Main pause menu with options and submenus\n */\nexport const PauseMenu: React.FC<PauseMenuProps> = ({\n onResume,\n onRestart,\n onReturnToMenu,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n const themeColors = useMemo(() => ({\n overlayBg: hexToRgbaString(theme.colors.BLACK_SOLID, 0.85),\n accentGold: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n accentGoldGlow: hexToRgbaString(theme.colors.ACCENT_GOLD, 0.6),\n textSecondary: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.8),\n }), [theme.colors.BLACK_SOLID, theme.colors.ACCENT_GOLD, theme.colors.TEXT_SECONDARY]);\n const [activeSubmenu, setActiveSubmenu] = useState<\n \"controls\" | \"settings\" | null\n >(null);\n const [showConfirm, setShowConfirm] = useState<\n \"restart\" | \"menu\" | null\n >(null);\n const [focusedIndex, setFocusedIndex] = useState<number>(0);\n const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const menuItems: MenuItem[] = React.useMemo(\n () => [\n {\n key: \"resume\",\n labelKorean: \"계속\",\n labelEnglish: \"Resume\",\n testId: \"pause-resume-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n onResume();\n },\n icon: \"▶️\",\n },\n {\n key: \"restart\",\n labelKorean: \"재시작\",\n labelEnglish: \"Restart Match\",\n testId: \"pause-restart-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"restart\");\n },\n icon: \"🔄\",\n },\n {\n key: \"controls\",\n labelKorean: \"조작법\",\n labelEnglish: \"Controls\",\n testId: \"pause-controls-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"controls\");\n },\n icon: \"🎮\",\n },\n {\n key: \"settings\",\n labelKorean: \"설정\",\n labelEnglish: \"Settings\",\n testId: \"pause-settings-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"settings\");\n },\n icon: \"⚙️\",\n },\n {\n key: \"menu\",\n labelKorean: \"메인 메뉴\",\n labelEnglish: \"Return to Menu\",\n testId: \"pause-menu-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"menu\");\n },\n icon: \"🏠\",\n },\n ],\n [audio, onResume]\n );\n\n useEffect(() => {\n if (buttonRefs.current[0]) {\n buttonRefs.current[0].focus();\n }\n }, [menuItems]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent, index: number) => {\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n menuItems[index].onClick();\n },\n onCancel: () => {\n onResume();\n },\n onNavigate: (direction) => {\n if (direction === 'up') {\n const newIndex = (index - 1 + menuItems.length) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n } else if (direction === 'down') {\n const newIndex = (index + 1) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n }\n },\n });\n },\n [menuItems, onResume]\n );\n\n\n return (\n <>\n {/* Main Pause Menu */}\n <div\n data-testid=\"pause-menu\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pause-title\"\n aria-describedby=\"pause-hint\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: themeColors.overlayBg,\n backdropFilter: \"blur(8px)\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n }}\n >\n {/* Pause Title */}\n <h1\n id=\"pause-title\"\n data-testid=\"pause-title\"\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: themeColors.accentGold,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"40px\" : \"60px\"} 0`,\n textShadow: `0 0 30px ${themeColors.accentGoldGlow}`,\n textAlign: \"center\",\n }}\n >\n 일시정지 | Paused\n </h1>\n\n {/* Menu Buttons */}\n <div\n role=\"menu\"\n aria-label={createBilingualLabel('일시정지 메뉴', 'Pause Menu').label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n }}\n >\n {menuItems.map((item, index) => (\n <PauseMenuButton\n key={item.key}\n ref={(el) => { buttonRefs.current[index] = el; }}\n labelKorean={item.labelKorean}\n labelEnglish={item.labelEnglish}\n icon={item.icon}\n onClick={item.onClick}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n onKeyDown={(e) => handleKeyDown(e, index)}\n onFocus={() => setFocusedIndex(index)}\n isFocused={focusedIndex === index}\n isMobile={isMobile}\n testId={item.testId}\n />\n ))}\n </div>\n\n {/* ESC hint */}\n <div\n id=\"pause-hint\"\n data-testid=\"pause-hint\"\n style={{\n marginTop: isMobile ? \"40px\" : \"60px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: themeColors.textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n ESC 키를 눌러 계속 | Press ESC to resume\n <br />\n ↑↓ 키로 이동 | Use ↑↓ to navigate\n </div>\n </div>\n\n {/* Controls Guide Submenu */}\n {activeSubmenu === \"controls\" && (\n <ControlsGuide\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Settings Submenu */}\n {activeSubmenu === \"settings\" && (\n <QuickSettings\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Restart Confirmation Dialog */}\n {showConfirm === \"restart\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Restart Match?\"\n titleKorean=\"경기를 재시작하시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 초기화됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onRestart();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Return to Menu Confirmation Dialog */}\n {showConfirm === \"menu\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Return to Menu?\"\n titleKorean=\"메인 메뉴로 돌아가시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 손실됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onReturnToMenu();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n </>\n );\n};\n\nexport default PauseMenu;\n"],"mappings":"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"}
1
+ {"version":3,"file":"PauseMenu.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenu.tsx"],"sourcesContent":["/**\n * PauseMenu Component - Main pause menu overlay\n *\n * Features:\n * - Resume combat\n * - Restart match\n * - View controls\n * - Adjust settings\n * - Return to menu (with confirmation)\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - WCAG 2.1 Level AA compliant\n * - Keyboard navigation (Arrow keys, Enter, Escape)\n * - Focus indicators with high contrast\n * - ARIA labels for screen readers\n * \n * Refactored to use useKoreanTheme for consistent theming\n */\n\nimport React, { useCallback, useMemo, useState, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport ConfirmDialog from \"../../../../shared/ui/shared/ConfirmDialog\";\nimport ControlsGuide from \"./ControlsGuide\";\nimport QuickSettings from \"./QuickSettings\";\nimport { handleKeyboardNav } from \"../../../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../../../types/AccessibilityTypes\";\nimport { PauseMenuButton } from \"./PauseMenuButton\";\n\nexport interface PauseMenuProps {\n readonly onResume: () => void;\n readonly onRestart: () => void;\n readonly onReturnToMenu: () => void;\n readonly isMobile: boolean;\n}\n\ninterface MenuItem {\n readonly key: string;\n readonly labelKorean: string;\n readonly labelEnglish: string;\n readonly testId: string;\n readonly onClick: () => void;\n readonly icon?: string;\n}\n\n/**\n * PauseMenu - Main pause menu with options and submenus\n */\nexport const PauseMenu: React.FC<PauseMenuProps> = ({\n onResume,\n onRestart,\n onReturnToMenu,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n const themeColors = useMemo(() => ({\n overlayBg: hexToRgbaString(theme.colors.BLACK_SOLID, 0.85),\n accentGold: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n accentGoldGlow: hexToRgbaString(theme.colors.ACCENT_GOLD, 0.6),\n textSecondary: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.8),\n }), [theme.colors.BLACK_SOLID, theme.colors.ACCENT_GOLD, theme.colors.TEXT_SECONDARY]);\n const [activeSubmenu, setActiveSubmenu] = useState<\n \"controls\" | \"settings\" | null\n >(null);\n const [showConfirm, setShowConfirm] = useState<\n \"restart\" | \"menu\" | null\n >(null);\n const [focusedIndex, setFocusedIndex] = useState<number>(0);\n const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const menuItems: MenuItem[] = React.useMemo(\n () => [\n {\n key: \"resume\",\n labelKorean: \"계속\",\n labelEnglish: \"Resume\",\n testId: \"pause-resume-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n onResume();\n },\n icon: \"▶️\",\n },\n {\n key: \"restart\",\n labelKorean: \"재시작\",\n labelEnglish: \"Restart Match\",\n testId: \"pause-restart-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"restart\");\n },\n icon: \"🔄\",\n },\n {\n key: \"controls\",\n labelKorean: \"조작법\",\n labelEnglish: \"Controls\",\n testId: \"pause-controls-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"controls\");\n },\n icon: \"🎮\",\n },\n {\n key: \"settings\",\n labelKorean: \"설정\",\n labelEnglish: \"Settings\",\n testId: \"pause-settings-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"settings\");\n },\n icon: \"⚙️\",\n },\n {\n key: \"menu\",\n labelKorean: \"메인 메뉴\",\n labelEnglish: \"Return to Menu\",\n testId: \"pause-menu-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"menu\");\n },\n icon: \"🏠\",\n },\n ],\n [audio, onResume]\n );\n\n useEffect(() => {\n if (buttonRefs.current[0]) {\n buttonRefs.current[0].focus();\n }\n }, [menuItems]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent, index: number) => {\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n menuItems[index].onClick();\n },\n onCancel: () => {\n onResume();\n },\n onNavigate: (direction) => {\n if (direction === 'up') {\n const newIndex = (index - 1 + menuItems.length) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n } else if (direction === 'down') {\n const newIndex = (index + 1) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n }\n },\n });\n },\n [menuItems, onResume]\n );\n\n\n return (\n <>\n {/* Main Pause Menu */}\n <div\n data-testid=\"pause-menu\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pause-title\"\n aria-describedby=\"pause-hint\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: themeColors.overlayBg,\n backdropFilter: \"blur(8px)\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n }}\n >\n {/* Pause Title */}\n <h1\n id=\"pause-title\"\n data-testid=\"pause-title\"\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: themeColors.accentGold,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"40px\" : \"60px\"} 0`,\n textShadow: `0 0 30px ${themeColors.accentGoldGlow}`,\n textAlign: \"center\",\n }}\n >\n 일시정지 | Paused\n </h1>\n\n {/* Menu Buttons */}\n <div\n role=\"menu\"\n aria-label={createBilingualLabel('일시정지 메뉴', 'Pause Menu').label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n }}\n >\n {menuItems.map((item, index) => (\n <PauseMenuButton\n key={item.key}\n ref={(el) => { buttonRefs.current[index] = el; }}\n labelKorean={item.labelKorean}\n labelEnglish={item.labelEnglish}\n icon={item.icon}\n onClick={item.onClick}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n onKeyDown={(e) => handleKeyDown(e, index)}\n onFocus={() => setFocusedIndex(index)}\n isFocused={focusedIndex === index}\n isMobile={isMobile}\n testId={item.testId}\n />\n ))}\n </div>\n\n {/* ESC hint */}\n <div\n id=\"pause-hint\"\n data-testid=\"pause-hint\"\n style={{\n marginTop: isMobile ? \"40px\" : \"60px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: themeColors.textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n ESC 키를 눌러 계속 | Press ESC to resume\n <br />\n ↑↓ 키로 이동 | Use ↑↓ to navigate\n </div>\n </div>\n\n {/* Controls Guide Submenu */}\n {activeSubmenu === \"controls\" && (\n <ControlsGuide\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Settings Submenu */}\n {activeSubmenu === \"settings\" && (\n <QuickSettings\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Restart Confirmation Dialog */}\n {showConfirm === \"restart\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Restart Match?\"\n titleKorean=\"경기를 재시작하시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 초기화됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onRestart();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Return to Menu Confirmation Dialog */}\n {showConfirm === \"menu\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Return to Menu?\"\n titleKorean=\"메인 메뉴로 돌아가시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 손실됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onReturnToMenu();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n </>\n );\n};\n\nexport default PauseMenu;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PauseMenuButton.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenuButton.tsx"],"sourcesContent":["/**\n * PauseMenuButton - Reusable button for pause menu items\n * \n * Provides consistent styling for pause menu action buttons with Korean theming.\n * Extracted from PauseMenu to reduce code duplication.\n * \n * Refactored to use useKoreanTheme for consistent theming.\n * \n * @module components/screens/combat/controls\n * @category Combat UI\n * @korean 일시정지메뉴버튼\n */\n\nimport React, { forwardRef } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { getFocusStyle } from \"../../../../../utils/accessibility\";\n\nexport interface PauseMenuButtonProps {\n /** Korean label text */\n readonly labelKorean: string;\n /** English label text */\n readonly labelEnglish: string;\n /** Optional icon/emoji */\n readonly icon?: string;\n /** Click handler */\n readonly onClick: () => void;\n /** Mouse enter handler */\n readonly onMouseEnter?: () => void;\n /** Key down handler */\n readonly onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;\n /** Focus handler */\n readonly onFocus?: () => void;\n /** Whether button is focused */\n readonly isFocused: boolean;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Test ID */\n readonly testId: string;\n}\n\n/**\n * PauseMenuButton Component\n * \n * Styled button for pause menu with:\n * - Korean/English bilingual text with icon support\n * - Cyan themed styling matching combat screen via useKoreanTheme\n * - Hover and focus effects with accessibility support\n * - Responsive sizing for mobile/desktop\n * \n * Reduces code duplication by ~70 lines from PauseMenu (inline button styling)\n * \n * @example\n * ```tsx\n * <PauseMenuButton\n * ref={buttonRef}\n * labelKorean=\"계속\"\n * labelEnglish=\"Resume\"\n * icon=\"▶️\"\n * onClick={handleResume}\n * isFocused={focusedIndex === 0}\n * isMobile={false}\n * testId=\"pause-resume-button\"\n * />\n * ```\n */\nexport const PauseMenuButton = forwardRef<\n HTMLButtonElement,\n PauseMenuButtonProps\n>(\n (\n {\n labelKorean,\n labelEnglish,\n icon,\n onClick,\n onMouseEnter,\n onKeyDown,\n onFocus,\n isFocused,\n isMobile,\n testId,\n },\n ref,\n ) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n \n return (\n <button\n ref={ref}\n onClick={onClick}\n onMouseEnter={onMouseEnter}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n data-testid={testId}\n aria-label={`${labelKorean} | ${labelEnglish}`}\n role=\"menuitem\"\n tabIndex={0}\n style={{\n padding: isMobile ? \"12px 24px\" : \"16px 32px\",\n fontSize: isMobile ? \"16px\" : \"20px\",\n backgroundColor: hexToRgbaString(\n theme.colors.UI_BACKGROUND_MEDIUM,\n 0.9,\n ),\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"8px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n textAlign: \"center\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"12px\",\n boxShadow: \"none\",\n ...getFocusStyle(isFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}, 0 0 20px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.5)}`,\n }),\n }}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1,\n );\n e.currentTarget.style.color = hexToRgbaString(\n theme.colors.UI_BACKGROUND_DARK,\n 1,\n );\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 0.5,\n )}`;\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.UI_BACKGROUND_MEDIUM,\n 0.9,\n );\n e.currentTarget.style.color = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1,\n );\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n }}\n >\n {icon && <span style={{ fontSize: \"24px\" }}>{icon}</span>}\n <span>\n {labelKorean} | {labelEnglish}\n </span>\n </button>\n );\n },\n);\n\nPauseMenuButton.displayName = \"PauseMenuButton\";\n\nexport default PauseMenuButton;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,kBAAkB,YAK3B,EACE,aACA,cACA,MACA,SACA,cACA,WACA,SACA,WACA,UACA,UAEF,QACG;CACH,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;EAAU,CAAC;CAE1E,OACE,qBAAC,UAAD;EACO;EACI;EACK;EACH;EACF;EACT,eAAa;EACb,cAAY,GAAG,YAAY,KAAK;EAChC,MAAK;EACL,UAAU;EACV,OAAO;GACL,SAAS,WAAW,cAAc;GAClC,UAAU,WAAW,SAAS;GAC9B,iBAAiB,gBACf,MAAM,OAAO,sBACb,GACD;GACD,OAAO,gBAAgB,MAAM,OAAO,cAAc,EAAE;GACpD,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,GAAI;GACpE,cAAc;GACd,YAAY,MAAM,WAAW;GAC7B,YAAY;GACZ,QAAQ;GACR,YAAY;GACZ,WAAW;GACX,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,KAAK;GACL,WAAW;GACX,GAAG,cAAc,WAAW;IAC1B,cAAc;IACd,WAAW,aAAa,gBAAgB,MAAM,OAAO,cAAc,GAAI,CAAC,aAAa,gBAAgB,MAAM,OAAO,cAAc,GAAI;IACrI,CAAC;GACH;EACD,cAAc,MAAM;GAClB,EAAE,cAAc,MAAM,kBAAkB,gBACtC,MAAM,OAAO,cACb,EACD;GACD,EAAE,cAAc,MAAM,QAAQ,gBAC5B,MAAM,OAAO,oBACb,EACD;GACD,EAAE,cAAc,MAAM,YAAY;GAClC,EAAE,cAAc,MAAM,YAAY,YAAY,gBAC5C,MAAM,OAAO,cACb,GACD;;EAEH,aAAa,MAAM;GACjB,EAAE,cAAc,MAAM,kBAAkB,gBACtC,MAAM,OAAO,sBACb,GACD;GACD,EAAE,cAAc,MAAM,QAAQ,gBAC5B,MAAM,OAAO,cACb,EACD;GACD,EAAE,cAAc,MAAM,YAAY;GAClC,EAAE,cAAc,MAAM,YAAY;;YA5DtC,CA+DG,QAAQ,oBAAC,QAAD;GAAM,OAAO,EAAE,UAAU,QAAQ;aAAG;GAAY,CAAA,EACzD,qBAAC,QAAD,EAAA,UAAA;GACG;GAAY;GAAI;GACZ,EAAA,CAAA,CACA;;EAGd;AAED,gBAAgB,cAAc"}
1
+ {"version":3,"file":"PauseMenuButton.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenuButton.tsx"],"sourcesContent":["/**\n * PauseMenuButton - Reusable button for pause menu items\n * \n * Provides consistent styling for pause menu action buttons with Korean theming.\n * Extracted from PauseMenu to reduce code duplication.\n * \n * Refactored to use useKoreanTheme for consistent theming.\n * \n * @module components/screens/combat/controls\n * @category Combat UI\n * @korean 일시정지메뉴버튼\n */\n\nimport React, { forwardRef } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { getFocusStyle } from \"../../../../../utils/accessibility\";\n\nexport interface PauseMenuButtonProps {\n /** Korean label text */\n readonly labelKorean: string;\n /** English label text */\n readonly labelEnglish: string;\n /** Optional icon/emoji */\n readonly icon?: string;\n /** Click handler */\n readonly onClick: () => void;\n /** Mouse enter handler */\n readonly onMouseEnter?: () => void;\n /** Key down handler */\n readonly onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;\n /** Focus handler */\n readonly onFocus?: () => void;\n /** Whether button is focused */\n readonly isFocused: boolean;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Test ID */\n readonly testId: string;\n}\n\n/**\n * PauseMenuButton Component\n * \n * Styled button for pause menu with:\n * - Korean/English bilingual text with icon support\n * - Cyan themed styling matching combat screen via useKoreanTheme\n * - Hover and focus effects with accessibility support\n * - Responsive sizing for mobile/desktop\n * \n * Reduces code duplication by ~70 lines from PauseMenu (inline button styling)\n * \n * @example\n * ```tsx\n * <PauseMenuButton\n * ref={buttonRef}\n * labelKorean=\"계속\"\n * labelEnglish=\"Resume\"\n * icon=\"▶️\"\n * onClick={handleResume}\n * isFocused={focusedIndex === 0}\n * isMobile={false}\n * testId=\"pause-resume-button\"\n * />\n * ```\n */\nexport const PauseMenuButton = forwardRef<\n HTMLButtonElement,\n PauseMenuButtonProps\n>(\n (\n {\n labelKorean,\n labelEnglish,\n icon,\n onClick,\n onMouseEnter,\n onKeyDown,\n onFocus,\n isFocused,\n isMobile,\n testId,\n },\n ref,\n ) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n \n return (\n <button\n ref={ref}\n onClick={onClick}\n onMouseEnter={onMouseEnter}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n data-testid={testId}\n aria-label={`${labelKorean} | ${labelEnglish}`}\n role=\"menuitem\"\n tabIndex={0}\n style={{\n padding: isMobile ? \"12px 24px\" : \"16px 32px\",\n fontSize: isMobile ? \"16px\" : \"20px\",\n backgroundColor: hexToRgbaString(\n theme.colors.UI_BACKGROUND_MEDIUM,\n 0.9,\n ),\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"8px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n textAlign: \"center\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: \"12px\",\n boxShadow: \"none\",\n ...getFocusStyle(isFocused, {\n outlineWidth: 3,\n boxShadow: `0 0 0 4px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}, 0 0 20px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.5)}`,\n }),\n }}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1,\n );\n e.currentTarget.style.color = hexToRgbaString(\n theme.colors.UI_BACKGROUND_DARK,\n 1,\n );\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 0.5,\n )}`;\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.UI_BACKGROUND_MEDIUM,\n 0.9,\n );\n e.currentTarget.style.color = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1,\n );\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n }}\n >\n {icon && <span style={{ fontSize: \"24px\" }}>{icon}</span>}\n <span>\n {labelKorean} | {labelEnglish}\n </span>\n </button>\n );\n },\n);\n\nPauseMenuButton.displayName = \"PauseMenuButton\";\n\nexport default PauseMenuButton;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,kBAAkB,YAK3B,EACE,aACA,cACA,MACA,SACA,cACA,WACA,SACA,WACA,UACA,UAEF,QACG;CACH,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;CAAS,CAAC;CAEzE,OACE,qBAAC,UAAD;EACO;EACI;EACK;EACH;EACF;EACT,eAAa;EACb,cAAY,GAAG,YAAY,KAAK;EAChC,MAAK;EACL,UAAU;EACV,OAAO;GACL,SAAS,WAAW,cAAc;GAClC,UAAU,WAAW,SAAS;GAC9B,iBAAiB,gBACf,MAAM,OAAO,sBACb,EACF;GACA,OAAO,gBAAgB,MAAM,OAAO,cAAc,CAAC;GACnD,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,EAAG;GACnE,cAAc;GACd,YAAY,MAAM,WAAW;GAC7B,YAAY;GACZ,QAAQ;GACR,YAAY;GACZ,WAAW;GACX,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,KAAK;GACL,WAAW;GACX,GAAG,cAAc,WAAW;IAC1B,cAAc;IACd,WAAW,aAAa,gBAAgB,MAAM,OAAO,cAAc,EAAG,EAAE,aAAa,gBAAgB,MAAM,OAAO,cAAc,EAAG;GACrI,CAAC;EACH;EACA,cAAc,MAAM;GAClB,EAAE,cAAc,MAAM,kBAAkB,gBACtC,MAAM,OAAO,cACb,CACF;GACA,EAAE,cAAc,MAAM,QAAQ,gBAC5B,MAAM,OAAO,oBACb,CACF;GACA,EAAE,cAAc,MAAM,YAAY;GAClC,EAAE,cAAc,MAAM,YAAY,YAAY,gBAC5C,MAAM,OAAO,cACb,EACF;EACF;EACA,aAAa,MAAM;GACjB,EAAE,cAAc,MAAM,kBAAkB,gBACtC,MAAM,OAAO,sBACb,EACF;GACA,EAAE,cAAc,MAAM,QAAQ,gBAC5B,MAAM,OAAO,cACb,CACF;GACA,EAAE,cAAc,MAAM,YAAY;GAClC,EAAE,cAAc,MAAM,YAAY;EACpC;YA7DF,CA+DG,QAAQ,oBAAC,QAAD;GAAM,OAAO,EAAE,UAAU,OAAO;aAAI;EAAW,CAAA,GACxD,qBAAC,QAAD,EAAA,UAAA;GACG;GAAY;GAAI;EACb,EAAA,CAAA,CACA;;AAEZ,CACF;AAEA,gBAAgB,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"QuickSettings.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/QuickSettings.tsx"],"sourcesContent":["/**\n * QuickSettings Component - In-game settings panel\n * \n * Features:\n * - Volume controls (SFX, Music)\n * - Audio toggle\n * - Korean/English bilingual labels\n * - Cyberpunk Korean theming\n * - Uses useKoreanTheme for consistent styling\n * - Enhanced accessibility with ARIA labels\n * \n * @module components/combat/controls\n * @category Combat UI\n * @korean 빠른설정\n */\n\nimport React from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\nexport interface QuickSettingsProps {\n readonly onClose: () => void;\n readonly isMobile: boolean;\n}\n\n/**\n * QuickSettings - In-game settings overlay for audio controls\n * \n * Refactored to use useKoreanTheme for consistent theming:\n * - useKoreanTheme hook for centralized color and font management\n * - Improved accessibility with ARIA labels and dialog semantics\n * - Enhanced form controls with proper ARIA attributes\n * \n * Note: Uses standard HTML button elements rather than BaseButton\n * since this is a 2D HTML overlay component, not a Three.js component.\n * \n * @example\n * ```tsx\n * <QuickSettings\n * onClose={() => setShowSettings(false)}\n * isMobile={false}\n * />\n * ```\n */\nexport const QuickSettings: React.FC<QuickSettingsProps> = ({\n onClose,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"md\", isMobile });\n\n return (\n <div\n data-testid=\"quick-settings\"\n role=\"dialog\"\n aria-labelledby=\"settings-title\"\n aria-modal=\"true\"\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n padding: isMobile ? \"24px\" : \"32px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"12px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n boxShadow: `0 0 30px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n zIndex: 1000,\n }}\n >\n {/* Title */}\n <h2\n id=\"settings-title\"\n data-testid=\"settings-title\"\n style={{\n fontSize: isMobile ? \"20px\" : \"24px\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 24px 0\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.4)}`,\n }}\n >\n 설정 | Settings\n </h2>\n\n {/* Volume Controls */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"16px\" : \"20px\",\n }}\n >\n {/* SFX Volume */}\n <div data-testid=\"sfx-volume-control\">\n <label\n htmlFor=\"sfx-volume-slider\"\n style={{\n display: \"block\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n }}\n >\n 효과음 | SFX Volume\n </label>\n <input\n id=\"sfx-volume-slider\"\n type=\"range\"\n min=\"0\"\n max=\"100\"\n value={audio.sfxVolume * 100}\n onChange={(e) => {\n const value = parseInt(e.target.value) / 100;\n audio.setVolume(\"sfx\", value);\n }}\n aria-label=\"Sound effects volume\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(audio.sfxVolume * 100)}\n data-testid=\"sfx-volume-slider\"\n style={{\n width: \"100%\",\n height: \"6px\",\n borderRadius: \"3px\",\n outline: \"none\",\n cursor: \"pointer\",\n }}\n />\n <div\n data-testid=\"sfx-volume-value\"\n aria-live=\"polite\"\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontFamily: theme.fontFamily.KOREAN,\n marginTop: \"4px\",\n textAlign: \"right\",\n }}\n >\n {Math.round(audio.sfxVolume * 100)}%\n </div>\n </div>\n\n {/* Music Volume */}\n <div data-testid=\"music-volume-control\">\n <label\n htmlFor=\"music-volume-slider\"\n style={{\n display: \"block\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n }}\n >\n 음악 | Music Volume\n </label>\n <input\n id=\"music-volume-slider\"\n type=\"range\"\n min=\"0\"\n max=\"100\"\n value={audio.musicVolume * 100}\n onChange={(e) => {\n const value = parseInt(e.target.value) / 100;\n audio.setVolume(\"music\", value);\n }}\n aria-label=\"Music volume\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(audio.musicVolume * 100)}\n data-testid=\"music-volume-slider\"\n style={{\n width: \"100%\",\n height: \"6px\",\n borderRadius: \"3px\",\n outline: \"none\",\n cursor: \"pointer\",\n }}\n />\n <div\n data-testid=\"music-volume-value\"\n aria-live=\"polite\"\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontFamily: theme.fontFamily.KOREAN,\n marginTop: \"4px\",\n textAlign: \"right\",\n }}\n >\n {Math.round(audio.musicVolume * 100)}%\n </div>\n </div>\n\n {/* Mute Toggle */}\n <div\n data-testid=\"mute-toggle-control\"\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n }}\n >\n <label\n htmlFor=\"mute-toggle-button\"\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n }}\n >\n 음소거 | Mute All\n </label>\n <button\n id=\"mute-toggle-button\"\n onClick={() => {\n if (audio.muted) {\n audio.unmute();\n } else {\n audio.mute();\n }\n audio.playSFX(\"menu_click\");\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n aria-label={audio.muted ? \"Unmute audio\" : \"Mute audio\"}\n aria-pressed={audio.muted}\n data-testid=\"mute-toggle-button\"\n style={{\n padding: isMobile ? \"8px 16px\" : \"10px 20px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: audio.muted\n ? hexToRgbaString(theme.colors.ACCENT_GOLD, 1)\n : hexToRgbaString(theme.colors.UI_BACKGROUND_MEDIUM, 1),\n color: audio.muted\n ? hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1)\n : hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n border: audio.muted\n ? \"none\"\n : `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n >\n {audio.muted ? \"🔇\" : \"🔊\"}\n </button>\n </div>\n </div>\n\n {/* Close Button */}\n <button\n onClick={() => {\n audio.playSFX(\"menu_back\");\n onClose();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n aria-label=\"Close settings dialog\"\n data-testid=\"settings-close-button\"\n style={{\n marginTop: isMobile ? \"24px\" : \"32px\",\n width: \"100%\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n color: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1),\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.ACCENT_GOLD,\n 1\n );\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1\n );\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n >\n 닫기 | Close\n </button>\n </div>\n );\n};\n\nexport default QuickSettings;\n"],"mappings":"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"}
1
+ {"version":3,"file":"QuickSettings.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/QuickSettings.tsx"],"sourcesContent":["/**\n * QuickSettings Component - In-game settings panel\n * \n * Features:\n * - Volume controls (SFX, Music)\n * - Audio toggle\n * - Korean/English bilingual labels\n * - Cyberpunk Korean theming\n * - Uses useKoreanTheme for consistent styling\n * - Enhanced accessibility with ARIA labels\n * \n * @module components/combat/controls\n * @category Combat UI\n * @korean 빠른설정\n */\n\nimport React from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\nexport interface QuickSettingsProps {\n readonly onClose: () => void;\n readonly isMobile: boolean;\n}\n\n/**\n * QuickSettings - In-game settings overlay for audio controls\n * \n * Refactored to use useKoreanTheme for consistent theming:\n * - useKoreanTheme hook for centralized color and font management\n * - Improved accessibility with ARIA labels and dialog semantics\n * - Enhanced form controls with proper ARIA attributes\n * \n * Note: Uses standard HTML button elements rather than BaseButton\n * since this is a 2D HTML overlay component, not a Three.js component.\n * \n * @example\n * ```tsx\n * <QuickSettings\n * onClose={() => setShowSettings(false)}\n * isMobile={false}\n * />\n * ```\n */\nexport const QuickSettings: React.FC<QuickSettingsProps> = ({\n onClose,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"md\", isMobile });\n\n return (\n <div\n data-testid=\"quick-settings\"\n role=\"dialog\"\n aria-labelledby=\"settings-title\"\n aria-modal=\"true\"\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n padding: isMobile ? \"24px\" : \"32px\",\n backgroundColor: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"12px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n boxShadow: `0 0 30px ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.3)}`,\n zIndex: 1000,\n }}\n >\n {/* Title */}\n <h2\n id=\"settings-title\"\n data-testid=\"settings-title\"\n style={{\n fontSize: isMobile ? \"20px\" : \"24px\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 24px 0\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(theme.colors.ACCENT_GOLD, 0.4)}`,\n }}\n >\n 설정 | Settings\n </h2>\n\n {/* Volume Controls */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"16px\" : \"20px\",\n }}\n >\n {/* SFX Volume */}\n <div data-testid=\"sfx-volume-control\">\n <label\n htmlFor=\"sfx-volume-slider\"\n style={{\n display: \"block\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n }}\n >\n 효과음 | SFX Volume\n </label>\n <input\n id=\"sfx-volume-slider\"\n type=\"range\"\n min=\"0\"\n max=\"100\"\n value={audio.sfxVolume * 100}\n onChange={(e) => {\n const value = parseInt(e.target.value) / 100;\n audio.setVolume(\"sfx\", value);\n }}\n aria-label=\"Sound effects volume\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(audio.sfxVolume * 100)}\n data-testid=\"sfx-volume-slider\"\n style={{\n width: \"100%\",\n height: \"6px\",\n borderRadius: \"3px\",\n outline: \"none\",\n cursor: \"pointer\",\n }}\n />\n <div\n data-testid=\"sfx-volume-value\"\n aria-live=\"polite\"\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontFamily: theme.fontFamily.KOREAN,\n marginTop: \"4px\",\n textAlign: \"right\",\n }}\n >\n {Math.round(audio.sfxVolume * 100)}%\n </div>\n </div>\n\n {/* Music Volume */}\n <div data-testid=\"music-volume-control\">\n <label\n htmlFor=\"music-volume-slider\"\n style={{\n display: \"block\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n }}\n >\n 음악 | Music Volume\n </label>\n <input\n id=\"music-volume-slider\"\n type=\"range\"\n min=\"0\"\n max=\"100\"\n value={audio.musicVolume * 100}\n onChange={(e) => {\n const value = parseInt(e.target.value) / 100;\n audio.setVolume(\"music\", value);\n }}\n aria-label=\"Music volume\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(audio.musicVolume * 100)}\n data-testid=\"music-volume-slider\"\n style={{\n width: \"100%\",\n height: \"6px\",\n borderRadius: \"3px\",\n outline: \"none\",\n cursor: \"pointer\",\n }}\n />\n <div\n data-testid=\"music-volume-value\"\n aria-live=\"polite\"\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(theme.colors.TEXT_SECONDARY, 1),\n fontFamily: theme.fontFamily.KOREAN,\n marginTop: \"4px\",\n textAlign: \"right\",\n }}\n >\n {Math.round(audio.musicVolume * 100)}%\n </div>\n </div>\n\n {/* Mute Toggle */}\n <div\n data-testid=\"mute-toggle-control\"\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n }}\n >\n <label\n htmlFor=\"mute-toggle-button\"\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n }}\n >\n 음소거 | Mute All\n </label>\n <button\n id=\"mute-toggle-button\"\n onClick={() => {\n if (audio.muted) {\n audio.unmute();\n } else {\n audio.mute();\n }\n audio.playSFX(\"menu_click\");\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n aria-label={audio.muted ? \"Unmute audio\" : \"Mute audio\"}\n aria-pressed={audio.muted}\n data-testid=\"mute-toggle-button\"\n style={{\n padding: isMobile ? \"8px 16px\" : \"10px 20px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: audio.muted\n ? hexToRgbaString(theme.colors.ACCENT_GOLD, 1)\n : hexToRgbaString(theme.colors.UI_BACKGROUND_MEDIUM, 1),\n color: audio.muted\n ? hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1)\n : hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n border: audio.muted\n ? \"none\"\n : `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n >\n {audio.muted ? \"🔇\" : \"🔊\"}\n </button>\n </div>\n </div>\n\n {/* Close Button */}\n <button\n onClick={() => {\n audio.playSFX(\"menu_back\");\n onClose();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n aria-label=\"Close settings dialog\"\n data-testid=\"settings-close-button\"\n style={{\n marginTop: isMobile ? \"24px\" : \"32px\",\n width: \"100%\",\n padding: isMobile ? \"10px\" : \"12px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n color: hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 1),\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.ACCENT_GOLD,\n 1\n );\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = hexToRgbaString(\n theme.colors.PRIMARY_CYAN,\n 1\n );\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n >\n 닫기 | Close\n </button>\n </div>\n );\n};\n\nexport default QuickSettings;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BloodDecals3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodDecals3D.tsx"],"sourcesContent":["/**\n * BloodDecals3D - Blood accumulation decal system for character models\n *\n * Projects blood decals onto 3D character surfaces using decal geometry for\n * persistent blood visualization. Decals fade over time and track injury\n * persistence across combat rounds.\n *\n * Features:\n * - Decal projection onto character meshes\n * - Blood accumulation from hits and lacerations\n * - Blood trail visualization\n * - Cross-round injury persistence\n * - Korean-themed blood visualization\n *\n * @module components/combat/BloodDecals3D\n * @category Combat Effects\n * @korean 피흔적3D\n */\n\nimport { DecalGeometry } from \"three/examples/jsm/geometries/DecalGeometry.js\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo, useEffect } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\n/**\n * Blood decal configuration\n */\nexport interface BloodDecal {\n /** Unique identifier */\n readonly id: string;\n /** Position in world space */\n readonly position: [number, number, number];\n /** Normal vector for surface projection */\n readonly normal: [number, number, number];\n /** Size of decal */\n readonly size: [number, number, number];\n /** Rotation around normal (radians) */\n readonly rotation: number;\n /** Opacity (0.0 to 1.0) */\n readonly opacity: number;\n /** Creation timestamp */\n readonly timestamp: number;\n /** Whether decal is from a laceration (adds blood trail) */\n readonly isLaceration?: boolean;\n}\n\n/**\n * Props for BloodDecals3D component\n */\nexport interface BloodDecals3DProps {\n /** Active blood decals to render */\n readonly decals: readonly BloodDecal[];\n /** Character mesh reference for decal projection */\n readonly targetMeshRef?: React.RefObject<THREE.Mesh>;\n /** Whether decals are enabled (violence settings) */\n readonly enabled?: boolean;\n /** Mobile mode (simplified decals) */\n readonly isMobile?: boolean;\n /** Decal fade duration in seconds */\n readonly fadeDuration?: number;\n /** Callback when decal fully fades */\n readonly onDecalComplete?: (decalId: string) => void;\n}\n\n/**\n * Blood decal constants\n */\nconst DECAL_CONSTANTS = {\n /** Default fade duration (seconds) */\n FADE_DURATION: 15.0,\n /** Base decal size */\n BASE_SIZE: [0.15, 0.15, 0.05] as [number, number, number],\n /** Laceration trail size multiplier */\n TRAIL_MULTIPLIER: 3.0,\n /** Maximum concurrent decals for performance */\n MAX_DECALS: 20,\n /** Mobile decal limit */\n MAX_DECALS_MOBILE: 10,\n} as const;\n\n/**\n * Generate blood decal texture (procedural)\n */\n/**\n * Generate a procedural blood texture with irregular edges.\n * Note: Uses Math.random() for visual variety. Each texture will be slightly different,\n * providing natural variation in blood splatter appearance across different decals.\n * This is intentional for realism, though it does create unique textures per component.\n * For better memory efficiency in production, consider pre-generating 3-5 variations\n * and randomly selecting one.\n */\nconst createBloodTexture = (): THREE.Texture => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 256;\n canvas.height = 256;\n const ctx = canvas.getContext(\"2d\");\n \n if (!ctx) {\n console.warn(\"Blood decal texture generation failed: Could not get 2D context\");\n const fallbackCanvas = document.createElement(\"canvas\");\n fallbackCanvas.width = 256;\n fallbackCanvas.height = 256;\n return new THREE.CanvasTexture(fallbackCanvas);\n }\n\n ctx.clearRect(0, 0, 256, 256);\n\n const centerX = 128;\n const centerY = 128;\n\n const gradient = ctx.createRadialGradient(\n centerX,\n centerY,\n 0,\n centerX,\n centerY,\n 100\n );\n\n const bloodColor = new THREE.Color(KOREAN_COLORS.BLOODLOSS_INDICATOR);\n const r = Math.floor(bloodColor.r * 255);\n const g = Math.floor(bloodColor.g * 255);\n const b = Math.floor(bloodColor.b * 255);\n\n gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1.0)`);\n gradient.addColorStop(0.6, `rgba(${r * 0.8}, ${g * 0.6}, ${b * 0.6}, 0.8)`);\n gradient.addColorStop(1, `rgba(${r * 0.5}, ${g * 0.3}, ${b * 0.3}, 0)`);\n\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, 256, 256);\n\n for (let i = 0; i < 30; i++) {\n const angle = (i / 30) * Math.PI * 2;\n const radius = 80 + Math.random() * 20;\n const x = centerX + Math.cos(angle) * radius;\n const y = centerY + Math.sin(angle) * radius;\n const size = 5 + Math.random() * 10;\n\n ctx.fillStyle = `rgba(${r * 0.7}, ${g * 0.4}, ${b * 0.4}, ${0.5 + Math.random() * 0.3})`;\n ctx.beginPath();\n ctx.arc(x, y, size, 0, Math.PI * 2);\n ctx.fill();\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.needsUpdate = true;\n return texture;\n};\n\n/**\n * Individual decal component\n */\nconst DecalMesh: React.FC<{\n decal: BloodDecal;\n texture: THREE.Texture;\n targetMesh?: THREE.Mesh;\n fadeDuration: number;\n}> = ({ decal, texture, targetMesh, fadeDuration }) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const materialRef = useRef<THREE.MeshBasicMaterial | null>(null);\n const ageRef = useRef(0);\n\n useEffect(() => {\n if (!meshRef.current || !targetMesh) return;\n\n try {\n const position = new THREE.Vector3(...decal.position);\n const size = new THREE.Vector3(...decal.size);\n\n const orientation = new THREE.Euler();\n orientation.copy(targetMesh.rotation);\n \n const decalGeometry = new DecalGeometry(\n targetMesh,\n position,\n orientation,\n size\n );\n\n meshRef.current.geometry = decalGeometry;\n \n meshRef.current.rotation.set(0, 0, decal.rotation);\n } catch (error) {\n if (process.env.NODE_ENV === \"development\") {\n console.warn(\"BloodDecals3D: Failed to project blood decal onto target mesh.\", {\n decalId: decal.id,\n position: decal.position,\n size: decal.size,\n error,\n });\n }\n }\n }, [decal, targetMesh]);\n\n useFrame((_, delta) => {\n if (!materialRef.current) return;\n\n ageRef.current += delta;\n const fadeProgress = Math.min(ageRef.current / fadeDuration, 1);\n materialRef.current.opacity = decal.opacity * (1 - fadeProgress);\n });\n\n return (\n <mesh ref={meshRef} data-testid={`blood-decal-${decal.id}`}>\n <meshBasicMaterial\n ref={materialRef}\n map={texture}\n transparent\n opacity={decal.opacity}\n depthTest={true}\n depthWrite={false}\n polygonOffset\n polygonOffsetFactor={-4}\n />\n </mesh>\n );\n};\n\n/**\n * BloodDecals3D Component\n *\n * Renders persistent blood decals on 3D character models using decal projection.\n * Decals fade over time and can persist across combat rounds for injury tracking.\n *\n * Performance optimized:\n * - Limited concurrent decals (20 desktop, 10 mobile)\n * - Efficient decal geometry generation\n * - Texture reuse across all decals\n * - Automatic cleanup of faded decals\n *\n * @example\n * ```tsx\n * const [bloodDecals, setBloodDecals] = useState<BloodDecal[]>([]);\n * const characterMeshRef = useRef<THREE.Mesh>(null);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], normal: [number, number, number]) => {\n * setBloodDecals([...bloodDecals, {\n * id: generateId(),\n * position,\n * normal,\n * size: [0.15, 0.15, 0.05],\n * rotation: Math.random() * Math.PI * 2,\n * opacity: 0.8,\n * timestamp: Date.now(),\n * isLaceration: false,\n * }]);\n * };\n *\n * <mesh ref={characterMeshRef}>\n * <capsuleGeometry args={[0.5, 1.6, 16, 32]} />\n * <meshStandardMaterial color={0xcccccc} />\n * </mesh>\n *\n * <BloodDecals3D\n * decals={bloodDecals}\n * targetMeshRef={characterMeshRef}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onDecalComplete={(id) => {\n * setBloodDecals(prev => prev.filter(d => d.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodDecals3D: React.FC<BloodDecals3DProps> = ({\n decals,\n targetMeshRef,\n enabled = true,\n isMobile = false,\n fadeDuration = DECAL_CONSTANTS.FADE_DURATION,\n onDecalComplete,\n}) => {\n const completedDecalsRef = useRef<Set<string>>(new Set());\n\n const maxDecals = isMobile\n ? DECAL_CONSTANTS.MAX_DECALS_MOBILE\n : DECAL_CONSTANTS.MAX_DECALS;\n\n const bloodTexture = useMemo(() => createBloodTexture(), []);\n\n const activeDecals = useMemo(() => {\n const sorted = [...decals].sort((a, b) => b.timestamp - a.timestamp);\n return sorted.slice(0, maxDecals);\n }, [decals, maxDecals]);\n\n const decalAgesRef = useRef<Map<string, number>>(new Map());\n\n useEffect(() => {\n activeDecals.forEach((decal) => {\n if (!decalAgesRef.current.has(decal.id)) {\n decalAgesRef.current.set(decal.id, 0);\n }\n });\n }, [activeDecals]);\n\n useFrame((_, delta) => {\n activeDecals.forEach((decal) => {\n const currentAge = (decalAgesRef.current.get(decal.id) ?? 0) + delta;\n decalAgesRef.current.set(decal.id, currentAge);\n \n const isExpired = currentAge >= fadeDuration;\n\n if (\n isExpired &&\n onDecalComplete &&\n !completedDecalsRef.current.has(decal.id)\n ) {\n completedDecalsRef.current.add(decal.id);\n onDecalComplete(decal.id);\n }\n });\n });\n\n useEffect(() => {\n return () => {\n bloodTexture.dispose();\n };\n }, [bloodTexture]);\n\n const [targetMesh, setTargetMesh] = React.useState<THREE.Mesh | undefined>();\n\n React.useLayoutEffect(() => {\n setTargetMesh(targetMeshRef?.current ?? undefined);\n }, [targetMeshRef]);\n\n if (!enabled || activeDecals.length === 0) {\n return null;\n }\n\n return (\n <group data-testid=\"blood-decals-3d\">\n {activeDecals.map((decal) => (\n <DecalMesh\n key={decal.id}\n decal={decal}\n texture={bloodTexture}\n targetMesh={targetMesh}\n fadeDuration={fadeDuration}\n />\n ))}\n </group>\n );\n};\n\nexport default BloodDecals3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"BloodDecals3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodDecals3D.tsx"],"sourcesContent":["/**\n * BloodDecals3D - Blood accumulation decal system for character models\n *\n * Projects blood decals onto 3D character surfaces using decal geometry for\n * persistent blood visualization. Decals fade over time and track injury\n * persistence across combat rounds.\n *\n * Features:\n * - Decal projection onto character meshes\n * - Blood accumulation from hits and lacerations\n * - Blood trail visualization\n * - Cross-round injury persistence\n * - Korean-themed blood visualization\n *\n * @module components/combat/BloodDecals3D\n * @category Combat Effects\n * @korean 피흔적3D\n */\n\nimport { DecalGeometry } from \"three/examples/jsm/geometries/DecalGeometry.js\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo, useEffect } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\n/**\n * Blood decal configuration\n */\nexport interface BloodDecal {\n /** Unique identifier */\n readonly id: string;\n /** Position in world space */\n readonly position: [number, number, number];\n /** Normal vector for surface projection */\n readonly normal: [number, number, number];\n /** Size of decal */\n readonly size: [number, number, number];\n /** Rotation around normal (radians) */\n readonly rotation: number;\n /** Opacity (0.0 to 1.0) */\n readonly opacity: number;\n /** Creation timestamp */\n readonly timestamp: number;\n /** Whether decal is from a laceration (adds blood trail) */\n readonly isLaceration?: boolean;\n}\n\n/**\n * Props for BloodDecals3D component\n */\nexport interface BloodDecals3DProps {\n /** Active blood decals to render */\n readonly decals: readonly BloodDecal[];\n /** Character mesh reference for decal projection */\n readonly targetMeshRef?: React.RefObject<THREE.Mesh>;\n /** Whether decals are enabled (violence settings) */\n readonly enabled?: boolean;\n /** Mobile mode (simplified decals) */\n readonly isMobile?: boolean;\n /** Decal fade duration in seconds */\n readonly fadeDuration?: number;\n /** Callback when decal fully fades */\n readonly onDecalComplete?: (decalId: string) => void;\n}\n\n/**\n * Blood decal constants\n */\nconst DECAL_CONSTANTS = {\n /** Default fade duration (seconds) */\n FADE_DURATION: 15.0,\n /** Base decal size */\n BASE_SIZE: [0.15, 0.15, 0.05] as [number, number, number],\n /** Laceration trail size multiplier */\n TRAIL_MULTIPLIER: 3.0,\n /** Maximum concurrent decals for performance */\n MAX_DECALS: 20,\n /** Mobile decal limit */\n MAX_DECALS_MOBILE: 10,\n} as const;\n\n/**\n * Generate blood decal texture (procedural)\n */\n/**\n * Generate a procedural blood texture with irregular edges.\n * Note: Uses Math.random() for visual variety. Each texture will be slightly different,\n * providing natural variation in blood splatter appearance across different decals.\n * This is intentional for realism, though it does create unique textures per component.\n * For better memory efficiency in production, consider pre-generating 3-5 variations\n * and randomly selecting one.\n */\nconst createBloodTexture = (): THREE.Texture => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 256;\n canvas.height = 256;\n const ctx = canvas.getContext(\"2d\");\n \n if (!ctx) {\n console.warn(\"Blood decal texture generation failed: Could not get 2D context\");\n const fallbackCanvas = document.createElement(\"canvas\");\n fallbackCanvas.width = 256;\n fallbackCanvas.height = 256;\n return new THREE.CanvasTexture(fallbackCanvas);\n }\n\n ctx.clearRect(0, 0, 256, 256);\n\n const centerX = 128;\n const centerY = 128;\n\n const gradient = ctx.createRadialGradient(\n centerX,\n centerY,\n 0,\n centerX,\n centerY,\n 100\n );\n\n const bloodColor = new THREE.Color(KOREAN_COLORS.BLOODLOSS_INDICATOR);\n const r = Math.floor(bloodColor.r * 255);\n const g = Math.floor(bloodColor.g * 255);\n const b = Math.floor(bloodColor.b * 255);\n\n gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1.0)`);\n gradient.addColorStop(0.6, `rgba(${r * 0.8}, ${g * 0.6}, ${b * 0.6}, 0.8)`);\n gradient.addColorStop(1, `rgba(${r * 0.5}, ${g * 0.3}, ${b * 0.3}, 0)`);\n\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, 256, 256);\n\n for (let i = 0; i < 30; i++) {\n const angle = (i / 30) * Math.PI * 2;\n const radius = 80 + Math.random() * 20;\n const x = centerX + Math.cos(angle) * radius;\n const y = centerY + Math.sin(angle) * radius;\n const size = 5 + Math.random() * 10;\n\n ctx.fillStyle = `rgba(${r * 0.7}, ${g * 0.4}, ${b * 0.4}, ${0.5 + Math.random() * 0.3})`;\n ctx.beginPath();\n ctx.arc(x, y, size, 0, Math.PI * 2);\n ctx.fill();\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.needsUpdate = true;\n return texture;\n};\n\n/**\n * Individual decal component\n */\nconst DecalMesh: React.FC<{\n decal: BloodDecal;\n texture: THREE.Texture;\n targetMesh?: THREE.Mesh;\n fadeDuration: number;\n}> = ({ decal, texture, targetMesh, fadeDuration }) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const materialRef = useRef<THREE.MeshBasicMaterial | null>(null);\n const ageRef = useRef(0);\n\n useEffect(() => {\n if (!meshRef.current || !targetMesh) return;\n\n try {\n const position = new THREE.Vector3(...decal.position);\n const size = new THREE.Vector3(...decal.size);\n\n const orientation = new THREE.Euler();\n orientation.copy(targetMesh.rotation);\n \n const decalGeometry = new DecalGeometry(\n targetMesh,\n position,\n orientation,\n size\n );\n\n meshRef.current.geometry = decalGeometry;\n \n meshRef.current.rotation.set(0, 0, decal.rotation);\n } catch (error) {\n if (process.env.NODE_ENV === \"development\") {\n console.warn(\"BloodDecals3D: Failed to project blood decal onto target mesh.\", {\n decalId: decal.id,\n position: decal.position,\n size: decal.size,\n error,\n });\n }\n }\n }, [decal, targetMesh]);\n\n useFrame((_, delta) => {\n if (!materialRef.current) return;\n\n ageRef.current += delta;\n const fadeProgress = Math.min(ageRef.current / fadeDuration, 1);\n materialRef.current.opacity = decal.opacity * (1 - fadeProgress);\n });\n\n return (\n <mesh ref={meshRef} data-testid={`blood-decal-${decal.id}`}>\n <meshBasicMaterial\n ref={materialRef}\n map={texture}\n transparent\n opacity={decal.opacity}\n depthTest={true}\n depthWrite={false}\n polygonOffset\n polygonOffsetFactor={-4}\n />\n </mesh>\n );\n};\n\n/**\n * BloodDecals3D Component\n *\n * Renders persistent blood decals on 3D character models using decal projection.\n * Decals fade over time and can persist across combat rounds for injury tracking.\n *\n * Performance optimized:\n * - Limited concurrent decals (20 desktop, 10 mobile)\n * - Efficient decal geometry generation\n * - Texture reuse across all decals\n * - Automatic cleanup of faded decals\n *\n * @example\n * ```tsx\n * const [bloodDecals, setBloodDecals] = useState<BloodDecal[]>([]);\n * const characterMeshRef = useRef<THREE.Mesh>(null);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], normal: [number, number, number]) => {\n * setBloodDecals([...bloodDecals, {\n * id: generateId(),\n * position,\n * normal,\n * size: [0.15, 0.15, 0.05],\n * rotation: Math.random() * Math.PI * 2,\n * opacity: 0.8,\n * timestamp: Date.now(),\n * isLaceration: false,\n * }]);\n * };\n *\n * <mesh ref={characterMeshRef}>\n * <capsuleGeometry args={[0.5, 1.6, 16, 32]} />\n * <meshStandardMaterial color={0xcccccc} />\n * </mesh>\n *\n * <BloodDecals3D\n * decals={bloodDecals}\n * targetMeshRef={characterMeshRef}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onDecalComplete={(id) => {\n * setBloodDecals(prev => prev.filter(d => d.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodDecals3D: React.FC<BloodDecals3DProps> = ({\n decals,\n targetMeshRef,\n enabled = true,\n isMobile = false,\n fadeDuration = DECAL_CONSTANTS.FADE_DURATION,\n onDecalComplete,\n}) => {\n const completedDecalsRef = useRef<Set<string>>(new Set());\n\n const maxDecals = isMobile\n ? DECAL_CONSTANTS.MAX_DECALS_MOBILE\n : DECAL_CONSTANTS.MAX_DECALS;\n\n const bloodTexture = useMemo(() => createBloodTexture(), []);\n\n const activeDecals = useMemo(() => {\n const sorted = [...decals].sort((a, b) => b.timestamp - a.timestamp);\n return sorted.slice(0, maxDecals);\n }, [decals, maxDecals]);\n\n const decalAgesRef = useRef<Map<string, number>>(new Map());\n\n useEffect(() => {\n activeDecals.forEach((decal) => {\n if (!decalAgesRef.current.has(decal.id)) {\n decalAgesRef.current.set(decal.id, 0);\n }\n });\n }, [activeDecals]);\n\n useFrame((_, delta) => {\n activeDecals.forEach((decal) => {\n const currentAge = (decalAgesRef.current.get(decal.id) ?? 0) + delta;\n decalAgesRef.current.set(decal.id, currentAge);\n \n const isExpired = currentAge >= fadeDuration;\n\n if (\n isExpired &&\n onDecalComplete &&\n !completedDecalsRef.current.has(decal.id)\n ) {\n completedDecalsRef.current.add(decal.id);\n onDecalComplete(decal.id);\n }\n });\n });\n\n useEffect(() => {\n return () => {\n bloodTexture.dispose();\n };\n }, [bloodTexture]);\n\n const [targetMesh, setTargetMesh] = React.useState<THREE.Mesh | undefined>();\n\n React.useLayoutEffect(() => {\n setTargetMesh(targetMeshRef?.current ?? undefined);\n }, [targetMeshRef]);\n\n if (!enabled || activeDecals.length === 0) {\n return null;\n }\n\n return (\n <group data-testid=\"blood-decals-3d\">\n {activeDecals.map((decal) => (\n <DecalMesh\n key={decal.id}\n decal={decal}\n texture={bloodTexture}\n targetMesh={targetMesh}\n fadeDuration={fadeDuration}\n />\n ))}\n </group>\n );\n};\n\nexport default BloodDecals3D;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BloodLossOverlayHtml.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodLossOverlayHtml.tsx"],"sourcesContent":["/**\n * BloodLossOverlayHtml Component - Visual warning for blood loss\n *\n * Displays a pulsing red overlay when blood loss exceeds critical threshold (50%).\n * Uses CSS animation for smooth pulsing effect.\n *\n * NOTE: This component is rendered OUTSIDE the Canvas as part of the HTML overlay.\n * It does NOT use Html from drei - it's a standard React component.\n *\n * @module components/combat/BloodLossOverlayHtml\n * @category Combat UI\n * @korean 출혈오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\nexport interface BloodLossOverlayProps {\n /**\n * Current blood loss amount (0-100)\n * @korean 출혈량\n */\n readonly bloodLoss: number;\n\n /**\n * Mobile responsive mode (reduced pulse intensity)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Multiplier applied to the effect's maximum opacity (0.0-1.0).\n *\n * Use this to soften the fullscreen effect when the 3D arena is already\n * visually compressed (e.g. portrait mobile). Default is `1.0`.\n *\n * @korean 효과강도배수\n */\n readonly intensityScale?: number;\n}\n\n/**\n * BloodLossOverlayHtml - Pulsing red warning for critical blood loss\n *\n * Renders a fullscreen pulsing red overlay when blood loss is 50 or higher.\n * Only visible when bloodLoss is 50 or above; does not render for values below 50.\n * Uses CSS keyframe animation for smooth pulsing effect at 60fps.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when blood loss hasn't changed significantly\n * - Memoized style calculations\n * \n * Accessibility notes:\n * - Purely decorative visual effect; no ARIA role or aria-live region is defined here\n * - Typically rendered within an aria-hidden container so it is ignored by screen readers\n * - Critical status announcements should be provided by separate, semantic HUD components\n *\n * @example\n * ```tsx\n * // Overlay is rendered (bloodLoss >= 50)\n * <BloodLossOverlayHtml bloodLoss={75} isMobile={false} />\n *\n * // Overlay is not rendered (bloodLoss < 50)\n * <BloodLossOverlayHtml bloodLoss={30} isMobile={false} />\n * ```\n */\nexport const BloodLossOverlayHtml = React.memo<BloodLossOverlayProps>(\n ({ bloodLoss, isMobile, intensityScale = 1 }) => {\n const overlayStyle = useMemo(() => {\n const criticalThreshold = 50;\n if (bloodLoss < criticalThreshold) {\n return null;\n }\n\n const clampedBloodLoss = Math.max(\n criticalThreshold,\n Math.min(100, bloodLoss)\n );\n\n const intensity =\n (clampedBloodLoss - criticalThreshold) / (100 - criticalThreshold);\n\n const safeScale = Math.max(0, Math.min(1, intensityScale));\n const maxOpacity = (isMobile ? 0.15 : 0.25) * safeScale;\n const baseOpacity = intensity * maxOpacity;\n\n const rgb = KOREAN_COLORS.BLOODLOSS_INDICATOR;\n const bloodColor = `rgb(${(rgb >> 16) & 255}, ${(rgb >> 8) & 255}, ${\n rgb & 255\n })`;\n\n return {\n position: \"fixed\" as const,\n inset: 0,\n pointerEvents: \"none\" as const,\n backgroundColor: bloodColor,\n [\"--base-opacity\"]: baseOpacity.toString(),\n animation: \"bloodLossPulse 1.5s ease-in-out infinite\",\n transition: \"opacity 0.5s ease-out\",\n zIndex: 55, // Between pain vignette and consciousness blur\n } as React.CSSProperties;\n }, [bloodLoss, isMobile, intensityScale]);\n\n if (bloodLoss < 50 || !overlayStyle) {\n return null;\n }\n\n return (\n <>\n {/* CSS keyframe animation for pulsing with CSS variables */}\n <style>\n {`\n @keyframes bloodLossPulse {\n 0%, 100% {\n opacity: var(--base-opacity);\n }\n 50% {\n opacity: calc(var(--base-opacity) * 1.5);\n }\n }\n `}\n </style>\n <div\n data-testid=\"bloodloss-overlay\"\n style={overlayStyle}\n aria-hidden=\"true\"\n />\n </>\n );\n },\n (prevProps, nextProps) => {\n const wasCritical = prevProps.bloodLoss >= 50;\n const isCritical = nextProps.bloodLoss >= 50;\n \n if (!wasCritical && !isCritical) return true;\n \n if (wasCritical !== isCritical) return false;\n \n return (\n Math.abs(prevProps.bloodLoss - nextProps.bloodLoss) < 5 &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.intensityScale === nextProps.intensityScale\n );\n },\n);\n\nBloodLossOverlayHtml.displayName = \"BloodLossOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,uBAAuB,MAAM,MACvC,EAAE,WAAW,UAAU,iBAAiB,QAAQ;CACjD,MAAM,eAAe,cAAc;EACjC,MAAM,oBAAoB;EAC1B,IAAI,YAAY,mBACd,OAAO;EAaT,MAAM,eAVmB,KAAK,IAC5B,mBACA,KAAK,IAAI,KAAK,UAAU,CAIvB,GAAmB,sBAAsB,MAAM,uBAG9B,WAAW,MAAO,OADpB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,eAAe,CACX;EAG9C,MAAM,MAAM,cAAc;EAK1B,OAAO;GACL,UAAU;GACV,OAAO;GACP,eAAe;GACf,iBAAiB,OARQ,OAAO,KAAM,IAAI,IAAK,OAAO,IAAK,IAAI,IAC/D,MAAM,IACP;IAOE,mBAAmB,YAAY,UAAU;GAC1C,WAAW;GACX,YAAY;GACZ,QAAQ;GACT;IACA;EAAC;EAAW;EAAU;EAAe,CAAC;CAEzC,IAAI,YAAY,MAAM,CAAC,cACrB,OAAO;CAGT,OACE,qBAAA,UAAA,EAAA,UAAA,CAEE,oBAAC,SAAD,EAAA,UACG;;;;;;;;;WAUK,CAAA,EACR,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;EACP,eAAY;EACZ,CAAA,CACD,EAAA,CAAA;IAGJ,WAAW,cAAc;CACxB,MAAM,cAAc,UAAU,aAAa;CAC3C,MAAM,aAAa,UAAU,aAAa;CAE1C,IAAI,CAAC,eAAe,CAAC,YAAY,OAAO;CAExC,IAAI,gBAAgB,YAAY,OAAO;CAEvC,OACE,KAAK,IAAI,UAAU,YAAY,UAAU,UAAU,GAAG,KACtD,UAAU,aAAa,UAAU,YACjC,UAAU,mBAAmB,UAAU;EAG5C;AAED,qBAAqB,cAAc"}
1
+ {"version":3,"file":"BloodLossOverlayHtml.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodLossOverlayHtml.tsx"],"sourcesContent":["/**\n * BloodLossOverlayHtml Component - Visual warning for blood loss\n *\n * Displays a pulsing red overlay when blood loss exceeds critical threshold (50%).\n * Uses CSS animation for smooth pulsing effect.\n *\n * NOTE: This component is rendered OUTSIDE the Canvas as part of the HTML overlay.\n * It does NOT use Html from drei - it's a standard React component.\n *\n * @module components/combat/BloodLossOverlayHtml\n * @category Combat UI\n * @korean 출혈오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\nexport interface BloodLossOverlayProps {\n /**\n * Current blood loss amount (0-100)\n * @korean 출혈량\n */\n readonly bloodLoss: number;\n\n /**\n * Mobile responsive mode (reduced pulse intensity)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Multiplier applied to the effect's maximum opacity (0.0-1.0).\n *\n * Use this to soften the fullscreen effect when the 3D arena is already\n * visually compressed (e.g. portrait mobile). Default is `1.0`.\n *\n * @korean 효과강도배수\n */\n readonly intensityScale?: number;\n}\n\n/**\n * BloodLossOverlayHtml - Pulsing red warning for critical blood loss\n *\n * Renders a fullscreen pulsing red overlay when blood loss is 50 or higher.\n * Only visible when bloodLoss is 50 or above; does not render for values below 50.\n * Uses CSS keyframe animation for smooth pulsing effect at 60fps.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when blood loss hasn't changed significantly\n * - Memoized style calculations\n * \n * Accessibility notes:\n * - Purely decorative visual effect; no ARIA role or aria-live region is defined here\n * - Typically rendered within an aria-hidden container so it is ignored by screen readers\n * - Critical status announcements should be provided by separate, semantic HUD components\n *\n * @example\n * ```tsx\n * // Overlay is rendered (bloodLoss >= 50)\n * <BloodLossOverlayHtml bloodLoss={75} isMobile={false} />\n *\n * // Overlay is not rendered (bloodLoss < 50)\n * <BloodLossOverlayHtml bloodLoss={30} isMobile={false} />\n * ```\n */\nexport const BloodLossOverlayHtml = React.memo<BloodLossOverlayProps>(\n ({ bloodLoss, isMobile, intensityScale = 1 }) => {\n const overlayStyle = useMemo(() => {\n const criticalThreshold = 50;\n if (bloodLoss < criticalThreshold) {\n return null;\n }\n\n const clampedBloodLoss = Math.max(\n criticalThreshold,\n Math.min(100, bloodLoss)\n );\n\n const intensity =\n (clampedBloodLoss - criticalThreshold) / (100 - criticalThreshold);\n\n const safeScale = Math.max(0, Math.min(1, intensityScale));\n const maxOpacity = (isMobile ? 0.15 : 0.25) * safeScale;\n const baseOpacity = intensity * maxOpacity;\n\n const rgb = KOREAN_COLORS.BLOODLOSS_INDICATOR;\n const bloodColor = `rgb(${(rgb >> 16) & 255}, ${(rgb >> 8) & 255}, ${\n rgb & 255\n })`;\n\n return {\n position: \"fixed\" as const,\n inset: 0,\n pointerEvents: \"none\" as const,\n backgroundColor: bloodColor,\n [\"--base-opacity\"]: baseOpacity.toString(),\n animation: \"bloodLossPulse 1.5s ease-in-out infinite\",\n transition: \"opacity 0.5s ease-out\",\n zIndex: 55, // Between pain vignette and consciousness blur\n } as React.CSSProperties;\n }, [bloodLoss, isMobile, intensityScale]);\n\n if (bloodLoss < 50 || !overlayStyle) {\n return null;\n }\n\n return (\n <>\n {/* CSS keyframe animation for pulsing with CSS variables */}\n <style>\n {`\n @keyframes bloodLossPulse {\n 0%, 100% {\n opacity: var(--base-opacity);\n }\n 50% {\n opacity: calc(var(--base-opacity) * 1.5);\n }\n }\n `}\n </style>\n <div\n data-testid=\"bloodloss-overlay\"\n style={overlayStyle}\n aria-hidden=\"true\"\n />\n </>\n );\n },\n (prevProps, nextProps) => {\n const wasCritical = prevProps.bloodLoss >= 50;\n const isCritical = nextProps.bloodLoss >= 50;\n \n if (!wasCritical && !isCritical) return true;\n \n if (wasCritical !== isCritical) return false;\n \n return (\n Math.abs(prevProps.bloodLoss - nextProps.bloodLoss) < 5 &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.intensityScale === nextProps.intensityScale\n );\n },\n);\n\nBloodLossOverlayHtml.displayName = \"BloodLossOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,uBAAuB,MAAM,MACvC,EAAE,WAAW,UAAU,iBAAiB,QAAQ;CACjD,MAAM,eAAe,cAAc;EACjC,MAAM,oBAAoB;EAC1B,IAAI,YAAY,mBACd,OAAO;EAaT,MAAM,eAVmB,KAAK,IAC5B,mBACA,KAAK,IAAI,KAAK,SAAS,CAItB,IAAmB,sBAAsB,MAAM,uBAG9B,WAAW,MAAO,OADpB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,cAAc,CACV;EAG9C,MAAM,MAAM,cAAc;EAK1B,OAAO;GACL,UAAU;GACV,OAAO;GACP,eAAe;GACf,iBAAiB,OARQ,OAAO,KAAM,IAAI,IAAK,OAAO,IAAK,IAAI,IAC/D,MAAM,IACP;IAOE,mBAAmB,YAAY,SAAS;GACzC,WAAW;GACX,YAAY;GACZ,QAAQ;EACV;CACF,GAAG;EAAC;EAAW;EAAU;CAAc,CAAC;CAExC,IAAI,YAAY,MAAM,CAAC,cACrB,OAAO;CAGT,OACE,qBAAA,UAAA,EAAA,UAAA,CAEE,oBAAC,SAAD,EAAA,UACG;;;;;;;;;UAUI,CAAA,GACP,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;EACP,eAAY;CACb,CAAA,CACD,EAAA,CAAA;AAEJ,IACC,WAAW,cAAc;CACxB,MAAM,cAAc,UAAU,aAAa;CAC3C,MAAM,aAAa,UAAU,aAAa;CAE1C,IAAI,CAAC,eAAe,CAAC,YAAY,OAAO;CAExC,IAAI,gBAAgB,YAAY,OAAO;CAEvC,OACE,KAAK,IAAI,UAAU,YAAY,UAAU,SAAS,IAAI,KACtD,UAAU,aAAa,UAAU,YACjC,UAAU,mBAAmB,UAAU;AAE3C,CACF;AAEA,qBAAqB,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"BloodParticles3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodParticles3D.tsx"],"sourcesContent":["/**\n * BloodParticles3D - Realistic blood splatter particle system\n *\n * Creates physics-based blood particles that spray from impact points with gravity,\n * creating pools on the arena floor. Optimized for 60fps with instanced rendering.\n *\n * Features:\n * - Gravity-based particle physics\n * - Blood pool accumulation on floor\n * - 10-second fade-out for pools\n * - Instanced rendering for performance\n * - Mobile-optimized particle counts\n *\n * @module components/combat/BloodParticles3D\n * @category Combat Effects\n * @korean 피입자3D\n */\n\nimport { Points, PointMaterial } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../../utils/threeObjectPool\";\n\n/**\n * Blood particle data structure for efficient simulation\n * \n * PERFORMANCE: position and velocity now use pooled Vector3 objects\n * that are acquired on particle creation and released on expiration\n */\ninterface BloodParticle {\n /** Current position [x, y, z] - POOLED Vector3 */\n position: THREE.Vector3;\n /** Current velocity [x, y, z] - POOLED Vector3 */\n velocity: THREE.Vector3;\n /** Particle lifetime in seconds */\n lifetime: number;\n /** Time elapsed since creation */\n age: number;\n /** Whether particle has settled on floor */\n settled: boolean;\n /** Flag to track if vectors are pooled and need release */\n isPooled: boolean;\n}\n\n/**\n * Blood splatter effect configuration\n */\nexport interface BloodSplatterEffect {\n /** Unique identifier */\n readonly id: string;\n /** Origin position in 3D world space */\n readonly position: [number, number, number];\n /** Impact direction for splatter */\n readonly direction: [number, number, number];\n /** Intensity of effect (0.0 to 1.0) */\n readonly intensity: number;\n /** Timestamp when effect was created */\n readonly startTime: number;\n}\n\n/**\n * Props for BloodParticles3D component\n */\nexport interface BloodParticles3DProps {\n /** Active blood splatter effects to render */\n readonly effects: readonly BloodSplatterEffect[];\n /** Whether to enable blood effects (violence settings) */\n readonly enabled?: boolean;\n /** Mobile device mode (reduced particle count) */\n readonly isMobile?: boolean;\n /** Callback when effect completes */\n readonly onEffectComplete?: (effectId: string) => void;\n}\n\n/**\n * Performance and physics constants\n */\nconst BLOOD_CONSTANTS = {\n /** Gravity acceleration (m/s²) */\n GRAVITY: -9.8,\n /** Maximum particles per splatter effect */\n MAX_PARTICLES_DESKTOP: 300,\n MAX_PARTICLES_MOBILE: 100,\n /** Particle lifetime in seconds */\n PARTICLE_LIFETIME: 2.0,\n /** Blood pool fade duration in seconds */\n POOL_FADE_DURATION: 10.0,\n /** Initial velocity range */\n VELOCITY_MIN: 2.0,\n VELOCITY_MAX: 5.0,\n /** Spread angle in radians */\n SPREAD_ANGLE: Math.PI / 3,\n /** Floor Y position */\n FLOOR_Y: 0.0,\n /** Particle size */\n PARTICLE_SIZE: 0.05,\n /**\n * Maximum per-frame delta time (seconds) used to clamp the blood physics update.\n * \n * We cap the simulation step at ~33ms (1/30) to avoid the classic\n * \"spiral of death\" when the frame rate drops: very large timesteps can\n * cause unstable motion, particles tunneling through the floor, or\n * visually exaggerated splatter. Treating anything below 30fps as\n * \"slow motion\" for this effect keeps the blood behavior stable even\n * on slower devices. If the engine's minimum target frame rate changes,\n * adjust this threshold accordingly.\n */\n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * Generate initial particles for a blood splatter effect\n * \n * PERFORMANCE OPTIMIZATION: Uses ThreeObjectPools for ALL Vector3 allocations\n * \n * Pooling Strategy (UPDATED):\n * - Acquire temp vectors for calculations (baseDir, origin, axis vectors) - RELEASED\n * - Acquire position/velocity from pool for particle ownership - MUST BE RELEASED on expiration\n * \n * Memory Impact:\n * - Before: 600 Vector3 allocations per splatter (2 per particle × 300)\n * - After: 5 temp vectors (reused) + particles use pooled vectors (released on expiration)\n * - Reduction: ~99.2% fewer allocations (600 → 5 temp + pooled reuse)\n * \n * @param effect - Blood splatter effect configuration\n * @param maxParticles - Maximum number of particles to generate\n * @returns Array of blood particles with ALL vectors from pool (must be released later)\n */\nconst generateBloodParticles = (\n effect: BloodSplatterEffect,\n maxParticles: number\n): BloodParticle[] => {\n const particles: BloodParticle[] = [];\n const particleCount = Math.floor(maxParticles * effect.intensity);\n\n const baseDir = ThreeObjectPools.vector3.acquire();\n const origin = ThreeObjectPools.vector3.acquire();\n const direction = ThreeObjectPools.vector3.acquire();\n const yAxis = ThreeObjectPools.vector3.acquire();\n const zAxis = ThreeObjectPools.vector3.acquire();\n\n baseDir.set(...effect.direction).normalize();\n origin.set(...effect.position);\n yAxis.set(0, 1, 0);\n zAxis.set(0, 0, 1);\n\n for (let i = 0; i < particleCount; i++) {\n const spreadAngle = BLOOD_CONSTANTS.SPREAD_ANGLE;\n const phi = (Math.random() - 0.5) * spreadAngle;\n const theta = Math.random() * Math.PI * 2;\n\n direction.copy(baseDir);\n direction.applyAxisAngle(yAxis, phi);\n direction.applyAxisAngle(zAxis, theta);\n\n const speed =\n BLOOD_CONSTANTS.VELOCITY_MIN +\n Math.random() * (BLOOD_CONSTANTS.VELOCITY_MAX - BLOOD_CONSTANTS.VELOCITY_MIN);\n\n const particlePosition = ThreeObjectPools.vector3.acquire();\n const particleVelocity = ThreeObjectPools.vector3.acquire();\n \n particlePosition.copy(origin);\n particleVelocity.copy(direction).multiplyScalar(speed);\n\n particles.push({\n position: particlePosition,\n velocity: particleVelocity,\n lifetime: BLOOD_CONSTANTS.PARTICLE_LIFETIME,\n age: 0,\n settled: false,\n isPooled: true, // Mark as pooled for cleanup\n });\n }\n\n ThreeObjectPools.vector3.release(baseDir);\n ThreeObjectPools.vector3.release(origin);\n ThreeObjectPools.vector3.release(direction);\n ThreeObjectPools.vector3.release(yAxis);\n ThreeObjectPools.vector3.release(zAxis);\n\n return particles;\n};\n\n/**\n * BloodParticles3D Component\n *\n * Renders realistic blood splatter effects using physics-based particle simulation.\n * Optimized for performance with instanced rendering and efficient physics updates.\n *\n * @example\n * ```tsx\n * const [bloodEffects, setBloodEffects] = useState<BloodSplatterEffect[]>([]);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], direction: [number, number, number]) => {\n * setBloodEffects([...bloodEffects, {\n * id: generateId(),\n * position,\n * direction,\n * intensity: 0.8,\n * startTime: Date.now(),\n * }]);\n * };\n *\n * <BloodParticles3D\n * effects={bloodEffects}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onEffectComplete={(id) => {\n * setBloodEffects(prev => prev.filter(e => e.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodParticles3D: React.FC<BloodParticles3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const particlesRef = useRef<Map<string, BloodParticle[]>>(new Map());\n const poolParticlesRef = useRef<BloodParticle[]>([]);\n const pointsRef = useRef<THREE.Points>(null);\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const maxParticles = isMobile\n ? BLOOD_CONSTANTS.MAX_PARTICLES_MOBILE\n : BLOOD_CONSTANTS.MAX_PARTICLES_DESKTOP;\n\n const bloodColor = useMemo(() => KOREAN_COLORS.BLOODLOSS_INDICATOR, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n effects.forEach((effect) => {\n if (!particlesRef.current.has(effect.id)) {\n const particles = generateBloodParticles(effect, maxParticles);\n particlesRef.current.set(effect.id, particles);\n }\n });\n\n const effectIds = new Set(effects.map((e) => e.id));\n particlesRef.current.forEach((particles, id) => {\n if (!effectIds.has(id)) {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n particlesRef.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n }, [effects, enabled, maxParticles]);\n\n React.useEffect(() => {\n const currentParticles = particlesRef.current;\n const currentPoolParticles = poolParticlesRef.current;\n const currentCompletedEffects = completedEffectsRef.current;\n \n return () => {\n currentParticles.forEach((particles) => {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n });\n \n currentPoolParticles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n \n poolParticlesRef.current = [];\n \n currentParticles.clear();\n currentCompletedEffects.clear();\n };\n }, []);\n\n const initialPositions = useMemo(() => {\n const positions = new Float32Array(maxParticles * 3);\n return positions;\n }, [maxParticles]);\n\n useFrame((_, delta) => {\n if (!enabled || !pointsRef.current) return;\n\n const safeDelta = Math.min(delta, BLOOD_CONSTANTS.MAX_DELTA);\n\n let totalParticleIndex = 0;\n const attr = pointsRef.current.geometry.attributes.position;\n const posArray = attr.array as Float32Array;\n\n particlesRef.current.forEach((particles, effectId) => {\n let hasActiveParticles = false;\n\n for (let i = 0; i < particles.length; i++) {\n const p = particles[i];\n p.age += safeDelta;\n\n if (!p.settled) {\n p.velocity.y += BLOOD_CONSTANTS.GRAVITY * safeDelta;\n\n p.position.addScaledVector(p.velocity, safeDelta);\n\n if (p.position.y <= BLOOD_CONSTANTS.FLOOR_Y) {\n p.position.y = BLOOD_CONSTANTS.FLOOR_Y;\n p.velocity.set(0, 0, 0);\n p.settled = true;\n p.lifetime = BLOOD_CONSTANTS.POOL_FADE_DURATION;\n p.age = 0;\n\n poolParticlesRef.current.push(p);\n } else {\n hasActiveParticles = true;\n }\n }\n\n if (totalParticleIndex < posArray.length / 3) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n } else if (process.env.NODE_ENV === \"development\") {\n console.warn(\n `BloodParticles3D: Particle buffer full (${posArray.length / 3} particles). ` +\n `Additional particles are not rendered. Consider reducing particle counts or ` +\n `increasing maxParticles if this happens frequently.`\n );\n }\n }\n\n if (!hasActiveParticles && !completedEffectsRef.current.has(effectId)) {\n completedEffectsRef.current.add(effectId);\n onEffectComplete?.(effectId);\n }\n });\n\n const maxVisibleParticles = Math.min(maxParticles, posArray.length / 3);\n\n if (totalParticleIndex >= maxVisibleParticles) {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n \n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n \n return isAlive;\n });\n } else {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n\n if (isAlive && totalParticleIndex < maxVisibleParticles) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n }\n\n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n\n return isAlive;\n });\n }\n\n attr.needsUpdate = true;\n });\n\n if (!enabled || effects.length === 0) {\n return null;\n }\n\n return (\n <Points\n ref={pointsRef}\n positions={initialPositions}\n data-testid=\"blood-particles-3d\"\n >\n <PointMaterial\n color={bloodColor}\n size={BLOOD_CONSTANTS.PARTICLE_SIZE}\n sizeAttenuation\n transparent\n opacity={0.9}\n depthWrite={false}\n blending={THREE.NormalBlending}\n />\n </Points>\n );\n};\n\nexport default BloodParticles3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"BloodParticles3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodParticles3D.tsx"],"sourcesContent":["/**\n * BloodParticles3D - Realistic blood splatter particle system\n *\n * Creates physics-based blood particles that spray from impact points with gravity,\n * creating pools on the arena floor. Optimized for 60fps with instanced rendering.\n *\n * Features:\n * - Gravity-based particle physics\n * - Blood pool accumulation on floor\n * - 10-second fade-out for pools\n * - Instanced rendering for performance\n * - Mobile-optimized particle counts\n *\n * @module components/combat/BloodParticles3D\n * @category Combat Effects\n * @korean 피입자3D\n */\n\nimport { Points, PointMaterial } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../../utils/threeObjectPool\";\n\n/**\n * Blood particle data structure for efficient simulation\n * \n * PERFORMANCE: position and velocity now use pooled Vector3 objects\n * that are acquired on particle creation and released on expiration\n */\ninterface BloodParticle {\n /** Current position [x, y, z] - POOLED Vector3 */\n position: THREE.Vector3;\n /** Current velocity [x, y, z] - POOLED Vector3 */\n velocity: THREE.Vector3;\n /** Particle lifetime in seconds */\n lifetime: number;\n /** Time elapsed since creation */\n age: number;\n /** Whether particle has settled on floor */\n settled: boolean;\n /** Flag to track if vectors are pooled and need release */\n isPooled: boolean;\n}\n\n/**\n * Blood splatter effect configuration\n */\nexport interface BloodSplatterEffect {\n /** Unique identifier */\n readonly id: string;\n /** Origin position in 3D world space */\n readonly position: [number, number, number];\n /** Impact direction for splatter */\n readonly direction: [number, number, number];\n /** Intensity of effect (0.0 to 1.0) */\n readonly intensity: number;\n /** Timestamp when effect was created */\n readonly startTime: number;\n}\n\n/**\n * Props for BloodParticles3D component\n */\nexport interface BloodParticles3DProps {\n /** Active blood splatter effects to render */\n readonly effects: readonly BloodSplatterEffect[];\n /** Whether to enable blood effects (violence settings) */\n readonly enabled?: boolean;\n /** Mobile device mode (reduced particle count) */\n readonly isMobile?: boolean;\n /** Callback when effect completes */\n readonly onEffectComplete?: (effectId: string) => void;\n}\n\n/**\n * Performance and physics constants\n */\nconst BLOOD_CONSTANTS = {\n /** Gravity acceleration (m/s²) */\n GRAVITY: -9.8,\n /** Maximum particles per splatter effect */\n MAX_PARTICLES_DESKTOP: 300,\n MAX_PARTICLES_MOBILE: 100,\n /** Particle lifetime in seconds */\n PARTICLE_LIFETIME: 2.0,\n /** Blood pool fade duration in seconds */\n POOL_FADE_DURATION: 10.0,\n /** Initial velocity range */\n VELOCITY_MIN: 2.0,\n VELOCITY_MAX: 5.0,\n /** Spread angle in radians */\n SPREAD_ANGLE: Math.PI / 3,\n /** Floor Y position */\n FLOOR_Y: 0.0,\n /** Particle size */\n PARTICLE_SIZE: 0.05,\n /**\n * Maximum per-frame delta time (seconds) used to clamp the blood physics update.\n * \n * We cap the simulation step at ~33ms (1/30) to avoid the classic\n * \"spiral of death\" when the frame rate drops: very large timesteps can\n * cause unstable motion, particles tunneling through the floor, or\n * visually exaggerated splatter. Treating anything below 30fps as\n * \"slow motion\" for this effect keeps the blood behavior stable even\n * on slower devices. If the engine's minimum target frame rate changes,\n * adjust this threshold accordingly.\n */\n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * Generate initial particles for a blood splatter effect\n * \n * PERFORMANCE OPTIMIZATION: Uses ThreeObjectPools for ALL Vector3 allocations\n * \n * Pooling Strategy (UPDATED):\n * - Acquire temp vectors for calculations (baseDir, origin, axis vectors) - RELEASED\n * - Acquire position/velocity from pool for particle ownership - MUST BE RELEASED on expiration\n * \n * Memory Impact:\n * - Before: 600 Vector3 allocations per splatter (2 per particle × 300)\n * - After: 5 temp vectors (reused) + particles use pooled vectors (released on expiration)\n * - Reduction: ~99.2% fewer allocations (600 → 5 temp + pooled reuse)\n * \n * @param effect - Blood splatter effect configuration\n * @param maxParticles - Maximum number of particles to generate\n * @returns Array of blood particles with ALL vectors from pool (must be released later)\n */\nconst generateBloodParticles = (\n effect: BloodSplatterEffect,\n maxParticles: number\n): BloodParticle[] => {\n const particles: BloodParticle[] = [];\n const particleCount = Math.floor(maxParticles * effect.intensity);\n\n const baseDir = ThreeObjectPools.vector3.acquire();\n const origin = ThreeObjectPools.vector3.acquire();\n const direction = ThreeObjectPools.vector3.acquire();\n const yAxis = ThreeObjectPools.vector3.acquire();\n const zAxis = ThreeObjectPools.vector3.acquire();\n\n baseDir.set(...effect.direction).normalize();\n origin.set(...effect.position);\n yAxis.set(0, 1, 0);\n zAxis.set(0, 0, 1);\n\n for (let i = 0; i < particleCount; i++) {\n const spreadAngle = BLOOD_CONSTANTS.SPREAD_ANGLE;\n const phi = (Math.random() - 0.5) * spreadAngle;\n const theta = Math.random() * Math.PI * 2;\n\n direction.copy(baseDir);\n direction.applyAxisAngle(yAxis, phi);\n direction.applyAxisAngle(zAxis, theta);\n\n const speed =\n BLOOD_CONSTANTS.VELOCITY_MIN +\n Math.random() * (BLOOD_CONSTANTS.VELOCITY_MAX - BLOOD_CONSTANTS.VELOCITY_MIN);\n\n const particlePosition = ThreeObjectPools.vector3.acquire();\n const particleVelocity = ThreeObjectPools.vector3.acquire();\n \n particlePosition.copy(origin);\n particleVelocity.copy(direction).multiplyScalar(speed);\n\n particles.push({\n position: particlePosition,\n velocity: particleVelocity,\n lifetime: BLOOD_CONSTANTS.PARTICLE_LIFETIME,\n age: 0,\n settled: false,\n isPooled: true, // Mark as pooled for cleanup\n });\n }\n\n ThreeObjectPools.vector3.release(baseDir);\n ThreeObjectPools.vector3.release(origin);\n ThreeObjectPools.vector3.release(direction);\n ThreeObjectPools.vector3.release(yAxis);\n ThreeObjectPools.vector3.release(zAxis);\n\n return particles;\n};\n\n/**\n * BloodParticles3D Component\n *\n * Renders realistic blood splatter effects using physics-based particle simulation.\n * Optimized for performance with instanced rendering and efficient physics updates.\n *\n * @example\n * ```tsx\n * const [bloodEffects, setBloodEffects] = useState<BloodSplatterEffect[]>([]);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], direction: [number, number, number]) => {\n * setBloodEffects([...bloodEffects, {\n * id: generateId(),\n * position,\n * direction,\n * intensity: 0.8,\n * startTime: Date.now(),\n * }]);\n * };\n *\n * <BloodParticles3D\n * effects={bloodEffects}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onEffectComplete={(id) => {\n * setBloodEffects(prev => prev.filter(e => e.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodParticles3D: React.FC<BloodParticles3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const particlesRef = useRef<Map<string, BloodParticle[]>>(new Map());\n const poolParticlesRef = useRef<BloodParticle[]>([]);\n const pointsRef = useRef<THREE.Points>(null);\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const maxParticles = isMobile\n ? BLOOD_CONSTANTS.MAX_PARTICLES_MOBILE\n : BLOOD_CONSTANTS.MAX_PARTICLES_DESKTOP;\n\n const bloodColor = useMemo(() => KOREAN_COLORS.BLOODLOSS_INDICATOR, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n effects.forEach((effect) => {\n if (!particlesRef.current.has(effect.id)) {\n const particles = generateBloodParticles(effect, maxParticles);\n particlesRef.current.set(effect.id, particles);\n }\n });\n\n const effectIds = new Set(effects.map((e) => e.id));\n particlesRef.current.forEach((particles, id) => {\n if (!effectIds.has(id)) {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n particlesRef.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n }, [effects, enabled, maxParticles]);\n\n React.useEffect(() => {\n const currentParticles = particlesRef.current;\n const currentPoolParticles = poolParticlesRef.current;\n const currentCompletedEffects = completedEffectsRef.current;\n \n return () => {\n currentParticles.forEach((particles) => {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n });\n \n currentPoolParticles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n \n poolParticlesRef.current = [];\n \n currentParticles.clear();\n currentCompletedEffects.clear();\n };\n }, []);\n\n const initialPositions = useMemo(() => {\n const positions = new Float32Array(maxParticles * 3);\n return positions;\n }, [maxParticles]);\n\n useFrame((_, delta) => {\n if (!enabled || !pointsRef.current) return;\n\n const safeDelta = Math.min(delta, BLOOD_CONSTANTS.MAX_DELTA);\n\n let totalParticleIndex = 0;\n const attr = pointsRef.current.geometry.attributes.position;\n const posArray = attr.array as Float32Array;\n\n particlesRef.current.forEach((particles, effectId) => {\n let hasActiveParticles = false;\n\n for (let i = 0; i < particles.length; i++) {\n const p = particles[i];\n p.age += safeDelta;\n\n if (!p.settled) {\n p.velocity.y += BLOOD_CONSTANTS.GRAVITY * safeDelta;\n\n p.position.addScaledVector(p.velocity, safeDelta);\n\n if (p.position.y <= BLOOD_CONSTANTS.FLOOR_Y) {\n p.position.y = BLOOD_CONSTANTS.FLOOR_Y;\n p.velocity.set(0, 0, 0);\n p.settled = true;\n p.lifetime = BLOOD_CONSTANTS.POOL_FADE_DURATION;\n p.age = 0;\n\n poolParticlesRef.current.push(p);\n } else {\n hasActiveParticles = true;\n }\n }\n\n if (totalParticleIndex < posArray.length / 3) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n } else if (process.env.NODE_ENV === \"development\") {\n console.warn(\n `BloodParticles3D: Particle buffer full (${posArray.length / 3} particles). ` +\n `Additional particles are not rendered. Consider reducing particle counts or ` +\n `increasing maxParticles if this happens frequently.`\n );\n }\n }\n\n if (!hasActiveParticles && !completedEffectsRef.current.has(effectId)) {\n completedEffectsRef.current.add(effectId);\n onEffectComplete?.(effectId);\n }\n });\n\n const maxVisibleParticles = Math.min(maxParticles, posArray.length / 3);\n\n if (totalParticleIndex >= maxVisibleParticles) {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n \n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n \n return isAlive;\n });\n } else {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n\n if (isAlive && totalParticleIndex < maxVisibleParticles) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n }\n\n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n\n return isAlive;\n });\n }\n\n attr.needsUpdate = true;\n });\n\n if (!enabled || effects.length === 0) {\n return null;\n }\n\n return (\n <Points\n ref={pointsRef}\n positions={initialPositions}\n data-testid=\"blood-particles-3d\"\n >\n <PointMaterial\n color={bloodColor}\n size={BLOOD_CONSTANTS.PARTICLE_SIZE}\n sizeAttenuation\n transparent\n opacity={0.9}\n depthWrite={false}\n blending={THREE.NormalBlending}\n />\n </Points>\n );\n};\n\nexport default BloodParticles3D;\n"],"mappings":"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"}