blacktrigram 0.7.39 → 0.7.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioAssetLoader.js.map +1 -1
  3. package/lib/audio/AudioAssetRegistry.js.map +1 -1
  4. package/lib/audio/AudioCache.js.map +1 -1
  5. package/lib/audio/AudioManager.js.map +1 -1
  6. package/lib/audio/AudioMonitor.js.map +1 -1
  7. package/lib/audio/AudioPool.js.map +1 -1
  8. package/lib/audio/AudioProvider.js.map +1 -1
  9. package/lib/audio/AudioUtils.js.map +1 -1
  10. package/lib/audio/BoneImpactAudioMap.js.map +1 -1
  11. package/lib/audio/VariantSelector.js.map +1 -1
  12. package/lib/audio/types.js.map +1 -1
  13. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  14. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  15. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  18. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  19. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  20. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  25. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  26. package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
  27. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  28. package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
  29. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  30. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  31. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  32. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  33. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  34. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  36. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  37. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  38. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  39. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  40. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  41. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  42. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  43. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  44. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  45. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  46. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  47. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  48. package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
  49. package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
  50. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  51. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  52. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  53. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  54. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  55. package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
  56. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  57. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  58. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  59. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  60. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  61. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  62. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  63. package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
  64. package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
  65. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  66. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  67. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  68. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  69. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  72. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  74. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  75. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  76. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  77. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  78. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  79. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  80. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  81. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  83. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  84. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  85. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  86. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  87. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  88. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  90. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  91. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  92. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  93. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  94. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  95. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  96. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  97. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  98. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  99. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  100. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  101. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  102. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  103. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  104. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  105. package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
  106. package/lib/components/shared/base/BaseButton.js.map +1 -1
  107. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  108. package/lib/components/shared/base/BasePanel.js.map +1 -1
  109. package/lib/components/shared/base/BaseText.js.map +1 -1
  110. package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
  111. package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
  112. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  113. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  114. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  115. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  116. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  117. package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
  118. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  119. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  120. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  121. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  122. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  123. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  124. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  125. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  126. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  127. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  128. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  129. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  130. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  131. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  132. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  133. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  134. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  135. package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
  136. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  137. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  138. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  139. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  140. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  141. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  142. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  143. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  144. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  145. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  146. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  147. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  148. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  149. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  150. package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
  151. package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
  152. package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
  153. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  154. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  155. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  156. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  157. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  158. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  159. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  160. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  161. package/lib/components/shared/ui/BackButton.js.map +1 -1
  162. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  163. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  164. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  165. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  166. package/lib/components/shared/ui/SplashScreen.js +2 -2
  167. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  168. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  169. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  170. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  171. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  172. package/lib/constants/bodyDimensions.js.map +1 -1
  173. package/lib/constants/bodyRenderingConstants.js.map +1 -1
  174. package/lib/data/archetypeClothing.js.map +1 -1
  175. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  176. package/lib/data/techniqueMappings.js.map +1 -1
  177. package/lib/data/techniques.js.map +1 -1
  178. package/lib/hooks/useActionFeedback.js.map +1 -1
  179. package/lib/hooks/useBalanceAnimations.js.map +1 -1
  180. package/lib/hooks/useCombatTimer.js.map +1 -1
  181. package/lib/hooks/useDebounce.js.map +1 -1
  182. package/lib/hooks/useHUDLayout.js.map +1 -1
  183. package/lib/hooks/useHandPoseTransitions.js.map +1 -1
  184. package/lib/hooks/useKeyboardControls.js.map +1 -1
  185. package/lib/hooks/useMatchCountdown.js.map +1 -1
  186. package/lib/hooks/useMuscleActivation.js.map +1 -1
  187. package/lib/hooks/usePauseMenu.js.map +1 -1
  188. package/lib/hooks/usePlayerAnimation.js.map +1 -1
  189. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  190. package/lib/hooks/useRoundTransition.js.map +1 -1
  191. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  192. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  193. package/lib/hooks/useThrottle.js.map +1 -1
  194. package/lib/hooks/useTouchControls.js.map +1 -1
  195. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  196. package/lib/hooks/useWindowSize.js.map +1 -1
  197. package/lib/systems/CombatSystem.js.map +1 -1
  198. package/lib/systems/EffectCalculator.js.map +1 -1
  199. package/lib/systems/LayoutSystem.js.map +1 -1
  200. package/lib/systems/PlayerEffectManager.js.map +1 -1
  201. package/lib/systems/ResponsiveScaling.js.map +1 -1
  202. package/lib/systems/TrigramSystem.js.map +1 -1
  203. package/lib/systems/VitalPointSystem.js.map +1 -1
  204. package/lib/systems/ai/AIPersonality.js.map +1 -1
  205. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  206. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  207. package/lib/systems/ai/ComboSystem.js.map +1 -1
  208. package/lib/systems/ai/DecisionTree.js.map +1 -1
  209. package/lib/systems/ai/TrainingAI.js.map +1 -1
  210. package/lib/systems/ai/types.js.map +1 -1
  211. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  212. package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
  213. package/lib/systems/animation/builders/HandPoses.js.map +1 -1
  214. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  215. package/lib/systems/animation/builders/KeyframeInterpolation.js +3 -90
  216. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  217. package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
  218. package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
  219. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  220. package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
  221. package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
  222. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  223. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  224. package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
  225. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  226. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  227. package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
  228. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  229. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  230. package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
  231. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  232. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  233. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  234. package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
  235. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  236. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  237. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  238. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  239. package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
  240. package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
  241. package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
  242. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  243. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  244. package/lib/systems/animation/core/types.js.map +1 -1
  245. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  246. package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
  247. package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
  248. package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
  249. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  250. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  251. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  252. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  253. package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
  254. package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
  255. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  256. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  257. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  258. package/lib/systems/bodypart/types.js.map +1 -1
  259. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  260. package/lib/systems/breathing/feedback.js.map +1 -1
  261. package/lib/systems/breathing/integration.js.map +1 -1
  262. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  263. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  264. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  265. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  266. package/lib/systems/combat/FallIntegration.js.map +1 -1
  267. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  268. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  269. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  270. package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
  271. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  272. package/lib/systems/combat/typeGuards.js.map +1 -1
  273. package/lib/systems/effects.js.map +1 -1
  274. package/lib/systems/game.js.map +1 -1
  275. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  276. package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
  277. package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
  278. package/lib/systems/movement/integration.js.map +1 -1
  279. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  280. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  281. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  282. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  283. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  284. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  285. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  286. package/lib/systems/trigram/KoreanCulture.js.map +1 -1
  287. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  288. package/lib/systems/trigram/StanceManager.js.map +1 -1
  289. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  290. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  291. package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
  292. package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
  293. package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
  294. package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
  295. package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
  296. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  297. package/lib/systems/trigram/techniques/index.js.map +1 -1
  298. package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
  299. package/lib/systems/trigram/types.js.map +1 -1
  300. package/lib/systems/types.js.map +1 -1
  301. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  302. package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
  303. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  304. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  305. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  306. package/lib/types/AccessibilityTypes.js.map +1 -1
  307. package/lib/types/PhysicsTypes.js.map +1 -1
  308. package/lib/types/common.js.map +1 -1
  309. package/lib/types/constants/colors.js.map +1 -1
  310. package/lib/types/constants/designSystem.js.map +1 -1
  311. package/lib/types/constants/layout.js.map +1 -1
  312. package/lib/types/constants/performance.js.map +1 -1
  313. package/lib/types/constants/typography.js.map +1 -1
  314. package/lib/types/facial.js.map +1 -1
  315. package/lib/types/hand-animation.js.map +1 -1
  316. package/lib/types/injury.js.map +1 -1
  317. package/lib/types/physics.js.map +1 -1
  318. package/lib/types/skeletal.js.map +1 -1
  319. package/lib/types/techniqueId.js.map +1 -1
  320. package/lib/utils/accessibility.js.map +1 -1
  321. package/lib/utils/arenaWorldDimensions.js.map +1 -1
  322. package/lib/utils/assetConfig.js.map +1 -1
  323. package/lib/utils/characterScaling.js.map +1 -1
  324. package/lib/utils/colorHelpers.js.map +1 -1
  325. package/lib/utils/colorUtils.js.map +1 -1
  326. package/lib/utils/combatReadiness.js.map +1 -1
  327. package/lib/utils/controlMapping.js.map +1 -1
  328. package/lib/utils/deviceDetection.js.map +1 -1
  329. package/lib/utils/effectUtils.js.map +1 -1
  330. package/lib/utils/fabricTextures.js.map +1 -1
  331. package/lib/utils/hapticFeedback.js.map +1 -1
  332. package/lib/utils/haptics.js.map +1 -1
  333. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  334. package/lib/utils/inputSystem.js.map +1 -1
  335. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  336. package/lib/utils/math.js.map +1 -1
  337. package/lib/utils/mobileLayoutHelpers.js.map +1 -1
  338. package/lib/utils/mobileUIUtils.js.map +1 -1
  339. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  340. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  341. package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
  342. package/lib/utils/performanceOptimization.js.map +1 -1
  343. package/lib/utils/player3DHelpers.js.map +1 -1
  344. package/lib/utils/playerUtils.js.map +1 -1
  345. package/lib/utils/responsiveLayout.js.map +1 -1
  346. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  347. package/lib/utils/responsiveOrientationConstants.js.map +1 -1
  348. package/lib/utils/safeAreaUtils.js.map +1 -1
  349. package/lib/utils/sharedPhysicsConfig.js.map +1 -1
  350. package/lib/utils/skeletonScaling.js.map +1 -1
  351. package/lib/utils/stanceHelpers.js.map +1 -1
  352. package/lib/utils/threeObjectPool.js.map +1 -1
  353. package/lib/utils/visualEffects.js.map +1 -1
  354. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"ComboCounter.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ComboCounter.tsx"],"sourcesContent":["/**\n * ComboCounter - Combo counter display component\n *\n * Displays the current combo count with Korean-English bilingual text.\n * Animates on combo increment and shows milestone indicators.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/ui/ComboCounter\n * @category Shared UI\n * @korean 콤보카운터\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\n/**\n * Props for the ComboCounter component\n */\nexport interface ComboCounterProps {\n /** Current combo count */\n readonly combo: number;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Minimum combo to display (default: 2) */\n readonly minDisplayCombo?: number;\n}\n\n/**\n * Get combo tier color based on combo count\n */\nfunction getComboColor(combo: number): string {\n if (combo >= 10) {\n // Rainbow effect for high combos - use magenta\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n }\n if (combo >= 7) {\n // Critical tier - red\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n }\n if (combo >= 5) {\n // High tier - gold\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n }\n if (combo >= 3) {\n // Medium tier - cyan\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n // Low tier - white\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n}\n\n/**\n * Get glow color based on combo tier\n */\nfunction getGlowColor(combo: number): string {\n if (combo >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n }\n if (combo >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n }\n if (combo >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n }\n if (combo >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n}\n\n/**\n * Get combo milestone text\n */\nfunction getComboMilestone(\n combo: number\n): { korean: string; english: string } | null {\n if (combo === 5) {\n return { korean: \"훌륭합니다!\", english: \"Great!\" };\n }\n if (combo === 10) {\n return { korean: \"놀라운 연속 공격!\", english: \"Amazing!\" };\n }\n if (combo === 15) {\n return { korean: \"전설적인 공격!\", english: \"Legendary!\" };\n }\n if (combo === 20) {\n return { korean: \"신의 일격!\", english: \"GODLIKE!\" };\n }\n return null;\n}\n\n/**\n * ComboCounter Component\n *\n * Displays the current combo count with animations and milestone indicators.\n * Only visible when combo >= minDisplayCombo (default: 2).\n *\n * @example\n * ```tsx\n * <ComboCounter\n * combo={5}\n * isMobile={isMobile}\n * />\n * ```\n */\nexport const ComboCounter: React.FC<ComboCounterProps> = ({\n combo,\n isMobile = false,\n minDisplayCombo = 2,\n}) => {\n // Animation state - all values that affect render must be in state\n const [scale, setScale] = useState(1);\n const [showMilestone, setShowMilestone] = useState(false);\n const [prevCombo, setPrevCombo] = useState(combo);\n const milestoneTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null\n );\n const scaleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Calculate 3D position - center top of screen\n const position3D: [number, number, number] = useMemo(() => {\n // Position at top center of scene\n return [0, 4.5, 0];\n }, []);\n\n // Handle combo changes via useEffect - this is the proper React pattern\n useEffect(() => {\n if (combo > prevCombo) {\n // Scale up on combo increment - defer to avoid cascading renders\n setTimeout(() => setScale(1.3), 0);\n\n // Clear previous scale timeout\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n scaleTimeoutRef.current = setTimeout(() => {\n setScale(1);\n }, 150);\n\n // Check for milestone\n const milestone = getComboMilestone(combo);\n if (milestone) {\n setTimeout(() => setShowMilestone(true), 0);\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n milestoneTimeoutRef.current = setTimeout(() => {\n setShowMilestone(false);\n }, 1500);\n }\n }\n // Defer prevCombo update to avoid cascading renders\n setTimeout(() => setPrevCombo(combo), 0);\n }, [combo, prevCombo]);\n\n // Cleanup\n useEffect(() => {\n return () => {\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n };\n }, []);\n\n // Don't render if combo is below minimum\n if (combo < minDisplayCombo) {\n return null;\n }\n\n const milestone = getComboMilestone(combo);\n const comboColor = getComboColor(combo);\n const glowColor = getGlowColor(combo);\n // Get background color with alpha for milestone box\n const getMilestoneBackground = (comboVal: number): string => {\n if (comboVal >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.3);\n }\n if (comboVal >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.3);\n }\n if (comboVal >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n }\n if (comboVal >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.3);\n };\n\n // Font sizes\n const mainFontSize = isMobile ? 32 : 48;\n const subFontSize = isMobile ? 16 : 20;\n const milestoneFontSize = isMobile ? 20 : 28;\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"combo-counter\"\n className=\"hud-animated\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n transform: `scale(${scale})`,\n transition: \"transform 0.15s ease-out\",\n animation: combo >= 5 ? \"comboFlash 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {/* Main combo number */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n lineHeight: 1,\n }}\n >\n {combo}\n </div>\n\n {/* Korean text */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}, 2px 2px 4px rgba(0, 0, 0, 0.8)`,\n marginTop: \"4px\",\n }}\n >\n 연속 타격!\n </div>\n\n {/* English text */}\n <div\n style={{\n fontSize: `${subFontSize - 4}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {combo} HIT COMBO!\n </div>\n\n {/* Milestone indicator */}\n {showMilestone && milestone && (\n <div\n className=\"hud-animated\"\n style={{\n marginTop: \"8px\",\n padding: \"4px 16px\",\n background: getMilestoneBackground(combo),\n borderRadius: \"4px\",\n border: `2px solid ${comboColor}`,\n animation: \"comboFlash 1s ease-in-out infinite\",\n }}\n >\n <div\n style={{\n fontSize: `${milestoneFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}`,\n }}\n >\n {milestone.korean}\n </div>\n <div\n style={{\n fontSize: `${milestoneFontSize - 6}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n {milestone.english}\n </div>\n </div>\n )}\n </div>\n\n {/* CSS Animation - kept for backwards compatibility */}\n <style>{`\n @keyframes pulse {\n 0%, 100% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n }\n `}</style>\n </Html>\n );\n};\n\nexport default ComboCounter;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ComboCounter.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ComboCounter.tsx"],"sourcesContent":["/**\n * ComboCounter - Combo counter display component\n *\n * Displays the current combo count with Korean-English bilingual text.\n * Animates on combo increment and shows milestone indicators.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/ui/ComboCounter\n * @category Shared UI\n * @korean 콤보카운터\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\n/**\n * Props for the ComboCounter component\n */\nexport interface ComboCounterProps {\n /** Current combo count */\n readonly combo: number;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Minimum combo to display (default: 2) */\n readonly minDisplayCombo?: number;\n}\n\n/**\n * Get combo tier color based on combo count\n */\nfunction getComboColor(combo: number): string {\n if (combo >= 10) {\n // Rainbow effect for high combos - use magenta\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n }\n if (combo >= 7) {\n // Critical tier - red\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n }\n if (combo >= 5) {\n // High tier - gold\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n }\n if (combo >= 3) {\n // Medium tier - cyan\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n // Low tier - white\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n}\n\n/**\n * Get glow color based on combo tier\n */\nfunction getGlowColor(combo: number): string {\n if (combo >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n }\n if (combo >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n }\n if (combo >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n }\n if (combo >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n}\n\n/**\n * Get combo milestone text\n */\nfunction getComboMilestone(\n combo: number\n): { korean: string; english: string } | null {\n if (combo === 5) {\n return { korean: \"훌륭합니다!\", english: \"Great!\" };\n }\n if (combo === 10) {\n return { korean: \"놀라운 연속 공격!\", english: \"Amazing!\" };\n }\n if (combo === 15) {\n return { korean: \"전설적인 공격!\", english: \"Legendary!\" };\n }\n if (combo === 20) {\n return { korean: \"신의 일격!\", english: \"GODLIKE!\" };\n }\n return null;\n}\n\n/**\n * ComboCounter Component\n *\n * Displays the current combo count with animations and milestone indicators.\n * Only visible when combo >= minDisplayCombo (default: 2).\n *\n * @example\n * ```tsx\n * <ComboCounter\n * combo={5}\n * isMobile={isMobile}\n * />\n * ```\n */\nexport const ComboCounter: React.FC<ComboCounterProps> = ({\n combo,\n isMobile = false,\n minDisplayCombo = 2,\n}) => {\n // Animation state - all values that affect render must be in state\n const [scale, setScale] = useState(1);\n const [showMilestone, setShowMilestone] = useState(false);\n const [prevCombo, setPrevCombo] = useState(combo);\n const milestoneTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null\n );\n const scaleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Calculate 3D position - center top of screen\n const position3D: [number, number, number] = useMemo(() => {\n // Position at top center of scene\n return [0, 4.5, 0];\n }, []);\n\n // Handle combo changes via useEffect - this is the proper React pattern\n useEffect(() => {\n if (combo > prevCombo) {\n // Scale up on combo increment - defer to avoid cascading renders\n setTimeout(() => setScale(1.3), 0);\n\n // Clear previous scale timeout\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n scaleTimeoutRef.current = setTimeout(() => {\n setScale(1);\n }, 150);\n\n // Check for milestone\n const milestone = getComboMilestone(combo);\n if (milestone) {\n setTimeout(() => setShowMilestone(true), 0);\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n milestoneTimeoutRef.current = setTimeout(() => {\n setShowMilestone(false);\n }, 1500);\n }\n }\n // Defer prevCombo update to avoid cascading renders\n setTimeout(() => setPrevCombo(combo), 0);\n }, [combo, prevCombo]);\n\n // Cleanup\n useEffect(() => {\n return () => {\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n };\n }, []);\n\n // Don't render if combo is below minimum\n if (combo < minDisplayCombo) {\n return null;\n }\n\n const milestone = getComboMilestone(combo);\n const comboColor = getComboColor(combo);\n const glowColor = getGlowColor(combo);\n // Get background color with alpha for milestone box\n const getMilestoneBackground = (comboVal: number): string => {\n if (comboVal >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.3);\n }\n if (comboVal >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.3);\n }\n if (comboVal >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n }\n if (comboVal >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.3);\n };\n\n // Font sizes\n const mainFontSize = isMobile ? 32 : 48;\n const subFontSize = isMobile ? 16 : 20;\n const milestoneFontSize = isMobile ? 20 : 28;\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"combo-counter\"\n className=\"hud-animated\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n transform: `scale(${scale})`,\n transition: \"transform 0.15s ease-out\",\n animation: combo >= 5 ? \"comboFlash 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {/* Main combo number */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n lineHeight: 1,\n }}\n >\n {combo}\n </div>\n\n {/* Korean text */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}, 2px 2px 4px rgba(0, 0, 0, 0.8)`,\n marginTop: \"4px\",\n }}\n >\n 연속 타격!\n </div>\n\n {/* English text */}\n <div\n style={{\n fontSize: `${subFontSize - 4}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {combo} HIT COMBO!\n </div>\n\n {/* Milestone indicator */}\n {showMilestone && milestone && (\n <div\n className=\"hud-animated\"\n style={{\n marginTop: \"8px\",\n padding: \"4px 16px\",\n background: getMilestoneBackground(combo),\n borderRadius: \"4px\",\n border: `2px solid ${comboColor}`,\n animation: \"comboFlash 1s ease-in-out infinite\",\n }}\n >\n <div\n style={{\n fontSize: `${milestoneFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}`,\n }}\n >\n {milestone.korean}\n </div>\n <div\n style={{\n fontSize: `${milestoneFontSize - 6}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n {milestone.english}\n </div>\n </div>\n )}\n </div>\n\n {/* CSS Animation - kept for backwards compatibility */}\n <style>{`\n @keyframes pulse {\n 0%, 100% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n }\n `}</style>\n </Html>\n );\n};\n\nexport default ComboCounter;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"HealthBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/HealthBar.tsx"],"sourcesContent":["/**\n * HealthBar Component - Segmented health display with Korean theming\n * \n * Displays player health with:\n * - 10 segmented bars\n * - Color transitions: Green (>50%), Yellow (25-50%), Red (<25%)\n * - Pulse animation when health <20%\n * - Korean/English bilingual labels\n * - Numeric value display (e.g., \"85/100\")\n * - Responsive sizing for mobile/tablet/desktop\n * - Smooth transitions and glow effects\n * \n * Performance: Uses React.memo with shallow comparison for 60fps optimization.\n * Note: React.memo uses shallow comparison by default, which works correctly\n * for this component since all props are primitives (number, string, boolean).\n * If object or function props are added in the future, consider adding a\n * custom comparison function or using useCallback/useMemo for prop stability.\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\nexport interface HealthBarProps {\n /** Current health value */\n readonly current: number;\n /** Maximum health capacity */\n readonly max: number;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (percentage: number): number => {\n if (percentage > 50) return KOREAN_COLORS.HEALTH_FULL; // Green\n if (percentage > 25) return KOREAN_COLORS.HEALTH_MEDIUM; // Yellow\n return KOREAN_COLORS.HEALTH_CRITICAL; // Red\n};\n\n/**\n * HealthBar - Segmented health display with Korean theming\n * Performance optimized with React.memo\n */\nexport const HealthBar: React.FC<HealthBarProps> = React.memo(({\n current,\n max,\n playerId,\n isMobile,\n}) => {\n // Calculate health percentage and determine styling\n const healthPercent = useMemo(\n () => Math.max(0, Math.min(100, (current / max) * 100)),\n [current, max]\n );\n\n const segments = 10;\n const filledSegments = Math.ceil((healthPercent / 100) * segments);\n const healthColor = getHealthColor(healthPercent);\n const shouldPulse = healthPercent < 20;\n\n // Responsive sizing with memoization\n const layout = useMemo(() => ({\n barWidth: isMobile ? 180 : 250,\n barHeight: isMobile ? 16 : 20,\n fontSize: isMobile ? 11 : 13,\n padding: isMobile ? \"8px 12px\" : \"12px 16px\",\n }), [isMobile]);\n\n return (\n <div\n data-testid={`health-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"체력 | Health\"\n aria-valuenow={Math.ceil(current)}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuetext={`${Math.ceil(current)} out of ${max}`}\n className=\"hud-animated\"\n style={{\n width: `${layout.barWidth}px`,\n padding: layout.padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n transition: \"box-shadow 0.3s ease-in-out, border-color 0.3s ease-in-out\",\n }}\n >\n {/* Label and numeric display */}\n <div\n style={{\n fontSize: `${layout.fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n transition: \"color 0.2s ease-in-out\",\n }}\n >\n <span>체력 | Health</span>\n <span data-testid={`health-value-${playerId}`}>\n {Math.ceil(current)}/{max}\n </span>\n </div>\n\n {/* Segmented health bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${layout.barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`health-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(healthColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.2s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n </div>\n );\n});\n\nHealthBar.displayName = \"HealthBar\";\n\nexport default HealthBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,IAAM,kBAAkB,eAA+B;AACrD,KAAI,aAAa,GAAI,QAAO,cAAc;AAC1C,KAAI,aAAa,GAAI,QAAO,cAAc;AAC1C,QAAO,cAAc;;;;;;AAOvB,IAAa,YAAsC,MAAM,MAAM,EAC7D,SACA,KACA,UACA,eACI;CAEJ,MAAM,gBAAgB,cACd,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,IAAI,CAAC,EACvD,CAAC,SAAS,IAAI,CACf;CAED,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,gBAAgB,MAAO,SAAS;CAClE,MAAM,cAAc,eAAe,cAAc;CACjD,MAAM,cAAc,gBAAgB;CAGpC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,aAAa;EAClC,GAAG,CAAC,SAAS,CAAC;AAEf,QACE,qBAAC,OAAD;EACE,eAAa,cAAc;EAC3B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,QAAQ;EACjC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,QAAQ,CAAC,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,EAAE;GACrE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAE;GACnE,WAAW,YAAY,gBAAgB,cAAc,cAAc,GAAI;GACvE,YAAY;GACb;YAjBH,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,cAAc,EAAE;IACrD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACb;aAVH,CAYE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,EACxB,qBAAC,QAAD;IAAM,eAAa,gBAAgB;cAAnC;KACG,KAAK,KAAK,QAAQ;KAAC;KAAE;KACjB;MACH;MAGN,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,0CAA0C;IACpE;aAEA,MAAM,KAAK,EAAE,QAAQ,UAAU,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,kBAAkB,SAAS,GAAG;IAC3C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,aAAa,EAAE,GAC/B,gBAAgB,cAAc,sBAAsB,EAAE;KAC5D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,aAAa,GAAI,KAC5C;KACP;IACD,EAfK,MAeL,CACF;GACE,CAAA,CACF;;EAER;AAEF,UAAU,cAAc"}
1
+ {"version":3,"file":"HealthBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/HealthBar.tsx"],"sourcesContent":["/**\n * HealthBar Component - Segmented health display with Korean theming\n * \n * Displays player health with:\n * - 10 segmented bars\n * - Color transitions: Green (>50%), Yellow (25-50%), Red (<25%)\n * - Pulse animation when health <20%\n * - Korean/English bilingual labels\n * - Numeric value display (e.g., \"85/100\")\n * - Responsive sizing for mobile/tablet/desktop\n * - Smooth transitions and glow effects\n * \n * Performance: Uses React.memo with shallow comparison for 60fps optimization.\n * Note: React.memo uses shallow comparison by default, which works correctly\n * for this component since all props are primitives (number, string, boolean).\n * If object or function props are added in the future, consider adding a\n * custom comparison function or using useCallback/useMemo for prop stability.\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\nexport interface HealthBarProps {\n /** Current health value */\n readonly current: number;\n /** Maximum health capacity */\n readonly max: number;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (percentage: number): number => {\n if (percentage > 50) return KOREAN_COLORS.HEALTH_FULL; // Green\n if (percentage > 25) return KOREAN_COLORS.HEALTH_MEDIUM; // Yellow\n return KOREAN_COLORS.HEALTH_CRITICAL; // Red\n};\n\n/**\n * HealthBar - Segmented health display with Korean theming\n * Performance optimized with React.memo\n */\nexport const HealthBar: React.FC<HealthBarProps> = React.memo(({\n current,\n max,\n playerId,\n isMobile,\n}) => {\n // Calculate health percentage and determine styling\n const healthPercent = useMemo(\n () => Math.max(0, Math.min(100, (current / max) * 100)),\n [current, max]\n );\n\n const segments = 10;\n const filledSegments = Math.ceil((healthPercent / 100) * segments);\n const healthColor = getHealthColor(healthPercent);\n const shouldPulse = healthPercent < 20;\n\n // Responsive sizing with memoization\n const layout = useMemo(() => ({\n barWidth: isMobile ? 180 : 250,\n barHeight: isMobile ? 16 : 20,\n fontSize: isMobile ? 11 : 13,\n padding: isMobile ? \"8px 12px\" : \"12px 16px\",\n }), [isMobile]);\n\n return (\n <div\n data-testid={`health-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"체력 | Health\"\n aria-valuenow={Math.ceil(current)}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuetext={`${Math.ceil(current)} out of ${max}`}\n className=\"hud-animated\"\n style={{\n width: `${layout.barWidth}px`,\n padding: layout.padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n transition: \"box-shadow 0.3s ease-in-out, border-color 0.3s ease-in-out\",\n }}\n >\n {/* Label and numeric display */}\n <div\n style={{\n fontSize: `${layout.fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n transition: \"color 0.2s ease-in-out\",\n }}\n >\n <span>체력 | Health</span>\n <span data-testid={`health-value-${playerId}`}>\n {Math.ceil(current)}/{max}\n </span>\n </div>\n\n {/* Segmented health bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${layout.barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`health-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(healthColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.2s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n </div>\n );\n});\n\nHealthBar.displayName = \"HealthBar\";\n\nexport default HealthBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,IAAM,kBAAkB,eAA+B;CACrD,IAAI,aAAa,IAAI,OAAO,cAAc;CAC1C,IAAI,aAAa,IAAI,OAAO,cAAc;CAC1C,OAAO,cAAc;;;;;;AAOvB,IAAa,YAAsC,MAAM,MAAM,EAC7D,SACA,KACA,UACA,eACI;CAEJ,MAAM,gBAAgB,cACd,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,IAAI,CAAC,EACvD,CAAC,SAAS,IAAI,CACf;CAED,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,gBAAgB,MAAO,SAAS;CAClE,MAAM,cAAc,eAAe,cAAc;CACjD,MAAM,cAAc,gBAAgB;CAGpC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,aAAa;EAClC,GAAG,CAAC,SAAS,CAAC;CAEf,OACE,qBAAC,OAAD;EACE,eAAa,cAAc;EAC3B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,QAAQ;EACjC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,QAAQ,CAAC,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,EAAE;GACrE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAE;GACnE,WAAW,YAAY,gBAAgB,cAAc,cAAc,GAAI;GACvE,YAAY;GACb;YAjBH,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,cAAc,EAAE;IACrD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACb;aAVH,CAYE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,EACxB,qBAAC,QAAD;IAAM,eAAa,gBAAgB;cAAnC;KACG,KAAK,KAAK,QAAQ;KAAC;KAAE;KACjB;MACH;MAGN,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,0CAA0C;IACpE;aAEA,MAAM,KAAK,EAAE,QAAQ,UAAU,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,kBAAkB,SAAS,GAAG;IAC3C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,aAAa,EAAE,GAC/B,gBAAgB,cAAc,sBAAsB,EAAE;KAC5D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,aAAa,GAAI,KAC5C;KACP;IACD,EAfK,MAeL,CACF;GACE,CAAA,CACF;;EAER;AAEF,UAAU,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"KoreanButton.js","names":[],"sources":["../../../../../src/components/shared/three/ui/KoreanButton.tsx"],"sourcesContent":["/**\n * KoreanButton - Three.js-compatible button component with Korean theming\n * \n * Provides bilingual button with cyberpunk Korean aesthetic\n * Supports both HTML overlay and 3D mesh rendering\n * \n * Now refactored to use BaseButton for consistent styling\n * \n * @module components/three\n */\n\nimport React from \"react\";\nimport { BaseButton, type BaseButtonProps } from \"../../base\";\n\n/**\n * Props for KoreanButton component\n * Extends BaseButtonProps for consistency\n */\nexport interface KoreanButtonProps extends Omit<BaseButtonProps, \"isMobile\"> {\n // All props inherited from BaseButton\n}\n\n/**\n * KoreanButton Component\n * \n * A bilingual button component with Korean cyberpunk theming.\n * Now uses BaseButton internally for consistent styling and reduced duplication.\n * \n * @example\n * ```tsx\n * <KoreanButton\n * korean=\"공격\"\n * english=\"Attack\"\n * onClick={() => console.log(\"Attack clicked\")}\n * variant=\"primary\"\n * size=\"md\"\n * />\n * ```\n */\nexport const KoreanButton: React.FC<KoreanButtonProps> = ({ testId, ...rest }) => {\n // Simply delegate to BaseButton - all logic is now centralized\n // Default testId to \"korean-button\" for backward compatibility\n return <BaseButton testId={testId ?? \"korean-button\"} {...rest} />;\n};\n\nKoreanButton.displayName = \"KoreanButton\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,gBAA6C,EAAE,QAAQ,GAAG,WAAW;AAGhF,QAAO,oBAAC,YAAD;EAAY,QAAQ,UAAU;EAAiB,GAAI;EAAQ,CAAA;;AAGpE,aAAa,cAAc"}
1
+ {"version":3,"file":"KoreanButton.js","names":[],"sources":["../../../../../src/components/shared/three/ui/KoreanButton.tsx"],"sourcesContent":["/**\n * KoreanButton - Three.js-compatible button component with Korean theming\n * \n * Provides bilingual button with cyberpunk Korean aesthetic\n * Supports both HTML overlay and 3D mesh rendering\n * \n * Now refactored to use BaseButton for consistent styling\n * \n * @module components/three\n */\n\nimport React from \"react\";\nimport { BaseButton, type BaseButtonProps } from \"../../base\";\n\n/**\n * Props for KoreanButton component\n * Extends BaseButtonProps for consistency\n */\nexport interface KoreanButtonProps extends Omit<BaseButtonProps, \"isMobile\"> {\n // All props inherited from BaseButton\n}\n\n/**\n * KoreanButton Component\n * \n * A bilingual button component with Korean cyberpunk theming.\n * Now uses BaseButton internally for consistent styling and reduced duplication.\n * \n * @example\n * ```tsx\n * <KoreanButton\n * korean=\"공격\"\n * english=\"Attack\"\n * onClick={() => console.log(\"Attack clicked\")}\n * variant=\"primary\"\n * size=\"md\"\n * />\n * ```\n */\nexport const KoreanButton: React.FC<KoreanButtonProps> = ({ testId, ...rest }) => {\n // Simply delegate to BaseButton - all logic is now centralized\n // Default testId to \"korean-button\" for backward compatibility\n return <BaseButton testId={testId ?? \"korean-button\"} {...rest} />;\n};\n\nKoreanButton.displayName = \"KoreanButton\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,gBAA6C,EAAE,QAAQ,GAAG,WAAW;CAGhF,OAAO,oBAAC,YAAD;EAAY,QAAQ,UAAU;EAAiB,GAAI;EAAQ,CAAA;;AAGpE,aAAa,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"KoreanPanel.js","names":[],"sources":["../../../../../src/components/shared/three/ui/KoreanPanel.tsx"],"sourcesContent":["/**\n * KoreanPanel - Three.js-compatible panel container component\n * \n * Provides a styled container with Korean cyberpunk theming\n * \n * Now refactored to use BasePanel for consistent styling\n * \n * @module components/three\n */\n\nimport React from \"react\";\nimport { BasePanel, type BasePanelProps } from \"../../base\";\n\n/**\n * Props for KoreanPanel component\n * Extends BasePanelProps for consistency\n */\nexport interface KoreanPanelProps extends Omit<BasePanelProps, \"isMobile\"> {\n // All props inherited from BasePanel\n}\n\n/**\n * KoreanPanel Component\n * \n * A container component with Korean cyberpunk styling.\n * Now uses BasePanel internally for consistent styling and reduced duplication.\n * \n * @example\n * ```tsx\n * <KoreanPanel variant=\"bordered\" padding={20}>\n * <h1>Panel Content</h1>\n * </KoreanPanel>\n * ```\n */\nexport const KoreanPanel: React.FC<KoreanPanelProps> = ({ testId, ...rest }) => {\n // Simply delegate to BasePanel - all logic is now centralized\n // Default testId to \"korean-panel\" for backward compatibility\n return <BasePanel testId={testId ?? \"korean-panel\"} {...rest} />;\n};\n\nKoreanPanel.displayName = \"KoreanPanel\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAkCA,IAAa,eAA2C,EAAE,QAAQ,GAAG,WAAW;AAG9E,QAAO,oBAAC,WAAD;EAAW,QAAQ,UAAU;EAAgB,GAAI;EAAQ,CAAA;;AAGlE,YAAY,cAAc"}
1
+ {"version":3,"file":"KoreanPanel.js","names":[],"sources":["../../../../../src/components/shared/three/ui/KoreanPanel.tsx"],"sourcesContent":["/**\n * KoreanPanel - Three.js-compatible panel container component\n * \n * Provides a styled container with Korean cyberpunk theming\n * \n * Now refactored to use BasePanel for consistent styling\n * \n * @module components/three\n */\n\nimport React from \"react\";\nimport { BasePanel, type BasePanelProps } from \"../../base\";\n\n/**\n * Props for KoreanPanel component\n * Extends BasePanelProps for consistency\n */\nexport interface KoreanPanelProps extends Omit<BasePanelProps, \"isMobile\"> {\n // All props inherited from BasePanel\n}\n\n/**\n * KoreanPanel Component\n * \n * A container component with Korean cyberpunk styling.\n * Now uses BasePanel internally for consistent styling and reduced duplication.\n * \n * @example\n * ```tsx\n * <KoreanPanel variant=\"bordered\" padding={20}>\n * <h1>Panel Content</h1>\n * </KoreanPanel>\n * ```\n */\nexport const KoreanPanel: React.FC<KoreanPanelProps> = ({ testId, ...rest }) => {\n // Simply delegate to BasePanel - all logic is now centralized\n // Default testId to \"korean-panel\" for backward compatibility\n return <BasePanel testId={testId ?? \"korean-panel\"} {...rest} />;\n};\n\nKoreanPanel.displayName = \"KoreanPanel\";\n"],"mappings":";;;;;;;;;;;;;;;;;AAkCA,IAAa,eAA2C,EAAE,QAAQ,GAAG,WAAW;CAG9E,OAAO,oBAAC,WAAD;EAAW,QAAQ,UAAU;EAAgB,GAAI;EAAQ,CAAA;;AAGlE,YAAY,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"KoreanText.js","names":[],"sources":["../../../../../src/components/shared/three/ui/KoreanText.tsx"],"sourcesContent":["/**\n * KoreanText - Three.js-compatible text component with bilingual support\n * \n * Displays Korean and English text with cyberpunk styling\n * \n * Now refactored to use BaseText for consistent styling\n * \n * @module components/three\n */\n\nimport React from \"react\";\nimport { BaseText, type BaseTextProps } from \"../../base\";\n\n/**\n * Props for KoreanText component\n * Extends BaseTextProps for consistency\n */\nexport interface KoreanTextProps extends Omit<BaseTextProps, \"isMobile\"> {\n // All props inherited from BaseText\n}\n\n/**\n * KoreanText Component\n * \n * A bilingual text component with Korean cyberpunk styling.\n * Now uses BaseText internally for consistent styling and reduced duplication.\n * \n * @example\n * ```tsx\n * <KoreanText\n * korean=\"공격\"\n * english=\"Attack\"\n * size=\"large\"\n * layout=\"vertical\"\n * />\n * ```\n */\nexport const KoreanText: React.FC<KoreanTextProps> = ({ testId, ...rest }) => {\n // Simply delegate to BaseText - all logic is now centralized\n // Default testId to \"korean-text\" for backward compatibility\n return <BaseText testId={testId ?? \"korean-text\"} {...rest} />;\n};\n\nKoreanText.displayName = \"KoreanText\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqCA,IAAa,cAAyC,EAAE,QAAQ,GAAG,WAAW;AAG5E,QAAO,oBAAC,UAAD;EAAU,QAAQ,UAAU;EAAe,GAAI;EAAQ,CAAA;;AAGhE,WAAW,cAAc"}
1
+ {"version":3,"file":"KoreanText.js","names":[],"sources":["../../../../../src/components/shared/three/ui/KoreanText.tsx"],"sourcesContent":["/**\n * KoreanText - Three.js-compatible text component with bilingual support\n * \n * Displays Korean and English text with cyberpunk styling\n * \n * Now refactored to use BaseText for consistent styling\n * \n * @module components/three\n */\n\nimport React from \"react\";\nimport { BaseText, type BaseTextProps } from \"../../base\";\n\n/**\n * Props for KoreanText component\n * Extends BaseTextProps for consistency\n */\nexport interface KoreanTextProps extends Omit<BaseTextProps, \"isMobile\"> {\n // All props inherited from BaseText\n}\n\n/**\n * KoreanText Component\n * \n * A bilingual text component with Korean cyberpunk styling.\n * Now uses BaseText internally for consistent styling and reduced duplication.\n * \n * @example\n * ```tsx\n * <KoreanText\n * korean=\"공격\"\n * english=\"Attack\"\n * size=\"large\"\n * layout=\"vertical\"\n * />\n * ```\n */\nexport const KoreanText: React.FC<KoreanTextProps> = ({ testId, ...rest }) => {\n // Simply delegate to BaseText - all logic is now centralized\n // Default testId to \"korean-text\" for backward compatibility\n return <BaseText testId={testId ?? \"korean-text\"} {...rest} />;\n};\n\nKoreanText.displayName = \"KoreanText\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqCA,IAAa,cAAyC,EAAE,QAAQ,GAAG,WAAW;CAG5E,OAAO,oBAAC,UAAD;EAAU,QAAQ,UAAU;EAAe,GAAI;EAAQ,CAAA;;AAGhE,WAAW,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"MenuList.js","names":[],"sources":["../../../../../src/components/shared/three/ui/MenuList.tsx"],"sourcesContent":["/**\n * MenuList - Three.js-compatible menu list component\n * \n * Navigation menu with Korean theming and keyboard support\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Menu item interface\n */\nexport interface MenuItem {\n readonly id: string;\n readonly korean: string;\n readonly english: string;\n readonly disabled?: boolean;\n}\n\n/**\n * Props for MenuList component\n */\nexport interface MenuListProps {\n readonly items: readonly MenuItem[];\n readonly onSelect: (id: string) => void;\n readonly selectedId?: string;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly testId?: string;\n}\n\n/**\n * MenuList Component\n * \n * A navigational menu component with Korean cyberpunk styling.\n * Supports keyboard navigation and hover states.\n * \n * @example\n * ```tsx\n * <MenuList\n * items={[\n * { id: \"combat\", korean: \"대전\", english: \"Combat\" },\n * { id: \"training\", korean: \"훈련\", english: \"Training\" }\n * ]}\n * onSelect={(id) => console.log(id)}\n * />\n * ```\n */\nexport const MenuList: React.FC<MenuListProps> = ({\n items,\n onSelect,\n selectedId,\n position = [0, 0, 0],\n width = 300,\n testId,\n}) => {\n const [hoveredId, setHoveredId] = useState<string | null>(null);\n\n const handleItemClick = useCallback(\n (id: string, disabled?: boolean) => {\n if (!disabled) {\n onSelect(id);\n }\n },\n [onSelect]\n );\n\n const handleMouseEnter = useCallback((id: string) => {\n setHoveredId(id);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setHoveredId(null);\n }, []);\n\n // Memoize container styles for performance\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"8px\",\n padding: \"12px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"8px\",\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [width]\n );\n\n // Function to get item styles based on state\n const getItemStyle = useCallback(\n (id: string, disabled?: boolean): React.CSSProperties => {\n const isSelected = selectedId === id;\n const isHovered = hoveredId === id;\n\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.5);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n } else if (isHovered && !disabled) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6);\n }\n\n return {\n padding: \"12px 16px\",\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"4px\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n transition: \"all 0.2s ease\",\n opacity: disabled ? 0.4 : 1,\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n transform: isHovered && !disabled ? \"translateX(4px)\" : \"translateX(0)\",\n };\n },\n [selectedId, hoveredId]\n );\n\n // Memoize text styles for performance\n const getTextStyle = useCallback(\n (isKorean: boolean, isSelected: boolean): React.CSSProperties => ({\n display: \"block\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isKorean ? \"16px\" : \"14px\",\n fontWeight: isKorean ? \"bold\" : \"normal\",\n fontStyle: isKorean ? \"normal\" : \"italic\",\n color: hexToRgbaString(\n isSelected ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.TEXT_PRIMARY\n ),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div style={containerStyle} data-testid={testId ?? \"menu-list\"}>\n {items.map((item) => {\n const isSelected = selectedId === item.id;\n return (\n <div\n key={item.id}\n onClick={() => handleItemClick(item.id, item.disabled)}\n onMouseEnter={() => handleMouseEnter(item.id)}\n onMouseLeave={handleMouseLeave}\n style={getItemStyle(item.id, item.disabled)}\n data-testid={`menu-item-${item.id}`}\n >\n <span style={getTextStyle(true, isSelected)}>\n {item.korean}\n </span>\n <span style={getTextStyle(false, isSelected)}>\n {item.english}\n </span>\n </div>\n );\n })}\n </div>\n </Html>\n );\n};\n\nMenuList.displayName = \"MenuList\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,IAAa,YAAqC,EAChD,OACA,UACA,YACA,WAAW;CAAC;CAAG;CAAG;CAAE,EACpB,QAAQ,KACR,aACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAwB,KAAK;CAE/D,MAAM,kBAAkB,aACrB,IAAY,aAAuB;AAClC,MAAI,CAAC,SACH,UAAS,GAAG;IAGhB,CAAC,SAAS,CACX;CAED,MAAM,mBAAmB,aAAa,OAAe;AACnD,eAAa,GAAG;IACf,EAAE,CAAC;CAEN,MAAM,mBAAmB,kBAAkB;AACzC,eAAa,KAAK;IACjB,EAAE,CAAC;CAGN,MAAM,iBAAiB,eACd;EACL,OAAO,GAAG,MAAM;EAChB,SAAS;EACT,eAAe;EACf,KAAK;EACL,SAAS;EACT,YAAY,gBAAgB,cAAc,oBAAoB,GAAI;EAClE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,GAAI;EACrE,cAAc;EACd,WAAW,YAAY,gBAAgB,cAAc,cAAc,GAAI;EACxE,GACD,CAAC,MAAM,CACR;CAGD,MAAM,eAAe,aAClB,IAAY,aAA4C;EACvD,MAAM,aAAa,eAAe;EAClC,MAAM,YAAY,cAAc;EAEhC,IAAI,aAAa,gBAAgB,cAAc,sBAAsB,GAAI;EACzE,IAAI,cAAc,gBAAgB,cAAc,aAAa,GAAI;AAEjE,MAAI,YAAY;AACd,gBAAa,gBAAgB,cAAc,cAAc,GAAI;AAC7D,iBAAc,gBAAgB,cAAc,cAAc,GAAI;aACrD,aAAa,CAAC,UAAU;AACjC,gBAAa,gBAAgB,cAAc,aAAa,IAAK;AAC7D,iBAAc,gBAAgB,cAAc,aAAa,GAAI;;AAG/D,SAAO;GACL,SAAS;GACT;GACA,QAAQ,aAAa;GACrB,cAAc;GACd,QAAQ,WAAW,gBAAgB;GACnC,YAAY;GACZ,SAAS,WAAW,KAAM;GAC1B,YAAY;GACZ,kBAAkB;GAClB,WAAW,aAAa,CAAC,WAAW,oBAAoB;GACzD;IAEH,CAAC,YAAY,UAAU,CACxB;CAGD,MAAM,eAAe,aAClB,UAAmB,gBAA8C;EAChE,SAAS;EACT,YAAY,YAAY;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY,WAAW,SAAS;EAChC,WAAW,WAAW,WAAW;EACjC,OAAO,gBACL,aAAa,cAAc,cAAc,cAAc,aACxD;EACD,YAAY,aAAa,gBAAgB,cAAc,aAAa,GAAI;EACzE,GACD,EAAE,CACH;AAED,QACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;YACxB,oBAAC,OAAD;GAAK,OAAO;GAAgB,eAAa,UAAU;aAChD,MAAM,KAAK,SAAS;IACnB,MAAM,aAAa,eAAe,KAAK;AACvC,WACE,qBAAC,OAAD;KAEE,eAAe,gBAAgB,KAAK,IAAI,KAAK,SAAS;KACtD,oBAAoB,iBAAiB,KAAK,GAAG;KAC7C,cAAc;KACd,OAAO,aAAa,KAAK,IAAI,KAAK,SAAS;KAC3C,eAAa,aAAa,KAAK;eANjC,CAQE,oBAAC,QAAD;MAAM,OAAO,aAAa,MAAM,WAAW;gBACxC,KAAK;MACD,CAAA,EACP,oBAAC,QAAD;MAAM,OAAO,aAAa,OAAO,WAAW;gBACzC,KAAK;MACD,CAAA,CACH;OAbC,KAAK,GAaN;KAER;GACE,CAAA;EACD,CAAA;;AAIX,SAAS,cAAc"}
1
+ {"version":3,"file":"MenuList.js","names":[],"sources":["../../../../../src/components/shared/three/ui/MenuList.tsx"],"sourcesContent":["/**\n * MenuList - Three.js-compatible menu list component\n * \n * Navigation menu with Korean theming and keyboard support\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Menu item interface\n */\nexport interface MenuItem {\n readonly id: string;\n readonly korean: string;\n readonly english: string;\n readonly disabled?: boolean;\n}\n\n/**\n * Props for MenuList component\n */\nexport interface MenuListProps {\n readonly items: readonly MenuItem[];\n readonly onSelect: (id: string) => void;\n readonly selectedId?: string;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly testId?: string;\n}\n\n/**\n * MenuList Component\n * \n * A navigational menu component with Korean cyberpunk styling.\n * Supports keyboard navigation and hover states.\n * \n * @example\n * ```tsx\n * <MenuList\n * items={[\n * { id: \"combat\", korean: \"대전\", english: \"Combat\" },\n * { id: \"training\", korean: \"훈련\", english: \"Training\" }\n * ]}\n * onSelect={(id) => console.log(id)}\n * />\n * ```\n */\nexport const MenuList: React.FC<MenuListProps> = ({\n items,\n onSelect,\n selectedId,\n position = [0, 0, 0],\n width = 300,\n testId,\n}) => {\n const [hoveredId, setHoveredId] = useState<string | null>(null);\n\n const handleItemClick = useCallback(\n (id: string, disabled?: boolean) => {\n if (!disabled) {\n onSelect(id);\n }\n },\n [onSelect]\n );\n\n const handleMouseEnter = useCallback((id: string) => {\n setHoveredId(id);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setHoveredId(null);\n }, []);\n\n // Memoize container styles for performance\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"8px\",\n padding: \"12px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"8px\",\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [width]\n );\n\n // Function to get item styles based on state\n const getItemStyle = useCallback(\n (id: string, disabled?: boolean): React.CSSProperties => {\n const isSelected = selectedId === id;\n const isHovered = hoveredId === id;\n\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.5);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n } else if (isHovered && !disabled) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6);\n }\n\n return {\n padding: \"12px 16px\",\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"4px\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n transition: \"all 0.2s ease\",\n opacity: disabled ? 0.4 : 1,\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n transform: isHovered && !disabled ? \"translateX(4px)\" : \"translateX(0)\",\n };\n },\n [selectedId, hoveredId]\n );\n\n // Memoize text styles for performance\n const getTextStyle = useCallback(\n (isKorean: boolean, isSelected: boolean): React.CSSProperties => ({\n display: \"block\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isKorean ? \"16px\" : \"14px\",\n fontWeight: isKorean ? \"bold\" : \"normal\",\n fontStyle: isKorean ? \"normal\" : \"italic\",\n color: hexToRgbaString(\n isSelected ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.TEXT_PRIMARY\n ),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div style={containerStyle} data-testid={testId ?? \"menu-list\"}>\n {items.map((item) => {\n const isSelected = selectedId === item.id;\n return (\n <div\n key={item.id}\n onClick={() => handleItemClick(item.id, item.disabled)}\n onMouseEnter={() => handleMouseEnter(item.id)}\n onMouseLeave={handleMouseLeave}\n style={getItemStyle(item.id, item.disabled)}\n data-testid={`menu-item-${item.id}`}\n >\n <span style={getTextStyle(true, isSelected)}>\n {item.korean}\n </span>\n <span style={getTextStyle(false, isSelected)}>\n {item.english}\n </span>\n </div>\n );\n })}\n </div>\n </Html>\n );\n};\n\nMenuList.displayName = \"MenuList\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,IAAa,YAAqC,EAChD,OACA,UACA,YACA,WAAW;CAAC;CAAG;CAAG;CAAE,EACpB,QAAQ,KACR,aACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAwB,KAAK;CAE/D,MAAM,kBAAkB,aACrB,IAAY,aAAuB;EAClC,IAAI,CAAC,UACH,SAAS,GAAG;IAGhB,CAAC,SAAS,CACX;CAED,MAAM,mBAAmB,aAAa,OAAe;EACnD,aAAa,GAAG;IACf,EAAE,CAAC;CAEN,MAAM,mBAAmB,kBAAkB;EACzC,aAAa,KAAK;IACjB,EAAE,CAAC;CAGN,MAAM,iBAAiB,eACd;EACL,OAAO,GAAG,MAAM;EAChB,SAAS;EACT,eAAe;EACf,KAAK;EACL,SAAS;EACT,YAAY,gBAAgB,cAAc,oBAAoB,GAAI;EAClE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,GAAI;EACrE,cAAc;EACd,WAAW,YAAY,gBAAgB,cAAc,cAAc,GAAI;EACxE,GACD,CAAC,MAAM,CACR;CAGD,MAAM,eAAe,aAClB,IAAY,aAA4C;EACvD,MAAM,aAAa,eAAe;EAClC,MAAM,YAAY,cAAc;EAEhC,IAAI,aAAa,gBAAgB,cAAc,sBAAsB,GAAI;EACzE,IAAI,cAAc,gBAAgB,cAAc,aAAa,GAAI;EAEjE,IAAI,YAAY;GACd,aAAa,gBAAgB,cAAc,cAAc,GAAI;GAC7D,cAAc,gBAAgB,cAAc,cAAc,GAAI;SACzD,IAAI,aAAa,CAAC,UAAU;GACjC,aAAa,gBAAgB,cAAc,aAAa,IAAK;GAC7D,cAAc,gBAAgB,cAAc,aAAa,GAAI;;EAG/D,OAAO;GACL,SAAS;GACT;GACA,QAAQ,aAAa;GACrB,cAAc;GACd,QAAQ,WAAW,gBAAgB;GACnC,YAAY;GACZ,SAAS,WAAW,KAAM;GAC1B,YAAY;GACZ,kBAAkB;GAClB,WAAW,aAAa,CAAC,WAAW,oBAAoB;GACzD;IAEH,CAAC,YAAY,UAAU,CACxB;CAGD,MAAM,eAAe,aAClB,UAAmB,gBAA8C;EAChE,SAAS;EACT,YAAY,YAAY;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY,WAAW,SAAS;EAChC,WAAW,WAAW,WAAW;EACjC,OAAO,gBACL,aAAa,cAAc,cAAc,cAAc,aACxD;EACD,YAAY,aAAa,gBAAgB,cAAc,aAAa,GAAI;EACzE,GACD,EAAE,CACH;CAED,OACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;YACxB,oBAAC,OAAD;GAAK,OAAO;GAAgB,eAAa,UAAU;aAChD,MAAM,KAAK,SAAS;IACnB,MAAM,aAAa,eAAe,KAAK;IACvC,OACE,qBAAC,OAAD;KAEE,eAAe,gBAAgB,KAAK,IAAI,KAAK,SAAS;KACtD,oBAAoB,iBAAiB,KAAK,GAAG;KAC7C,cAAc;KACd,OAAO,aAAa,KAAK,IAAI,KAAK,SAAS;KAC3C,eAAa,aAAa,KAAK;eANjC,CAQE,oBAAC,QAAD;MAAM,OAAO,aAAa,MAAM,WAAW;gBACxC,KAAK;MACD,CAAA,EACP,oBAAC,QAAD;MAAM,OAAO,aAAa,OAAO,WAAW;gBACzC,KAAK;MACD,CAAA,CACH;OAbC,KAAK,GAaN;KAER;GACE,CAAA;EACD,CAAA;;AAIX,SAAS,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"PlayerHUD.js","names":[],"sources":["../../../../../src/components/shared/three/ui/PlayerHUD.tsx"],"sourcesContent":["/**\n * PlayerHUD Component - Combined combat readiness, health and stamina display\n *\n * Displays a complete player HUD with:\n * - Archetype icon/image\n * - Player name (Korean/English)\n * - Combat Readiness bar (10-segment, multi-factor)\n * - Health bar (segmented, color-coded)\n * - Stamina bar (segmented, cyan-themed)\n * - Current stance indicator\n * - Responsive positioning (top-left for player 1, top-right for player 2)\n *\n * Performance optimized with React.memo for 60fps rendering.\n */\n\nimport React, { useCallback, useMemo } from \"react\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport type { StanceLaterality } from \"../../../../systems/trigram/types\";\nimport {\n ARCHETYPE_ASSETS,\n FALLBACK_ARCHETYPE_IMAGE,\n FONT_FAMILY,\n KOREAN_COLORS,\n} from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { BreathingIndicator } from \"./BreathingIndicator\";\nimport { CombatReadinessBar } from \"./CombatReadinessBar\";\nimport { HealthBar } from \"./HealthBar\";\nimport { StaminaBar } from \"./StaminaBar\";\n\nexport interface PlayerHUDProps {\n /** Player state with health, stamina, and other data */\n readonly player: PlayerState;\n /** Player position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n /** Stance laterality: left or right foot forward */\n readonly laterality?: StanceLaterality;\n}\n\n/**\n * Laterality Indicator Component - Shows L/R badge with Korean text\n * @korean 측면성표시기\n */\nconst LateralityIndicator: React.FC<{\n readonly laterality: StanceLaterality;\n readonly isMobile: boolean;\n}> = React.memo(({ laterality, isMobile }) => {\n const isLeft = laterality === \"left\";\n\n const badgeStyle = useMemo(\n () => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: isMobile ? \"3px\" : \"4px\",\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n }),\n [isMobile],\n );\n\n const labelStyle = useMemo(\n () => ({\n padding: isMobile ? \"1px 4px\" : \"2px 6px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"3px\",\n fontWeight: \"bold\",\n minWidth: isMobile ? \"16px\" : \"18px\",\n textAlign: \"center\" as const,\n }),\n [isMobile],\n );\n\n const textStyle = useMemo(\n () => ({\n opacity: 0.8,\n whiteSpace: \"nowrap\" as const,\n }),\n [],\n );\n\n return (\n <div style={badgeStyle} data-testid=\"laterality-indicator\">\n <span style={labelStyle} data-testid=\"laterality-badge\">\n {isLeft ? \"L\" : \"R\"}\n </span>\n <span style={textStyle} data-testid=\"laterality-text\">\n {isLeft ? \"왼발서기\" : \"오른발서기\"}\n </span>\n </div>\n );\n});\nLateralityIndicator.displayName = \"LateralityIndicator\";\n\n/**\n * PlayerHUD - Complete player status display with health and stamina bars\n * Performance optimized with React.memo\n */\nconst PlayerHUDComponent: React.FC<PlayerHUDProps> = ({\n player,\n position,\n isMobile,\n laterality,\n}) => {\n const playerId = player.id;\n const isLeft = position === \"left\";\n\n // Memoize responsive sizing to avoid recalculation\n const layout = useMemo(\n () => ({\n fontSize: isMobile ? 11 : 13,\n gap: isMobile ? \"6px\" : \"8px\",\n iconSize: isMobile ? 40 : 50,\n top: isMobile ? \"8px\" : \"10px\",\n horizontal: isMobile ? \"8px\" : \"12px\",\n }),\n [isMobile],\n );\n\n // Get archetype image path (memoized)\n const archetypeImagePath = useMemo(() => {\n const archetypeKey = player.archetype.toLowerCase();\n const assets =\n ARCHETYPE_ASSETS[archetypeKey as keyof typeof ARCHETYPE_ASSETS];\n return assets?.image ?? FALLBACK_ARCHETYPE_IMAGE;\n }, [player.archetype]);\n\n // Memoize style objects to prevent recreating on every render\n // Uses relative positioning for embedding in container HUDs\n const containerStyle = useMemo(\n () => ({\n position: \"relative\" as const,\n display: \"flex\",\n flexDirection: \"column\" as const,\n gap: layout.gap,\n pointerEvents: \"none\" as const,\n width: \"100%\",\n }),\n [layout],\n );\n\n const iconContainerStyle = useMemo(() => {\n const direction = isLeft ? \"row\" : \"row-reverse\";\n return {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n flexDirection: direction as \"row\" | \"row-reverse\",\n };\n }, [isLeft]);\n\n const iconStyle = useMemo(\n () => ({\n width: `${layout.iconSize}px`,\n height: `${layout.iconSize}px`,\n borderRadius: \"8px\",\n overflow: \"hidden\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.5)}`,\n flexShrink: 0,\n }),\n [layout.iconSize],\n );\n\n const nameStyle = useMemo(() => {\n const textAlign = isLeft ? \"left\" : \"right\";\n return {\n fontSize: `${layout.fontSize}px`,\n fontWeight: \"bold\" as const,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n textAlign: textAlign as \"left\" | \"right\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8), 0 0 8px rgba(0,0,0,0.6)\",\n padding: \"2px 6px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.7),\n borderRadius: \"4px\",\n whiteSpace: \"nowrap\" as const,\n };\n }, [layout.fontSize, isLeft]);\n\n const stanceStyle = useMemo(() => {\n const textAlign = isLeft ? \"left\" : \"right\";\n return {\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 1),\n textAlign: textAlign as \"left\" | \"right\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n padding: \"4px 8px\",\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n borderRadius: \"4px\",\n marginTop: \"2px\",\n };\n }, [isMobile, isLeft]);\n\n // Memoize error handler to prevent recreating on every render\n const handleImageError = useCallback(\n (e: React.SyntheticEvent<HTMLImageElement>) => {\n const target = e.target as HTMLImageElement;\n if (!target.src.endsWith(FALLBACK_ARCHETYPE_IMAGE)) {\n target.src = FALLBACK_ARCHETYPE_IMAGE;\n }\n },\n [],\n );\n\n return (\n <div data-testid={`player-hud-${playerId}`} style={containerStyle}>\n {/* Player Name with Archetype Icon */}\n <div data-testid={`player-name-${playerId}`} style={iconContainerStyle}>\n {/* Archetype Icon */}\n <div data-testid={`archetype-icon-${playerId}`} style={iconStyle}>\n <img\n src={archetypeImagePath}\n alt={`${player.name.english} archetype`}\n style={{\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n }}\n onError={handleImageError}\n />\n </div>\n {/* Player Name */}\n <div style={nameStyle}>\n {player.name.korean} | {player.name.english}\n </div>\n </div>\n\n {/* Combat Readiness Bar - shows overall combat capability */}\n <CombatReadinessBar\n player={player}\n playerId={playerId}\n isMobile={isMobile}\n />\n\n {/* Health Bar - shows aggregate body health */}\n <HealthBar\n current={player.health}\n max={player.maxHealth}\n playerId={playerId}\n isMobile={isMobile}\n />\n\n {/* Stamina Bar */}\n <StaminaBar\n current={player.stamina}\n max={player.maxStamina}\n playerId={playerId}\n isMobile={isMobile}\n />\n\n {/* Breathing Disruption Indicator */}\n <BreathingIndicator player={player} isMobile={isMobile} />\n\n {/* Laterality Indicator */}\n {laterality && (\n <LateralityIndicator laterality={laterality} isMobile={isMobile} />\n )}\n\n {/* Current Stance Indicator */}\n <div data-testid={`stance-indicator-${playerId}`} style={stanceStyle}>\n 자세 | Stance: {player.currentStance}\n </div>\n </div>\n );\n};\n\n/**\n * Memoized PlayerHUD with custom comparison\n * Only re-renders when relevant props change\n */\nexport const PlayerHUD = React.memo(\n PlayerHUDComponent,\n (prevProps, nextProps) => {\n // Compare player state\n const healthSame = prevProps.player.health === nextProps.player.health;\n const maxHealthSame =\n prevProps.player.maxHealth === nextProps.player.maxHealth;\n const staminaSame = prevProps.player.stamina === nextProps.player.stamina;\n const maxStaminaSame =\n prevProps.player.maxStamina === nextProps.player.maxStamina;\n const archetypeSame =\n prevProps.player.archetype === nextProps.player.archetype;\n const stanceSame =\n prevProps.player.currentStance === nextProps.player.currentStance;\n const idSame = prevProps.player.id === nextProps.player.id;\n const nameSame =\n prevProps.player.name.korean === nextProps.player.name.korean &&\n prevProps.player.name.english === nextProps.player.name.english;\n\n // Compare statusEffects for BreathingIndicator updates\n const statusEffectsSame =\n prevProps.player.statusEffects.length ===\n nextProps.player.statusEffects.length &&\n prevProps.player.statusEffects.every(\n (effect, index) => effect === nextProps.player.statusEffects[index],\n );\n\n // Compare combat readiness factors for CombatReadinessBar updates\n // These properties are used by calculateCombatReadiness()\n const bodyPartHealthSame =\n prevProps.player.bodyPartHealth === nextProps.player.bodyPartHealth;\n const painSame = prevProps.player.pain === nextProps.player.pain;\n const consciousnessSame =\n prevProps.player.consciousness === nextProps.player.consciousness;\n const balanceSame = prevProps.player.balance === nextProps.player.balance;\n\n // Compare other props\n const positionSame = prevProps.position === nextProps.position;\n const mobileSame = prevProps.isMobile === nextProps.isMobile;\n const lateralitySame = prevProps.laterality === nextProps.laterality;\n\n // Return true if all relevant props are the same (skip re-render)\n return (\n healthSame &&\n maxHealthSame &&\n staminaSame &&\n maxStaminaSame &&\n archetypeSame &&\n stanceSame &&\n idSame &&\n nameSame &&\n statusEffectsSame &&\n bodyPartHealthSame &&\n painSame &&\n consciousnessSame &&\n balanceSame &&\n positionSame &&\n mobileSame &&\n lateralitySame\n );\n },\n);\n\nexport default PlayerHUD;\n"],"mappings":"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"}
1
+ {"version":3,"file":"PlayerHUD.js","names":[],"sources":["../../../../../src/components/shared/three/ui/PlayerHUD.tsx"],"sourcesContent":["/**\n * PlayerHUD Component - Combined combat readiness, health and stamina display\n *\n * Displays a complete player HUD with:\n * - Archetype icon/image\n * - Player name (Korean/English)\n * - Combat Readiness bar (10-segment, multi-factor)\n * - Health bar (segmented, color-coded)\n * - Stamina bar (segmented, cyan-themed)\n * - Current stance indicator\n * - Responsive positioning (top-left for player 1, top-right for player 2)\n *\n * Performance optimized with React.memo for 60fps rendering.\n */\n\nimport React, { useCallback, useMemo } from \"react\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport type { StanceLaterality } from \"../../../../systems/trigram/types\";\nimport {\n ARCHETYPE_ASSETS,\n FALLBACK_ARCHETYPE_IMAGE,\n FONT_FAMILY,\n KOREAN_COLORS,\n} from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { BreathingIndicator } from \"./BreathingIndicator\";\nimport { CombatReadinessBar } from \"./CombatReadinessBar\";\nimport { HealthBar } from \"./HealthBar\";\nimport { StaminaBar } from \"./StaminaBar\";\n\nexport interface PlayerHUDProps {\n /** Player state with health, stamina, and other data */\n readonly player: PlayerState;\n /** Player position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n /** Stance laterality: left or right foot forward */\n readonly laterality?: StanceLaterality;\n}\n\n/**\n * Laterality Indicator Component - Shows L/R badge with Korean text\n * @korean 측면성표시기\n */\nconst LateralityIndicator: React.FC<{\n readonly laterality: StanceLaterality;\n readonly isMobile: boolean;\n}> = React.memo(({ laterality, isMobile }) => {\n const isLeft = laterality === \"left\";\n\n const badgeStyle = useMemo(\n () => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: isMobile ? \"3px\" : \"4px\",\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n }),\n [isMobile],\n );\n\n const labelStyle = useMemo(\n () => ({\n padding: isMobile ? \"1px 4px\" : \"2px 6px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"3px\",\n fontWeight: \"bold\",\n minWidth: isMobile ? \"16px\" : \"18px\",\n textAlign: \"center\" as const,\n }),\n [isMobile],\n );\n\n const textStyle = useMemo(\n () => ({\n opacity: 0.8,\n whiteSpace: \"nowrap\" as const,\n }),\n [],\n );\n\n return (\n <div style={badgeStyle} data-testid=\"laterality-indicator\">\n <span style={labelStyle} data-testid=\"laterality-badge\">\n {isLeft ? \"L\" : \"R\"}\n </span>\n <span style={textStyle} data-testid=\"laterality-text\">\n {isLeft ? \"왼발서기\" : \"오른발서기\"}\n </span>\n </div>\n );\n});\nLateralityIndicator.displayName = \"LateralityIndicator\";\n\n/**\n * PlayerHUD - Complete player status display with health and stamina bars\n * Performance optimized with React.memo\n */\nconst PlayerHUDComponent: React.FC<PlayerHUDProps> = ({\n player,\n position,\n isMobile,\n laterality,\n}) => {\n const playerId = player.id;\n const isLeft = position === \"left\";\n\n // Memoize responsive sizing to avoid recalculation\n const layout = useMemo(\n () => ({\n fontSize: isMobile ? 11 : 13,\n gap: isMobile ? \"6px\" : \"8px\",\n iconSize: isMobile ? 40 : 50,\n top: isMobile ? \"8px\" : \"10px\",\n horizontal: isMobile ? \"8px\" : \"12px\",\n }),\n [isMobile],\n );\n\n // Get archetype image path (memoized)\n const archetypeImagePath = useMemo(() => {\n const archetypeKey = player.archetype.toLowerCase();\n const assets =\n ARCHETYPE_ASSETS[archetypeKey as keyof typeof ARCHETYPE_ASSETS];\n return assets?.image ?? FALLBACK_ARCHETYPE_IMAGE;\n }, [player.archetype]);\n\n // Memoize style objects to prevent recreating on every render\n // Uses relative positioning for embedding in container HUDs\n const containerStyle = useMemo(\n () => ({\n position: \"relative\" as const,\n display: \"flex\",\n flexDirection: \"column\" as const,\n gap: layout.gap,\n pointerEvents: \"none\" as const,\n width: \"100%\",\n }),\n [layout],\n );\n\n const iconContainerStyle = useMemo(() => {\n const direction = isLeft ? \"row\" : \"row-reverse\";\n return {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n flexDirection: direction as \"row\" | \"row-reverse\",\n };\n }, [isLeft]);\n\n const iconStyle = useMemo(\n () => ({\n width: `${layout.iconSize}px`,\n height: `${layout.iconSize}px`,\n borderRadius: \"8px\",\n overflow: \"hidden\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.5)}`,\n flexShrink: 0,\n }),\n [layout.iconSize],\n );\n\n const nameStyle = useMemo(() => {\n const textAlign = isLeft ? \"left\" : \"right\";\n return {\n fontSize: `${layout.fontSize}px`,\n fontWeight: \"bold\" as const,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n textAlign: textAlign as \"left\" | \"right\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8), 0 0 8px rgba(0,0,0,0.6)\",\n padding: \"2px 6px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.7),\n borderRadius: \"4px\",\n whiteSpace: \"nowrap\" as const,\n };\n }, [layout.fontSize, isLeft]);\n\n const stanceStyle = useMemo(() => {\n const textAlign = isLeft ? \"left\" : \"right\";\n return {\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 1),\n textAlign: textAlign as \"left\" | \"right\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n padding: \"4px 8px\",\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n borderRadius: \"4px\",\n marginTop: \"2px\",\n };\n }, [isMobile, isLeft]);\n\n // Memoize error handler to prevent recreating on every render\n const handleImageError = useCallback(\n (e: React.SyntheticEvent<HTMLImageElement>) => {\n const target = e.target as HTMLImageElement;\n if (!target.src.endsWith(FALLBACK_ARCHETYPE_IMAGE)) {\n target.src = FALLBACK_ARCHETYPE_IMAGE;\n }\n },\n [],\n );\n\n return (\n <div data-testid={`player-hud-${playerId}`} style={containerStyle}>\n {/* Player Name with Archetype Icon */}\n <div data-testid={`player-name-${playerId}`} style={iconContainerStyle}>\n {/* Archetype Icon */}\n <div data-testid={`archetype-icon-${playerId}`} style={iconStyle}>\n <img\n src={archetypeImagePath}\n alt={`${player.name.english} archetype`}\n style={{\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n }}\n onError={handleImageError}\n />\n </div>\n {/* Player Name */}\n <div style={nameStyle}>\n {player.name.korean} | {player.name.english}\n </div>\n </div>\n\n {/* Combat Readiness Bar - shows overall combat capability */}\n <CombatReadinessBar\n player={player}\n playerId={playerId}\n isMobile={isMobile}\n />\n\n {/* Health Bar - shows aggregate body health */}\n <HealthBar\n current={player.health}\n max={player.maxHealth}\n playerId={playerId}\n isMobile={isMobile}\n />\n\n {/* Stamina Bar */}\n <StaminaBar\n current={player.stamina}\n max={player.maxStamina}\n playerId={playerId}\n isMobile={isMobile}\n />\n\n {/* Breathing Disruption Indicator */}\n <BreathingIndicator player={player} isMobile={isMobile} />\n\n {/* Laterality Indicator */}\n {laterality && (\n <LateralityIndicator laterality={laterality} isMobile={isMobile} />\n )}\n\n {/* Current Stance Indicator */}\n <div data-testid={`stance-indicator-${playerId}`} style={stanceStyle}>\n 자세 | Stance: {player.currentStance}\n </div>\n </div>\n );\n};\n\n/**\n * Memoized PlayerHUD with custom comparison\n * Only re-renders when relevant props change\n */\nexport const PlayerHUD = React.memo(\n PlayerHUDComponent,\n (prevProps, nextProps) => {\n // Compare player state\n const healthSame = prevProps.player.health === nextProps.player.health;\n const maxHealthSame =\n prevProps.player.maxHealth === nextProps.player.maxHealth;\n const staminaSame = prevProps.player.stamina === nextProps.player.stamina;\n const maxStaminaSame =\n prevProps.player.maxStamina === nextProps.player.maxStamina;\n const archetypeSame =\n prevProps.player.archetype === nextProps.player.archetype;\n const stanceSame =\n prevProps.player.currentStance === nextProps.player.currentStance;\n const idSame = prevProps.player.id === nextProps.player.id;\n const nameSame =\n prevProps.player.name.korean === nextProps.player.name.korean &&\n prevProps.player.name.english === nextProps.player.name.english;\n\n // Compare statusEffects for BreathingIndicator updates\n const statusEffectsSame =\n prevProps.player.statusEffects.length ===\n nextProps.player.statusEffects.length &&\n prevProps.player.statusEffects.every(\n (effect, index) => effect === nextProps.player.statusEffects[index],\n );\n\n // Compare combat readiness factors for CombatReadinessBar updates\n // These properties are used by calculateCombatReadiness()\n const bodyPartHealthSame =\n prevProps.player.bodyPartHealth === nextProps.player.bodyPartHealth;\n const painSame = prevProps.player.pain === nextProps.player.pain;\n const consciousnessSame =\n prevProps.player.consciousness === nextProps.player.consciousness;\n const balanceSame = prevProps.player.balance === nextProps.player.balance;\n\n // Compare other props\n const positionSame = prevProps.position === nextProps.position;\n const mobileSame = prevProps.isMobile === nextProps.isMobile;\n const lateralitySame = prevProps.laterality === nextProps.laterality;\n\n // Return true if all relevant props are the same (skip re-render)\n return (\n healthSame &&\n maxHealthSame &&\n staminaSame &&\n maxStaminaSame &&\n archetypeSame &&\n stanceSame &&\n idSame &&\n nameSame &&\n statusEffectsSame &&\n bodyPartHealthSame &&\n painSame &&\n consciousnessSame &&\n balanceSame &&\n positionSame &&\n mobileSame &&\n lateralitySame\n );\n },\n);\n\nexport default PlayerHUD;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProgressBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ProgressBar.tsx"],"sourcesContent":["/**\n * ProgressBar - Three.js-compatible progress bar component\n * \n * Displays health, ki, stamina with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Progress bar type\n */\nexport type ProgressBarType = \"health\" | \"ki\" | \"stamina\";\n\n/**\n * Props for ProgressBar component\n */\nexport interface ProgressBarProps {\n readonly type: ProgressBarType;\n readonly current: number;\n readonly max: number;\n readonly label?: { korean: string; english: string };\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly height?: number;\n readonly showText?: boolean;\n readonly animated?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ProgressBar Component\n * \n * A progress bar component for displaying health, ki, and stamina.\n * Uses Korean cyberpunk theming with gradient fills.\n * \n * @example\n * ```tsx\n * <ProgressBar\n * type=\"health\"\n * current={75}\n * max={100}\n * label={{ korean: \"체력\", english: \"Health\" }}\n * />\n * ```\n */\nexport const ProgressBar: React.FC<ProgressBarProps> = ({\n type,\n current,\n max,\n label,\n position = [0, 0, 0],\n width = 200,\n height = 24,\n showText = true,\n animated = true,\n testId,\n}) => {\n // Calculate percentage safely\n const percentage = useMemo(\n () => Math.max(0, Math.min(1, max > 0 ? current / max : 0)),\n [current, max]\n );\n\n // Get colors based on type and percentage\n const colors = useMemo(() => {\n switch (type) {\n case \"health\":\n if (percentage > 0.6) {\n return {\n start: KOREAN_COLORS.HEALTH_FULL,\n end: KOREAN_COLORS.HEALTH_MEDIUM,\n glow: KOREAN_COLORS.POSITIVE_GREEN,\n };\n } else if (percentage > 0.3) {\n return {\n start: KOREAN_COLORS.HEALTH_MEDIUM,\n end: KOREAN_COLORS.HEALTH_LOW,\n glow: KOREAN_COLORS.WARNING_ORANGE,\n };\n } else {\n return {\n start: KOREAN_COLORS.HEALTH_LOW,\n end: KOREAN_COLORS.HEALTH_CRITICAL,\n glow: KOREAN_COLORS.ACCENT_RED,\n };\n }\n case \"ki\":\n return {\n start: KOREAN_COLORS.KI_FULL,\n end: KOREAN_COLORS.KI_MEDIUM,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n case \"stamina\":\n return {\n start: KOREAN_COLORS.STAMINA_FULL,\n end: KOREAN_COLORS.STAMINA_MEDIUM,\n glow: KOREAN_COLORS.SECONDARY_YELLOW,\n };\n default:\n return {\n start: KOREAN_COLORS.PRIMARY_CYAN,\n end: KOREAN_COLORS.ACCENT_BLUE,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n }\n }, [type, percentage]);\n\n // Memoize container styles for performance\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n [width]\n );\n\n // Memoize label styles for performance\n const labelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }),\n []\n );\n\n // Memoize bar container styles for performance\n const barContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: \"100%\",\n height: `${height}px`,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.UI_BORDER, 0.6)}`,\n borderRadius: \"4px\",\n overflow: \"hidden\",\n position: \"relative\",\n }),\n [height]\n );\n\n // Memoize fill styles for performance\n const fillStyle = useMemo<React.CSSProperties>(() => {\n return {\n width: `${percentage * 100}%`,\n height: \"100%\",\n background: `linear-gradient(to right, ${hexToRgbaString(colors.start)}, ${hexToRgbaString(colors.end)})`,\n transition: animated ? \"width 0.3s ease\" : \"none\",\n position: \"relative\",\n boxShadow: animated\n ? `0 0 10px ${hexToRgbaString(colors.glow, 0.5)}`\n : \"none\",\n };\n }, [percentage, colors, animated]);\n\n // Memoize shine effect styles for performance\n const shineStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"0\",\n left: \"0\",\n width: \"60%\",\n height: \"40%\",\n background: hexToRgbaString(KOREAN_COLORS.WHITE_SOLID, 0.3),\n borderRadius: \"4px\",\n margin: \"2px\",\n }),\n []\n );\n\n // Memoize text styles for performance\n const textStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"11px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `\n 0 1px 2px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.8)},\n 0 0 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.6)}\n `,\n whiteSpace: \"nowrap\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div \n style={containerStyle} \n data-testid={testId ?? `progress-bar-${type}`}\n data-current={current}\n data-max={max}\n data-percentage={Math.round(percentage * 100)}\n >\n {/* Label */}\n {label && showText && (\n <div style={labelStyle}>\n <span>\n {label.korean} | {label.english}\n </span>\n <span>\n {Math.ceil(current)} / {max}\n </span>\n </div>\n )}\n\n {/* Bar Container */}\n <div style={barContainerStyle}>\n {/* Fill */}\n <div style={fillStyle}>\n {/* Shine effect */}\n <div style={shineStyle} />\n </div>\n\n {/* Percentage Text Overlay */}\n {showText && (\n <div style={textStyle}>\n {Math.round(percentage * 100)}%\n </div>\n )}\n </div>\n </div>\n </Html>\n );\n};\n\nProgressBar.displayName = \"ProgressBar\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProgressBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ProgressBar.tsx"],"sourcesContent":["/**\n * ProgressBar - Three.js-compatible progress bar component\n * \n * Displays health, ki, stamina with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Progress bar type\n */\nexport type ProgressBarType = \"health\" | \"ki\" | \"stamina\";\n\n/**\n * Props for ProgressBar component\n */\nexport interface ProgressBarProps {\n readonly type: ProgressBarType;\n readonly current: number;\n readonly max: number;\n readonly label?: { korean: string; english: string };\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly height?: number;\n readonly showText?: boolean;\n readonly animated?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ProgressBar Component\n * \n * A progress bar component for displaying health, ki, and stamina.\n * Uses Korean cyberpunk theming with gradient fills.\n * \n * @example\n * ```tsx\n * <ProgressBar\n * type=\"health\"\n * current={75}\n * max={100}\n * label={{ korean: \"체력\", english: \"Health\" }}\n * />\n * ```\n */\nexport const ProgressBar: React.FC<ProgressBarProps> = ({\n type,\n current,\n max,\n label,\n position = [0, 0, 0],\n width = 200,\n height = 24,\n showText = true,\n animated = true,\n testId,\n}) => {\n // Calculate percentage safely\n const percentage = useMemo(\n () => Math.max(0, Math.min(1, max > 0 ? current / max : 0)),\n [current, max]\n );\n\n // Get colors based on type and percentage\n const colors = useMemo(() => {\n switch (type) {\n case \"health\":\n if (percentage > 0.6) {\n return {\n start: KOREAN_COLORS.HEALTH_FULL,\n end: KOREAN_COLORS.HEALTH_MEDIUM,\n glow: KOREAN_COLORS.POSITIVE_GREEN,\n };\n } else if (percentage > 0.3) {\n return {\n start: KOREAN_COLORS.HEALTH_MEDIUM,\n end: KOREAN_COLORS.HEALTH_LOW,\n glow: KOREAN_COLORS.WARNING_ORANGE,\n };\n } else {\n return {\n start: KOREAN_COLORS.HEALTH_LOW,\n end: KOREAN_COLORS.HEALTH_CRITICAL,\n glow: KOREAN_COLORS.ACCENT_RED,\n };\n }\n case \"ki\":\n return {\n start: KOREAN_COLORS.KI_FULL,\n end: KOREAN_COLORS.KI_MEDIUM,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n case \"stamina\":\n return {\n start: KOREAN_COLORS.STAMINA_FULL,\n end: KOREAN_COLORS.STAMINA_MEDIUM,\n glow: KOREAN_COLORS.SECONDARY_YELLOW,\n };\n default:\n return {\n start: KOREAN_COLORS.PRIMARY_CYAN,\n end: KOREAN_COLORS.ACCENT_BLUE,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n }\n }, [type, percentage]);\n\n // Memoize container styles for performance\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n [width]\n );\n\n // Memoize label styles for performance\n const labelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }),\n []\n );\n\n // Memoize bar container styles for performance\n const barContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: \"100%\",\n height: `${height}px`,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.UI_BORDER, 0.6)}`,\n borderRadius: \"4px\",\n overflow: \"hidden\",\n position: \"relative\",\n }),\n [height]\n );\n\n // Memoize fill styles for performance\n const fillStyle = useMemo<React.CSSProperties>(() => {\n return {\n width: `${percentage * 100}%`,\n height: \"100%\",\n background: `linear-gradient(to right, ${hexToRgbaString(colors.start)}, ${hexToRgbaString(colors.end)})`,\n transition: animated ? \"width 0.3s ease\" : \"none\",\n position: \"relative\",\n boxShadow: animated\n ? `0 0 10px ${hexToRgbaString(colors.glow, 0.5)}`\n : \"none\",\n };\n }, [percentage, colors, animated]);\n\n // Memoize shine effect styles for performance\n const shineStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"0\",\n left: \"0\",\n width: \"60%\",\n height: \"40%\",\n background: hexToRgbaString(KOREAN_COLORS.WHITE_SOLID, 0.3),\n borderRadius: \"4px\",\n margin: \"2px\",\n }),\n []\n );\n\n // Memoize text styles for performance\n const textStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"11px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `\n 0 1px 2px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.8)},\n 0 0 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.6)}\n `,\n whiteSpace: \"nowrap\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div \n style={containerStyle} \n data-testid={testId ?? `progress-bar-${type}`}\n data-current={current}\n data-max={max}\n data-percentage={Math.round(percentage * 100)}\n >\n {/* Label */}\n {label && showText && (\n <div style={labelStyle}>\n <span>\n {label.korean} | {label.english}\n </span>\n <span>\n {Math.ceil(current)} / {max}\n </span>\n </div>\n )}\n\n {/* Bar Container */}\n <div style={barContainerStyle}>\n {/* Fill */}\n <div style={fillStyle}>\n {/* Shine effect */}\n <div style={shineStyle} />\n </div>\n\n {/* Percentage Text Overlay */}\n {showText && (\n <div style={textStyle}>\n {Math.round(percentage * 100)}%\n </div>\n )}\n </div>\n </div>\n </Html>\n );\n};\n\nProgressBar.displayName = \"ProgressBar\";\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SpeedIndicatorHUD.js","names":[],"sources":["../../../../../src/components/shared/three/ui/SpeedIndicatorHUD.tsx"],"sourcesContent":["/**\n * SpeedIndicatorHUD Component - Visual indicator for player movement speed\n *\n * Displays a speed percentage indicator showing the current movement speed\n * relative to base speed, taking into account stance modifiers, injuries,\n * stamina, and combat state.\n *\n * @module components/shared/three/ui/SpeedIndicatorHUD\n * @category Shared UI\n * @korean 속도표시기\n */\n\nimport React, { useMemo } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport \"./HUDAnimations.css\";\n\nexport interface SpeedIndicatorHUDProps {\n /**\n * Final movement speed in m/s\n * @korean 최종속도\n */\n readonly finalSpeed: number;\n\n /**\n * Base movement speed before modifiers in m/s\n * @korean 기본속도\n */\n readonly baseSpeed: number;\n\n /**\n * Player position ('left' or 'right' side of screen)\n * @korean 플레이어위치\n */\n readonly position: \"left\" | \"right\";\n\n /**\n * Mobile responsive mode (smaller text)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Whether to show the indicator (optional, default: true)\n * @korean 표시여부\n */\n readonly visible?: boolean;\n}\n\n/**\n * Get color for speed percentage\n *\n * **Korean**: 속도 색상 (Speed Color)\n *\n * Color coding:\n * - Green: 100%+ (boosted speed)\n * - Cyan: 80-99% (good speed)\n * - Yellow: 50-79% (reduced speed)\n * - Orange: 25-49% (heavily reduced)\n * - Red: <25% (critical reduction)\n */\nfunction getSpeedColor(speedPercent: number): string {\n let colorValue: number;\n\n if (speedPercent >= 100) {\n colorValue = KOREAN_COLORS.POSITIVE_GREEN;\n } else if (speedPercent >= 80) {\n colorValue = KOREAN_COLORS.PRIMARY_CYAN;\n } else if (speedPercent >= 50) {\n colorValue = KOREAN_COLORS.WARNING_YELLOW;\n } else if (speedPercent >= 25) {\n colorValue = KOREAN_COLORS.WARNING_ORANGE;\n } else {\n colorValue = KOREAN_COLORS.ACCENT_RED;\n }\n\n // Convert number to properly formatted hex string with # prefix\n return `#${colorValue.toString(16).padStart(6, \"0\")}`;\n}\n\n/**\n * Get Korean label for speed range\n */\nfunction getSpeedLabel(speedPercent: number): {\n korean: string;\n english: string;\n} {\n if (speedPercent >= 100) {\n return { korean: \"가속\", english: \"BOOSTED\" };\n } else if (speedPercent >= 80) {\n return { korean: \"양호\", english: \"GOOD\" };\n } else if (speedPercent >= 50) {\n return { korean: \"감소\", english: \"REDUCED\" };\n } else if (speedPercent >= 25) {\n return { korean: \"저하\", english: \"SLOWED\" };\n } else {\n return { korean: \"위급\", english: \"CRITICAL\" };\n }\n}\n\n/**\n * SpeedIndicatorHUD - Movement speed percentage indicator\n *\n * Displays current movement speed as a percentage of base speed\n * with color coding and bilingual labels. Updates dynamically as\n * speed modifiers change from stance, injury, stamina, and combat state.\n *\n * @example\n * ```tsx\n * <SpeedIndicatorHUD\n * finalSpeed={1.8}\n * baseSpeed={2.0}\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n *\n * @public\n * @korean 속도표시기\n */\nexport const SpeedIndicatorHUD: React.FC<SpeedIndicatorHUDProps> = ({\n finalSpeed,\n baseSpeed,\n position,\n isMobile,\n visible = true,\n}) => {\n const speedData = useMemo(() => {\n // Calculate speed as percentage of base\n const speedPercent = baseSpeed > 0 ? (finalSpeed / baseSpeed) * 100 : 100;\n const color = getSpeedColor(speedPercent);\n const label = getSpeedLabel(speedPercent);\n\n return {\n speedPercent: Math.round(speedPercent),\n color,\n label,\n };\n }, [finalSpeed, baseSpeed]);\n\n const containerStyle = useMemo(() => {\n const shouldGlow = speedData.speedPercent >= 100;\n return {\n position: \"relative\" as const,\n display: visible ? \"flex\" : \"none\",\n flexDirection: \"column\" as const,\n alignItems: \"center\" as const,\n gap: isMobile ? \"4px\" : \"6px\",\n width: \"100%\",\n padding: isMobile ? \"8px 12px\" : \"10px 16px\",\n backgroundColor: `rgba(0, 0, 0, 0.7)`,\n border: `2px solid ${speedData.color}`,\n borderRadius: \"6px\",\n boxShadow: shouldGlow \n ? `0 0 15px ${speedData.color}, 0 0 25px ${speedData.color}40`\n : `0 0 10px ${speedData.color}`,\n pointerEvents: \"none\" as const,\n transition: \"all 0.3s ease-out\",\n animation: shouldGlow ? \"speedGlow 1.5s ease-in-out infinite\" : \"none\",\n };\n }, [isMobile, visible, speedData.color, speedData.speedPercent]);\n\n const percentStyle = useMemo(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"18px\" : \"22px\",\n fontWeight: \"bold\" as const,\n color: speedData.color,\n textShadow: `0 0 8px ${speedData.color}`,\n lineHeight: 1,\n margin: 0,\n }),\n [isMobile, speedData.color],\n );\n\n const labelStyle = useMemo(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontWeight: \"normal\" as const,\n color: speedData.color,\n opacity: 0.9,\n letterSpacing: \"0.5px\",\n lineHeight: 1,\n margin: 0,\n }),\n [isMobile, speedData.color],\n );\n\n const koreanLabelStyle = useMemo(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"11px\" : \"13px\",\n fontWeight: \"600\" as const,\n color: speedData.color,\n opacity: 0.95,\n letterSpacing: \"0.5px\",\n lineHeight: 1,\n margin: 0,\n }),\n [isMobile, speedData.color],\n );\n\n return (\n <div\n data-testid={`speed-indicator-${position}`}\n className=\"hud-animated\"\n style={containerStyle}\n aria-label={`${speedData.label.korean} | ${speedData.label.english}: ${speedData.speedPercent}%`}\n role=\"status\"\n aria-live=\"polite\"\n >\n {/* Speed percentage */}\n <div style={percentStyle}>{speedData.speedPercent}%</div>\n\n {/* Korean label */}\n <div style={koreanLabelStyle}>{speedData.label.korean}</div>\n\n {/* English label */}\n <div style={labelStyle}>{speedData.label.english}</div>\n\n {/* Speed unit label */}\n <div\n style={{\n ...labelStyle,\n fontSize: isMobile ? \"9px\" : \"10px\",\n opacity: 0.7,\n marginTop: isMobile ? \"2px\" : \"3px\",\n }}\n >\n 속도변경 | Speed\n </div>\n </div>\n );\n};\n\nSpeedIndicatorHUD.displayName = \"SpeedIndicatorHUD\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"SpeedIndicatorHUD.js","names":[],"sources":["../../../../../src/components/shared/three/ui/SpeedIndicatorHUD.tsx"],"sourcesContent":["/**\n * SpeedIndicatorHUD Component - Visual indicator for player movement speed\n *\n * Displays a speed percentage indicator showing the current movement speed\n * relative to base speed, taking into account stance modifiers, injuries,\n * stamina, and combat state.\n *\n * @module components/shared/three/ui/SpeedIndicatorHUD\n * @category Shared UI\n * @korean 속도표시기\n */\n\nimport React, { useMemo } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport \"./HUDAnimations.css\";\n\nexport interface SpeedIndicatorHUDProps {\n /**\n * Final movement speed in m/s\n * @korean 최종속도\n */\n readonly finalSpeed: number;\n\n /**\n * Base movement speed before modifiers in m/s\n * @korean 기본속도\n */\n readonly baseSpeed: number;\n\n /**\n * Player position ('left' or 'right' side of screen)\n * @korean 플레이어위치\n */\n readonly position: \"left\" | \"right\";\n\n /**\n * Mobile responsive mode (smaller text)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Whether to show the indicator (optional, default: true)\n * @korean 표시여부\n */\n readonly visible?: boolean;\n}\n\n/**\n * Get color for speed percentage\n *\n * **Korean**: 속도 색상 (Speed Color)\n *\n * Color coding:\n * - Green: 100%+ (boosted speed)\n * - Cyan: 80-99% (good speed)\n * - Yellow: 50-79% (reduced speed)\n * - Orange: 25-49% (heavily reduced)\n * - Red: <25% (critical reduction)\n */\nfunction getSpeedColor(speedPercent: number): string {\n let colorValue: number;\n\n if (speedPercent >= 100) {\n colorValue = KOREAN_COLORS.POSITIVE_GREEN;\n } else if (speedPercent >= 80) {\n colorValue = KOREAN_COLORS.PRIMARY_CYAN;\n } else if (speedPercent >= 50) {\n colorValue = KOREAN_COLORS.WARNING_YELLOW;\n } else if (speedPercent >= 25) {\n colorValue = KOREAN_COLORS.WARNING_ORANGE;\n } else {\n colorValue = KOREAN_COLORS.ACCENT_RED;\n }\n\n // Convert number to properly formatted hex string with # prefix\n return `#${colorValue.toString(16).padStart(6, \"0\")}`;\n}\n\n/**\n * Get Korean label for speed range\n */\nfunction getSpeedLabel(speedPercent: number): {\n korean: string;\n english: string;\n} {\n if (speedPercent >= 100) {\n return { korean: \"가속\", english: \"BOOSTED\" };\n } else if (speedPercent >= 80) {\n return { korean: \"양호\", english: \"GOOD\" };\n } else if (speedPercent >= 50) {\n return { korean: \"감소\", english: \"REDUCED\" };\n } else if (speedPercent >= 25) {\n return { korean: \"저하\", english: \"SLOWED\" };\n } else {\n return { korean: \"위급\", english: \"CRITICAL\" };\n }\n}\n\n/**\n * SpeedIndicatorHUD - Movement speed percentage indicator\n *\n * Displays current movement speed as a percentage of base speed\n * with color coding and bilingual labels. Updates dynamically as\n * speed modifiers change from stance, injury, stamina, and combat state.\n *\n * @example\n * ```tsx\n * <SpeedIndicatorHUD\n * finalSpeed={1.8}\n * baseSpeed={2.0}\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n *\n * @public\n * @korean 속도표시기\n */\nexport const SpeedIndicatorHUD: React.FC<SpeedIndicatorHUDProps> = ({\n finalSpeed,\n baseSpeed,\n position,\n isMobile,\n visible = true,\n}) => {\n const speedData = useMemo(() => {\n // Calculate speed as percentage of base\n const speedPercent = baseSpeed > 0 ? (finalSpeed / baseSpeed) * 100 : 100;\n const color = getSpeedColor(speedPercent);\n const label = getSpeedLabel(speedPercent);\n\n return {\n speedPercent: Math.round(speedPercent),\n color,\n label,\n };\n }, [finalSpeed, baseSpeed]);\n\n const containerStyle = useMemo(() => {\n const shouldGlow = speedData.speedPercent >= 100;\n return {\n position: \"relative\" as const,\n display: visible ? \"flex\" : \"none\",\n flexDirection: \"column\" as const,\n alignItems: \"center\" as const,\n gap: isMobile ? \"4px\" : \"6px\",\n width: \"100%\",\n padding: isMobile ? \"8px 12px\" : \"10px 16px\",\n backgroundColor: `rgba(0, 0, 0, 0.7)`,\n border: `2px solid ${speedData.color}`,\n borderRadius: \"6px\",\n boxShadow: shouldGlow \n ? `0 0 15px ${speedData.color}, 0 0 25px ${speedData.color}40`\n : `0 0 10px ${speedData.color}`,\n pointerEvents: \"none\" as const,\n transition: \"all 0.3s ease-out\",\n animation: shouldGlow ? \"speedGlow 1.5s ease-in-out infinite\" : \"none\",\n };\n }, [isMobile, visible, speedData.color, speedData.speedPercent]);\n\n const percentStyle = useMemo(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"18px\" : \"22px\",\n fontWeight: \"bold\" as const,\n color: speedData.color,\n textShadow: `0 0 8px ${speedData.color}`,\n lineHeight: 1,\n margin: 0,\n }),\n [isMobile, speedData.color],\n );\n\n const labelStyle = useMemo(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontWeight: \"normal\" as const,\n color: speedData.color,\n opacity: 0.9,\n letterSpacing: \"0.5px\",\n lineHeight: 1,\n margin: 0,\n }),\n [isMobile, speedData.color],\n );\n\n const koreanLabelStyle = useMemo(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"11px\" : \"13px\",\n fontWeight: \"600\" as const,\n color: speedData.color,\n opacity: 0.95,\n letterSpacing: \"0.5px\",\n lineHeight: 1,\n margin: 0,\n }),\n [isMobile, speedData.color],\n );\n\n return (\n <div\n data-testid={`speed-indicator-${position}`}\n className=\"hud-animated\"\n style={containerStyle}\n aria-label={`${speedData.label.korean} | ${speedData.label.english}: ${speedData.speedPercent}%`}\n role=\"status\"\n aria-live=\"polite\"\n >\n {/* Speed percentage */}\n <div style={percentStyle}>{speedData.speedPercent}%</div>\n\n {/* Korean label */}\n <div style={koreanLabelStyle}>{speedData.label.korean}</div>\n\n {/* English label */}\n <div style={labelStyle}>{speedData.label.english}</div>\n\n {/* Speed unit label */}\n <div\n style={{\n ...labelStyle,\n fontSize: isMobile ? \"9px\" : \"10px\",\n opacity: 0.7,\n marginTop: isMobile ? \"2px\" : \"3px\",\n }}\n >\n 속도변경 | Speed\n </div>\n </div>\n );\n};\n\nSpeedIndicatorHUD.displayName = \"SpeedIndicatorHUD\";\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StaminaBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/StaminaBar.tsx"],"sourcesContent":["/**\n * StaminaBar Component - Segmented stamina display with Korean theming\n * \n * Displays player stamina with:\n * - 5 segmented bars\n * - Consistent cyan/blue theming\n * - Pulse animation when stamina <20%\n * - Korean/English bilingual labels\n * - Numeric value display (e.g., \"45/50\")\n * - Responsive sizing for mobile/tablet/desktop\n * - Smooth transitions and glow effects\n * \n * Performance: Uses React.memo with shallow comparison for 60fps optimization.\n * Note: React.memo uses shallow comparison by default, which works correctly\n * for this component since all props are primitives (number, string, boolean).\n * If object or function props are added in the future, consider adding a\n * custom comparison function or using useCallback/useMemo for prop stability.\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\nexport interface StaminaBarProps {\n /** Current stamina value */\n readonly current: number;\n /** Maximum stamina capacity */\n readonly max: number;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * StaminaBar - Segmented stamina display with Korean theming\n * Performance optimized with React.memo\n */\nexport const StaminaBar: React.FC<StaminaBarProps> = React.memo(({\n current,\n max,\n playerId,\n isMobile,\n}) => {\n // Calculate stamina percentage\n const staminaPercent = useMemo(\n () => Math.max(0, Math.min(100, (current / max) * 100)),\n [current, max]\n );\n\n const segments = 5;\n const filledSegments = Math.ceil((staminaPercent / 100) * segments);\n const shouldPulse = staminaPercent < 20;\n\n // Responsive sizing with memoization\n const layout = useMemo(() => ({\n barWidth: isMobile ? 180 : 250,\n barHeight: isMobile ? 10 : 12,\n fontSize: isMobile ? 10 : 11,\n padding: isMobile ? \"6px 8px\" : \"8px 12px\",\n }), [isMobile]);\n\n return (\n <div\n data-testid={`stamina-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"기력 | Stamina\"\n aria-valuenow={Math.ceil(current)}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuetext={`${Math.ceil(current)} out of ${max}`}\n className=\"hud-animated\"\n style={{\n width: `${layout.barWidth}px`,\n padding: layout.padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)}`,\n boxShadow: `0 0 8px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.2)}`,\n transition: \"box-shadow 0.3s ease-in-out, border-color 0.3s ease-in-out\",\n }}\n >\n {/* Label and numeric display */}\n <div\n style={{\n fontSize: `${layout.fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"3px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n transition: \"color 0.2s ease-in-out\",\n }}\n >\n <span>기력 | Stamina</span>\n <span data-testid={`stamina-value-${playerId}`}>\n {Math.ceil(current)}/{max}\n </span>\n </div>\n\n {/* Segmented stamina bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"4px\",\n height: `${layout.barHeight}px`,\n animation: shouldPulse ? \"staminaPulse 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`stamina-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.2s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 6px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n </div>\n );\n});\n\nStaminaBar.displayName = \"StaminaBar\";\n\nexport default StaminaBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,aAAwC,MAAM,MAAM,EAC/D,SACA,KACA,UACA,eACI;CAEJ,MAAM,iBAAiB,cACf,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,IAAI,CAAC,EACvD,CAAC,SAAS,IAAI,CACf;CAED,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,iBAAiB,MAAO,SAAS;CACnE,MAAM,cAAc,iBAAiB;CAGrC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,YAAY;EACjC,GAAG,CAAC,SAAS,CAAC;AAEf,QACE,qBAAC,OAAD;EACE,eAAa,eAAe;EAC5B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,QAAQ;EACjC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,QAAQ,CAAC,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,EAAE;GACrE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,aAAa,EAAE;GAClE,WAAW,WAAW,gBAAgB,cAAc,aAAa,GAAI;GACrE,YAAY;GACb;YAjBH,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,aAAa,EAAE;IACpD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACb;aAVH,CAYE,oBAAC,QAAD,EAAA,UAAM,gBAAmB,CAAA,EACzB,qBAAC,QAAD;IAAM,eAAa,iBAAiB;cAApC;KACG,KAAK,KAAK,QAAQ;KAAC;KAAE;KACjB;MACH;MAGN,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,2CAA2C;IACrE;aAEA,MAAM,KAAK,EAAE,QAAQ,UAAU,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,mBAAmB,SAAS,GAAG;IAC5C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,cAAc,aAAa,EAAE,GAC7C,gBAAgB,cAAc,sBAAsB,EAAE;KAC5D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,cAAc,aAAa,GAAI,KAC1D;KACP;IACD,EAfK,MAeL,CACF;GACE,CAAA,CACF;;EAER;AAEF,WAAW,cAAc"}
1
+ {"version":3,"file":"StaminaBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/StaminaBar.tsx"],"sourcesContent":["/**\n * StaminaBar Component - Segmented stamina display with Korean theming\n * \n * Displays player stamina with:\n * - 5 segmented bars\n * - Consistent cyan/blue theming\n * - Pulse animation when stamina <20%\n * - Korean/English bilingual labels\n * - Numeric value display (e.g., \"45/50\")\n * - Responsive sizing for mobile/tablet/desktop\n * - Smooth transitions and glow effects\n * \n * Performance: Uses React.memo with shallow comparison for 60fps optimization.\n * Note: React.memo uses shallow comparison by default, which works correctly\n * for this component since all props are primitives (number, string, boolean).\n * If object or function props are added in the future, consider adding a\n * custom comparison function or using useCallback/useMemo for prop stability.\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\nexport interface StaminaBarProps {\n /** Current stamina value */\n readonly current: number;\n /** Maximum stamina capacity */\n readonly max: number;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * StaminaBar - Segmented stamina display with Korean theming\n * Performance optimized with React.memo\n */\nexport const StaminaBar: React.FC<StaminaBarProps> = React.memo(({\n current,\n max,\n playerId,\n isMobile,\n}) => {\n // Calculate stamina percentage\n const staminaPercent = useMemo(\n () => Math.max(0, Math.min(100, (current / max) * 100)),\n [current, max]\n );\n\n const segments = 5;\n const filledSegments = Math.ceil((staminaPercent / 100) * segments);\n const shouldPulse = staminaPercent < 20;\n\n // Responsive sizing with memoization\n const layout = useMemo(() => ({\n barWidth: isMobile ? 180 : 250,\n barHeight: isMobile ? 10 : 12,\n fontSize: isMobile ? 10 : 11,\n padding: isMobile ? \"6px 8px\" : \"8px 12px\",\n }), [isMobile]);\n\n return (\n <div\n data-testid={`stamina-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"기력 | Stamina\"\n aria-valuenow={Math.ceil(current)}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuetext={`${Math.ceil(current)} out of ${max}`}\n className=\"hud-animated\"\n style={{\n width: `${layout.barWidth}px`,\n padding: layout.padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)}`,\n boxShadow: `0 0 8px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.2)}`,\n transition: \"box-shadow 0.3s ease-in-out, border-color 0.3s ease-in-out\",\n }}\n >\n {/* Label and numeric display */}\n <div\n style={{\n fontSize: `${layout.fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"3px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n transition: \"color 0.2s ease-in-out\",\n }}\n >\n <span>기력 | Stamina</span>\n <span data-testid={`stamina-value-${playerId}`}>\n {Math.ceil(current)}/{max}\n </span>\n </div>\n\n {/* Segmented stamina bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"4px\",\n height: `${layout.barHeight}px`,\n animation: shouldPulse ? \"staminaPulse 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`stamina-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.2s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 6px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n </div>\n );\n});\n\nStaminaBar.displayName = \"StaminaBar\";\n\nexport default StaminaBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,aAAwC,MAAM,MAAM,EAC/D,SACA,KACA,UACA,eACI;CAEJ,MAAM,iBAAiB,cACf,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,IAAI,CAAC,EACvD,CAAC,SAAS,IAAI,CACf;CAED,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,iBAAiB,MAAO,SAAS;CACnE,MAAM,cAAc,iBAAiB;CAGrC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,YAAY;EACjC,GAAG,CAAC,SAAS,CAAC;CAEf,OACE,qBAAC,OAAD;EACE,eAAa,eAAe;EAC5B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,QAAQ;EACjC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,QAAQ,CAAC,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,EAAE;GACrE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,aAAa,EAAE;GAClE,WAAW,WAAW,gBAAgB,cAAc,aAAa,GAAI;GACrE,YAAY;GACb;YAjBH,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,aAAa,EAAE;IACpD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACb;aAVH,CAYE,oBAAC,QAAD,EAAA,UAAM,gBAAmB,CAAA,EACzB,qBAAC,QAAD;IAAM,eAAa,iBAAiB;cAApC;KACG,KAAK,KAAK,QAAQ;KAAC;KAAE;KACjB;MACH;MAGN,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,2CAA2C;IACrE;aAEA,MAAM,KAAK,EAAE,QAAQ,UAAU,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,mBAAmB,SAAS,GAAG;IAC5C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,cAAc,aAAa,EAAE,GAC7C,gBAAgB,cAAc,sBAAsB,EAAE;KAC5D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,cAAc,aAAa,GAAI,KAC1D;KACP;IACD,EAfK,MAeL,CACF;GACE,CAAA,CACF;;EAER;AAEF,WAAW,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"TechniqueBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/TechniqueBar.tsx"],"sourcesContent":["/**\n * TechniqueBar Component\n *\n * **Korean**: 기술 바 컴포넌트 (Technique Bar Component)\n *\n * Horizontal bar displaying 3-5 technique cards at the bottom-center of combat HUD.\n * Manages technique selection, resource availability, and cooldown states.\n *\n * @module components/shared/three/ui/TechniqueBar\n * @category Shared UI\n * @korean 기술바\n */\n\nimport React, { useMemo } from \"react\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { Technique } from \"../../../../types\";\nimport {\n HUD_SIDE_CONTROL_RESERVES,\n TECHNIQUE_BAR_MIN_READABLE_SCALE,\n} from \"../../../../types/constants/layout\";\nimport { TechniqueCard } from \"./TechniqueCard\";\n\n/**\n * Props for TechniqueBar component.\n */\nexport interface TechniqueBarProps {\n /** Available techniques for player */\n readonly techniques: readonly Technique[];\n\n /** Player state with resources */\n readonly player: PlayerState;\n\n /** Index of currently selected technique */\n readonly selectedIndex: number;\n\n /** Active cooldowns map (techniqueId -> remaining ms) */\n readonly cooldowns: ReadonlyMap<string, number>;\n\n /** Callback when technique is selected */\n readonly onTechniqueSelect: (index: number) => void;\n\n /** Callback when hovering technique (for tooltip) */\n readonly onTechniqueHover: (technique: Technique | null) => void;\n\n /** Whether rendering for mobile device */\n readonly isMobile: boolean;\n\n /** Screen width for positioning */\n readonly screenWidth: number;\n\n /** Screen height for positioning */\n readonly screenHeight: number;\n\n /** Whether to use embedded mode (relative positioning, no absolute) */\n readonly embedded?: boolean;\n\n /**\n * Actual available pixel width of the container when in embedded mode.\n *\n * Embedded parents (TrainingBottomHUD, CombatBottomHUD) reserve space for\n * side controls via margins/padding, so the real container width is smaller\n * than `screenWidth`. Passing the pre-computed pixel width here ensures\n * the rawScale / shouldScroll decision is accurate and prevents cards from\n * overflowing back under side controls.\n *\n * When omitted in embedded mode, the component falls back to\n * `screenWidth − 2 × HUD_SIDE_CONTROL_RESERVES` (previous behaviour).\n */\n readonly containerWidth?: number;\n}\n\n/**\n * TechniqueBar Component\n *\n * Displays a horizontal bar of technique cards positioned at the bottom-center\n * of the combat screen. Each card shows technique details and availability.\n *\n * @param props - Component props\n * @returns TechniqueBar component\n */\nexport const TechniqueBar: React.FC<TechniqueBarProps> = ({\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n onTechniqueHover,\n isMobile,\n screenWidth,\n screenHeight,\n embedded = false,\n containerWidth,\n}) => {\n // Calculate card sizing and spacing\n const layout = useMemo(() => {\n const cardWidth = isMobile ? 70 : 90;\n const cardHeight = isMobile ? 80 : 100;\n const gap = isMobile ? 8 : 12;\n const totalWidth = Math.max(\n 0,\n techniques.length * cardWidth + (techniques.length - 1) * gap,\n );\n\n // When embedded and the parent supplies its actual pixel width, use that\n // directly so rawScale / shouldScroll reflects the real available space.\n // Falls back to screenWidth − 2× side-reserve when containerWidth is not\n // provided (non-embedded or legacy callers).\n let availableWidth: number;\n if (embedded && containerWidth !== undefined) {\n availableWidth = Math.max(cardWidth, containerWidth);\n } else {\n const reservedSideWidth = embedded\n ? (isMobile\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_DESKTOP)\n : 0;\n availableWidth = Math.max(cardWidth, screenWidth - reservedSideWidth * 2);\n }\n const rawScale =\n totalWidth > 0 ? Math.min(1, availableWidth / totalWidth) : 1;\n const shouldScroll =\n embedded && rawScale < TECHNIQUE_BAR_MIN_READABLE_SCALE;\n const visualScale = shouldScroll ? 1 : rawScale;\n\n return {\n cardWidth,\n cardHeight,\n gap,\n totalWidth,\n visualScale,\n shouldScroll,\n startX: (screenWidth - totalWidth) / 2,\n startY: screenHeight - cardHeight - (isMobile ? 100 : 120),\n };\n }, [techniques.length, isMobile, screenWidth, screenHeight, embedded, containerWidth]);\n\n // Check if player has sufficient resources for a technique\n const hasResources = (tech: Technique): boolean => {\n return player.stamina >= tech.staminaCost && player.ki >= tech.kiCost;\n };\n\n // Check if technique is available (has resources and not on cooldown)\n const isAvailable = (tech: Technique): boolean => {\n const onCooldown = (cooldowns.get(tech.id) ?? 0) > 0;\n return hasResources(tech) && !onCooldown;\n };\n\n // Calculate bottom position for proper placement (only used in non-embedded mode)\n const bottomOffset = isMobile ? 100 : 120;\n\n // Embedded mode: relative positioning inside parent container\n // Non-embedded: absolute positioning for standalone use\n const containerStyle: React.CSSProperties = embedded\n ? {\n position: \"relative\",\n display: \"flex\",\n justifyContent: layout.shouldScroll ? \"flex-start\" : \"center\",\n width: \"100%\",\n maxWidth: \"100%\",\n height: `${layout.cardHeight * layout.visualScale}px`,\n pointerEvents: \"auto\",\n overflowX: layout.shouldScroll ? \"auto\" : \"visible\",\n overflowY: \"visible\",\n // iOS momentum + scroll-snap for tactile feel\n scrollSnapType: layout.shouldScroll ? \"x proximity\" : undefined,\n WebkitOverflowScrolling: layout.shouldScroll ? \"touch\" : undefined,\n }\n : {\n position: \"absolute\",\n left: \"50%\",\n bottom: `${bottomOffset}px`,\n transform: \"translateX(-50%)\",\n width: `${layout.totalWidth}px`,\n height: `${layout.cardHeight}px`,\n display: \"flex\",\n gap: `${layout.gap}px`,\n pointerEvents: \"auto\",\n zIndex: 100,\n };\n\n return (\n <>\n {/* Technique Bar Container */}\n <div style={containerStyle} data-testid=\"technique-bar\">\n <div\n style={{\n display: \"flex\",\n gap: `${layout.gap}px`,\n justifyContent: \"center\",\n transform: embedded ? `scale(${layout.visualScale})` : undefined,\n transformOrigin: embedded\n ? layout.shouldScroll\n ? \"left center\"\n : \"center bottom\"\n : undefined,\n paddingInline: layout.shouldScroll ? \"8px\" : undefined,\n flexShrink: 0,\n }}\n >\n {techniques.map((technique, index) => {\n const cooldownRemaining = cooldowns.get(technique.id) ?? 0;\n const available = isAvailable(technique);\n\n return (\n <div\n key={technique.id}\n data-testid={`technique-slot-${index}`}\n style={layout.shouldScroll ? { scrollSnapAlign: \"start\" } : undefined}\n >\n <TechniqueCard\n technique={technique}\n isSelected={selectedIndex === index}\n isAvailable={available}\n staminaCost={technique.staminaCost}\n kiCost={technique.kiCost}\n remainingCooldown={cooldownRemaining}\n keyboardShortcut={technique.keyboardShortcut}\n onClick={() => onTechniqueSelect(index)}\n onHover={onTechniqueHover}\n isMobile={isMobile}\n playerArchetype={player.archetype}\n playerStance={player.currentStance}\n />\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Keyboard Hints - only shown in embedded mode or non-mobile */}\n {!isMobile && (\n <div\n style={{\n position: embedded ? \"relative\" : \"absolute\",\n left: embedded ? undefined : \"50%\",\n bottom: embedded\n ? undefined\n : `${bottomOffset - layout.cardHeight - 20}px`,\n transform: embedded ? undefined : \"translateX(-50%)\",\n width: embedded ? \"100%\" : `${layout.totalWidth}px`,\n textAlign: \"center\",\n fontSize: \"11px\",\n color: \"#aaa\",\n fontFamily: \"monospace\",\n pointerEvents: \"none\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n marginTop: embedded ? \"8px\" : undefined,\n }}\n >\n 기술 실행: Q-E-R-T-Y-F-G-Z-X-C | Press technique keys to execute\n </div>\n )}\n </>\n );\n};\n\nexport default TechniqueBar;\n"],"mappings":"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"}
1
+ {"version":3,"file":"TechniqueBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/TechniqueBar.tsx"],"sourcesContent":["/**\n * TechniqueBar Component\n *\n * **Korean**: 기술 바 컴포넌트 (Technique Bar Component)\n *\n * Horizontal bar displaying 3-5 technique cards at the bottom-center of combat HUD.\n * Manages technique selection, resource availability, and cooldown states.\n *\n * @module components/shared/three/ui/TechniqueBar\n * @category Shared UI\n * @korean 기술바\n */\n\nimport React, { useMemo } from \"react\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { Technique } from \"../../../../types\";\nimport {\n HUD_SIDE_CONTROL_RESERVES,\n TECHNIQUE_BAR_MIN_READABLE_SCALE,\n} from \"../../../../types/constants/layout\";\nimport { TechniqueCard } from \"./TechniqueCard\";\n\n/**\n * Props for TechniqueBar component.\n */\nexport interface TechniqueBarProps {\n /** Available techniques for player */\n readonly techniques: readonly Technique[];\n\n /** Player state with resources */\n readonly player: PlayerState;\n\n /** Index of currently selected technique */\n readonly selectedIndex: number;\n\n /** Active cooldowns map (techniqueId -> remaining ms) */\n readonly cooldowns: ReadonlyMap<string, number>;\n\n /** Callback when technique is selected */\n readonly onTechniqueSelect: (index: number) => void;\n\n /** Callback when hovering technique (for tooltip) */\n readonly onTechniqueHover: (technique: Technique | null) => void;\n\n /** Whether rendering for mobile device */\n readonly isMobile: boolean;\n\n /** Screen width for positioning */\n readonly screenWidth: number;\n\n /** Screen height for positioning */\n readonly screenHeight: number;\n\n /** Whether to use embedded mode (relative positioning, no absolute) */\n readonly embedded?: boolean;\n\n /**\n * Actual available pixel width of the container when in embedded mode.\n *\n * Embedded parents (TrainingBottomHUD, CombatBottomHUD) reserve space for\n * side controls via margins/padding, so the real container width is smaller\n * than `screenWidth`. Passing the pre-computed pixel width here ensures\n * the rawScale / shouldScroll decision is accurate and prevents cards from\n * overflowing back under side controls.\n *\n * When omitted in embedded mode, the component falls back to\n * `screenWidth − 2 × HUD_SIDE_CONTROL_RESERVES` (previous behaviour).\n */\n readonly containerWidth?: number;\n}\n\n/**\n * TechniqueBar Component\n *\n * Displays a horizontal bar of technique cards positioned at the bottom-center\n * of the combat screen. Each card shows technique details and availability.\n *\n * @param props - Component props\n * @returns TechniqueBar component\n */\nexport const TechniqueBar: React.FC<TechniqueBarProps> = ({\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n onTechniqueHover,\n isMobile,\n screenWidth,\n screenHeight,\n embedded = false,\n containerWidth,\n}) => {\n // Calculate card sizing and spacing\n const layout = useMemo(() => {\n const cardWidth = isMobile ? 70 : 90;\n const cardHeight = isMobile ? 80 : 100;\n const gap = isMobile ? 8 : 12;\n const totalWidth = Math.max(\n 0,\n techniques.length * cardWidth + (techniques.length - 1) * gap,\n );\n\n // When embedded and the parent supplies its actual pixel width, use that\n // directly so rawScale / shouldScroll reflects the real available space.\n // Falls back to screenWidth − 2× side-reserve when containerWidth is not\n // provided (non-embedded or legacy callers).\n let availableWidth: number;\n if (embedded && containerWidth !== undefined) {\n availableWidth = Math.max(cardWidth, containerWidth);\n } else {\n const reservedSideWidth = embedded\n ? (isMobile\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_DESKTOP)\n : 0;\n availableWidth = Math.max(cardWidth, screenWidth - reservedSideWidth * 2);\n }\n const rawScale =\n totalWidth > 0 ? Math.min(1, availableWidth / totalWidth) : 1;\n const shouldScroll =\n embedded && rawScale < TECHNIQUE_BAR_MIN_READABLE_SCALE;\n const visualScale = shouldScroll ? 1 : rawScale;\n\n return {\n cardWidth,\n cardHeight,\n gap,\n totalWidth,\n visualScale,\n shouldScroll,\n startX: (screenWidth - totalWidth) / 2,\n startY: screenHeight - cardHeight - (isMobile ? 100 : 120),\n };\n }, [techniques.length, isMobile, screenWidth, screenHeight, embedded, containerWidth]);\n\n // Check if player has sufficient resources for a technique\n const hasResources = (tech: Technique): boolean => {\n return player.stamina >= tech.staminaCost && player.ki >= tech.kiCost;\n };\n\n // Check if technique is available (has resources and not on cooldown)\n const isAvailable = (tech: Technique): boolean => {\n const onCooldown = (cooldowns.get(tech.id) ?? 0) > 0;\n return hasResources(tech) && !onCooldown;\n };\n\n // Calculate bottom position for proper placement (only used in non-embedded mode)\n const bottomOffset = isMobile ? 100 : 120;\n\n // Embedded mode: relative positioning inside parent container\n // Non-embedded: absolute positioning for standalone use\n const containerStyle: React.CSSProperties = embedded\n ? {\n position: \"relative\",\n display: \"flex\",\n justifyContent: layout.shouldScroll ? \"flex-start\" : \"center\",\n width: \"100%\",\n maxWidth: \"100%\",\n height: `${layout.cardHeight * layout.visualScale}px`,\n pointerEvents: \"auto\",\n overflowX: layout.shouldScroll ? \"auto\" : \"visible\",\n overflowY: \"visible\",\n // iOS momentum + scroll-snap for tactile feel\n scrollSnapType: layout.shouldScroll ? \"x proximity\" : undefined,\n WebkitOverflowScrolling: layout.shouldScroll ? \"touch\" : undefined,\n }\n : {\n position: \"absolute\",\n left: \"50%\",\n bottom: `${bottomOffset}px`,\n transform: \"translateX(-50%)\",\n width: `${layout.totalWidth}px`,\n height: `${layout.cardHeight}px`,\n display: \"flex\",\n gap: `${layout.gap}px`,\n pointerEvents: \"auto\",\n zIndex: 100,\n };\n\n return (\n <>\n {/* Technique Bar Container */}\n <div style={containerStyle} data-testid=\"technique-bar\">\n <div\n style={{\n display: \"flex\",\n gap: `${layout.gap}px`,\n justifyContent: \"center\",\n transform: embedded ? `scale(${layout.visualScale})` : undefined,\n transformOrigin: embedded\n ? layout.shouldScroll\n ? \"left center\"\n : \"center bottom\"\n : undefined,\n paddingInline: layout.shouldScroll ? \"8px\" : undefined,\n flexShrink: 0,\n }}\n >\n {techniques.map((technique, index) => {\n const cooldownRemaining = cooldowns.get(technique.id) ?? 0;\n const available = isAvailable(technique);\n\n return (\n <div\n key={technique.id}\n data-testid={`technique-slot-${index}`}\n style={layout.shouldScroll ? { scrollSnapAlign: \"start\" } : undefined}\n >\n <TechniqueCard\n technique={technique}\n isSelected={selectedIndex === index}\n isAvailable={available}\n staminaCost={technique.staminaCost}\n kiCost={technique.kiCost}\n remainingCooldown={cooldownRemaining}\n keyboardShortcut={technique.keyboardShortcut}\n onClick={() => onTechniqueSelect(index)}\n onHover={onTechniqueHover}\n isMobile={isMobile}\n playerArchetype={player.archetype}\n playerStance={player.currentStance}\n />\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Keyboard Hints - only shown in embedded mode or non-mobile */}\n {!isMobile && (\n <div\n style={{\n position: embedded ? \"relative\" : \"absolute\",\n left: embedded ? undefined : \"50%\",\n bottom: embedded\n ? undefined\n : `${bottomOffset - layout.cardHeight - 20}px`,\n transform: embedded ? undefined : \"translateX(-50%)\",\n width: embedded ? \"100%\" : `${layout.totalWidth}px`,\n textAlign: \"center\",\n fontSize: \"11px\",\n color: \"#aaa\",\n fontFamily: \"monospace\",\n pointerEvents: \"none\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n marginTop: embedded ? \"8px\" : undefined,\n }}\n >\n 기술 실행: Q-E-R-T-Y-F-G-Z-X-C | Press technique keys to execute\n </div>\n )}\n </>\n );\n};\n\nexport default TechniqueBar;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TechniqueCard.js","names":[],"sources":["../../../../../src/components/shared/three/ui/TechniqueCard.tsx"],"sourcesContent":["/**\n * TechniqueCard Component\n *\n * **Korean**: 기술 카드 컴포넌트 (Technique Card Component)\n *\n * Individual technique card displaying technique name, stamina cost, keyboard shortcut,\n * and availability state. Shows detailed tooltip on hover/focus with technique description.\n *\n * Uses Html overlay from @react-three/drei for positioning over 3D scene.\n *\n * @module components/shared/three/ui/TechniqueCard\n * @category Shared UI\n * @korean 기술카드\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { Technique } from \"../../../../types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { triggerHaptic } from \"../../../../utils/haptics\";\nimport { PlayerArchetype, TrigramStance } from \"../../../../types/common\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { AnimationType } from \"../../../../systems/animation\";\nimport \"./HUDAnimations.css\";\n\n/**\n * Props for TechniqueCard component.\n */\nexport interface TechniqueCardProps {\n /** Technique to display */\n readonly technique: Technique;\n\n /** Whether technique is currently selected */\n readonly isSelected: boolean;\n\n /** Whether technique is available (sufficient resources and no cooldown) */\n readonly isAvailable: boolean;\n\n /** Stamina cost percentage (0-100) */\n readonly staminaCost: number;\n\n /** Ki cost percentage (0-100) */\n readonly kiCost: number;\n\n /** Remaining cooldown in milliseconds */\n readonly remainingCooldown?: number;\n\n /** Keyboard shortcut key */\n readonly keyboardShortcut: string;\n\n /** Click handler */\n readonly onClick: () => void;\n\n /** Hover handler */\n readonly onHover: (technique: Technique | null) => void;\n\n /** Whether rendering for mobile device */\n readonly isMobile: boolean;\n\n /** Player archetype for reach calculation (optional) */\n readonly playerArchetype?: PlayerArchetype;\n\n /** Player stance for reach calculation (optional) */\n readonly playerStance?: TrigramStance;\n}\n\n/**\n * TechniqueCard Component\n *\n * Displays a single technique card with Korean/English names, resource costs,\n * keyboard shortcut, and availability indicators.\n *\n * @param props - Component props\n * @returns TechniqueCard component\n */\nexport const TechniqueCard: React.FC<TechniqueCardProps> = ({\n technique,\n isSelected,\n isAvailable,\n staminaCost,\n kiCost,\n remainingCooldown,\n keyboardShortcut,\n onClick,\n onHover,\n isMobile,\n playerArchetype,\n playerStance,\n}) => {\n const [showTooltip, setShowTooltip] = useState(false);\n\n // Calculate effective reach if player info is available\n const reachInfo = useMemo(() => {\n if (!playerArchetype || !playerStance || !technique.animation?.type) {\n return null;\n }\n\n // Map TechniqueAnimationConfig.type to AnimationType\n // For now, use a simple default mapping\n // TODO: Create proper mapping from AttackAnimationType to AnimationType\n const animationType = AnimationType.JAB; // Default fallback\n\n const physicalAttributes = getArchetypePhysicalAttributes(playerArchetype);\n const maxReach = physicalReachCalculator.calculateMaxReach(\n physicalAttributes,\n animationType,\n playerStance\n );\n\n // Determine body part from technique type using PhysicalReachCalculator\n const techniqueType = physicalReachCalculator.getTechniqueTypeFromAnimation(animationType);\n let bodyPart: string;\n \n switch (techniqueType) {\n case \"punch\":\n case \"elbow\":\n bodyPart = \"Arm (팔)\";\n break;\n case \"kick\":\n case \"knee\":\n bodyPart = \"Leg (다리)\";\n break;\n case \"pressure_point\":\n default:\n bodyPart = \"Body (몸통)\";\n break;\n }\n\n return {\n maxReach: (maxReach * 100).toFixed(1), // Convert to cm\n bodyPart,\n };\n }, [playerArchetype, playerStance, technique.animation]);\n\n // Calculate card size based on device\n const cardSize = useMemo(\n () => ({\n width: isMobile ? 70 : 90,\n height: isMobile ? 80 : 100,\n fontSize: isMobile ? 10 : 12,\n shortcutSize: isMobile ? 16 : 20,\n }),\n [isMobile]\n );\n\n // Format cooldown time\n const cooldownText = useMemo(() => {\n if (!remainingCooldown || remainingCooldown <= 0) return null;\n const seconds = Math.ceil(remainingCooldown / 1000);\n return `${seconds}s`;\n }, [remainingCooldown]);\n\n // Card background color based on state\n const backgroundColor = useMemo(() => {\n if (!isAvailable) return hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_LIGHT, 0.8);\n if (isSelected) return hexToRgbaString(KOREAN_COLORS.NEON_CYAN, 0.3);\n return hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9);\n }, [isAvailable, isSelected]);\n\n // Border color based on state\n const borderColor = useMemo(() => {\n if (!isAvailable) return hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT);\n if (isSelected) return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n }, [isAvailable, isSelected]);\n\n // Pre-computed hex color strings for styling\n const primaryCyanHex = useMemo(\n () => hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n []\n );\n const accentGoldHex = useMemo(\n () => hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD),\n []\n );\n\n // Border glow effect for selected card\n const boxShadow = useMemo(() => {\n if (isSelected && isAvailable) {\n return `0 0 15px ${hexToRgbaString(KOREAN_COLORS.NEON_CYAN, 0.8)}, 0 0 25px ${hexToRgbaString(KOREAN_COLORS.NEON_CYAN, 0.5)}`;\n }\n if (isAvailable) {\n return `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}, 0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK, 0.5)}`;\n }\n return `0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK, 0.5)}`;\n }, [isSelected, isAvailable]);\n\n // Animation class based on availability state\n const animationClass = useMemo(\n () => (isAvailable ? \"hud-animated\" : \"\"),\n [isAvailable]\n );\n\n // Touch handler for mobile - provides immediate response without 300ms delay\n const handleTouch = useCallback(\n (e: React.TouchEvent) => {\n if (!isAvailable) return;\n e.preventDefault(); // Prevent ghost click on mobile\n triggerHaptic(\"light\");\n onClick();\n },\n [isAvailable, onClick]\n );\n\n return (\n <div\n role=\"button\"\n tabIndex={isAvailable ? 0 : -1}\n aria-label={`${technique.name.korean} (${technique.name.english}). Stamina: ${staminaCost}, Ki: ${kiCost}`}\n aria-disabled={!isAvailable}\n aria-describedby={showTooltip && isAvailable ? `tooltip-${technique.id}` : undefined}\n className={animationClass}\n style={{\n position: \"relative\",\n width: `${cardSize.width}px`,\n height: `${cardSize.height}px`,\n backgroundColor,\n border: `2px solid ${borderColor}`,\n borderRadius: \"8px\",\n boxShadow,\n cursor: isAvailable ? \"pointer\" : \"not-allowed\",\n transition: \"all 0.2s ease-in-out, transform 0.15s ease-out\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"6px\",\n fontFamily: FONT_FAMILY.KOREAN,\n opacity: isAvailable ? 1 : 0.5,\n touchAction: \"manipulation\", // Disable double-tap zoom\n userSelect: \"none\", // Prevent text selection on touch\n animation: isSelected && isAvailable ? \"techniqueSelected 1.5s ease-in-out infinite\" : \n isAvailable ? \"techniqueGlow 2s ease-in-out infinite\" : \"none\",\n }}\n onClick={isAvailable ? onClick : undefined}\n onTouchEnd={handleTouch}\n onMouseEnter={() => {\n setShowTooltip(true);\n onHover(technique);\n }}\n onMouseLeave={() => {\n setShowTooltip(false);\n onHover(null);\n }}\n onFocus={() => {\n setShowTooltip(true);\n onHover(technique);\n }}\n onBlur={() => {\n setShowTooltip(false);\n onHover(null);\n }}\n data-testid={`technique-card-${technique.id}`}\n >\n {/* Keyboard Shortcut */}\n <div\n style={{\n position: \"absolute\",\n top: \"4px\",\n right: \"4px\",\n width: `${cardSize.shortcutSize}px`,\n height: `${cardSize.shortcutSize}px`,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n border: `1px solid ${hexColorToCSS(KOREAN_COLORS.UI_GRAY)}`,\n borderRadius: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: `${cardSize.fontSize}px`,\n fontWeight: \"bold\",\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n }}\n >\n {keyboardShortcut}\n </div>\n\n {/* Technique Name (Korean) */}\n <div\n style={{\n fontSize: `${cardSize.fontSize}px`,\n fontWeight: \"bold\",\n color: isAvailable ? accentGoldHex : hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n textAlign: \"center\",\n marginTop: \"20px\",\n lineHeight: \"1.2\",\n }}\n >\n {technique.name.korean}\n </div>\n\n {/* Technique Name (English) */}\n <div\n style={{\n fontSize: `${cardSize.fontSize - 2}px`,\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n textAlign: \"center\",\n marginTop: \"2px\",\n lineHeight: \"1.1\",\n }}\n >\n {technique.name.english}\n </div>\n\n {/* Resource Costs */}\n <div\n style={{\n display: \"flex\",\n gap: \"8px\",\n marginTop: \"auto\",\n fontSize: `${cardSize.fontSize - 2}px`,\n }}\n >\n {/* Stamina Cost */}\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"2px\",\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n }}\n >\n <span>⚡</span>\n <span>{staminaCost}</span>\n </div>\n\n {/* Ki Cost */}\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"2px\",\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.NEON_CYAN) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n }}\n >\n <span>氣</span>\n <span>{kiCost}</span>\n </div>\n </div>\n\n {/* Cooldown Overlay */}\n {cooldownText && (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"6px\",\n fontSize: `${cardSize.shortcutSize}px`,\n fontWeight: \"bold\",\n color: hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED),\n }}\n >\n {cooldownText}\n </div>\n )}\n\n {/* Tooltip */}\n {showTooltip && isAvailable && (\n <div\n id={`tooltip-${technique.id}`}\n role=\"tooltip\"\n style={{\n position: \"absolute\",\n bottom: `${cardSize.height + 10}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n minWidth: \"200px\",\n maxWidth: \"300px\",\n padding: \"10px\",\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${primaryCyanHex}`,\n borderRadius: \"8px\",\n fontSize: \"12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n zIndex: 1000,\n pointerEvents: \"none\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n <div\n style={{\n fontWeight: \"bold\",\n marginBottom: \"6px\",\n color: accentGoldHex,\n }}\n >\n {technique.name.korean} | {technique.name.english}\n </div>\n <div\n style={{ fontSize: \"11px\", lineHeight: \"1.4\", marginBottom: \"8px\" }}\n >\n {technique.description.korean}\n </div>\n <div style={{ fontSize: \"11px\", lineHeight: \"1.4\", color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n {technique.description.english}\n </div>\n <div style={{ marginTop: \"8px\", fontSize: \"10px\", color: hexColorToCSS(KOREAN_COLORS.TEXT_TERTIARY) }}>\n <div>\n Damage: {technique.damage.min}-{technique.damage.max}\n </div>\n <div>Cooldown: {technique.cooldown / 1000}s</div>\n {technique.requiredStance && (\n <div>Stance: {technique.requiredStance}</div>\n )}\n {reachInfo && (\n <>\n <div style={{ marginTop: \"4px\", color: primaryCyanHex, fontWeight: \"bold\" }}>\n Reach: {reachInfo.maxReach}cm\n </div>\n <div style={{ fontSize: \"9px\", color: hexColorToCSS(KOREAN_COLORS.UI_GRAY) }}>\n {reachInfo.bodyPart}\n </div>\n </>\n )}\n </div>\n </div>\n )}\n </div>\n );\n};\n\nexport default TechniqueCard;\n"],"mappings":"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"}
1
+ {"version":3,"file":"TechniqueCard.js","names":[],"sources":["../../../../../src/components/shared/three/ui/TechniqueCard.tsx"],"sourcesContent":["/**\n * TechniqueCard Component\n *\n * **Korean**: 기술 카드 컴포넌트 (Technique Card Component)\n *\n * Individual technique card displaying technique name, stamina cost, keyboard shortcut,\n * and availability state. Shows detailed tooltip on hover/focus with technique description.\n *\n * Uses Html overlay from @react-three/drei for positioning over 3D scene.\n *\n * @module components/shared/three/ui/TechniqueCard\n * @category Shared UI\n * @korean 기술카드\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { Technique } from \"../../../../types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { triggerHaptic } from \"../../../../utils/haptics\";\nimport { PlayerArchetype, TrigramStance } from \"../../../../types/common\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { AnimationType } from \"../../../../systems/animation\";\nimport \"./HUDAnimations.css\";\n\n/**\n * Props for TechniqueCard component.\n */\nexport interface TechniqueCardProps {\n /** Technique to display */\n readonly technique: Technique;\n\n /** Whether technique is currently selected */\n readonly isSelected: boolean;\n\n /** Whether technique is available (sufficient resources and no cooldown) */\n readonly isAvailable: boolean;\n\n /** Stamina cost percentage (0-100) */\n readonly staminaCost: number;\n\n /** Ki cost percentage (0-100) */\n readonly kiCost: number;\n\n /** Remaining cooldown in milliseconds */\n readonly remainingCooldown?: number;\n\n /** Keyboard shortcut key */\n readonly keyboardShortcut: string;\n\n /** Click handler */\n readonly onClick: () => void;\n\n /** Hover handler */\n readonly onHover: (technique: Technique | null) => void;\n\n /** Whether rendering for mobile device */\n readonly isMobile: boolean;\n\n /** Player archetype for reach calculation (optional) */\n readonly playerArchetype?: PlayerArchetype;\n\n /** Player stance for reach calculation (optional) */\n readonly playerStance?: TrigramStance;\n}\n\n/**\n * TechniqueCard Component\n *\n * Displays a single technique card with Korean/English names, resource costs,\n * keyboard shortcut, and availability indicators.\n *\n * @param props - Component props\n * @returns TechniqueCard component\n */\nexport const TechniqueCard: React.FC<TechniqueCardProps> = ({\n technique,\n isSelected,\n isAvailable,\n staminaCost,\n kiCost,\n remainingCooldown,\n keyboardShortcut,\n onClick,\n onHover,\n isMobile,\n playerArchetype,\n playerStance,\n}) => {\n const [showTooltip, setShowTooltip] = useState(false);\n\n // Calculate effective reach if player info is available\n const reachInfo = useMemo(() => {\n if (!playerArchetype || !playerStance || !technique.animation?.type) {\n return null;\n }\n\n // Map TechniqueAnimationConfig.type to AnimationType\n // For now, use a simple default mapping\n // TODO: Create proper mapping from AttackAnimationType to AnimationType\n const animationType = AnimationType.JAB; // Default fallback\n\n const physicalAttributes = getArchetypePhysicalAttributes(playerArchetype);\n const maxReach = physicalReachCalculator.calculateMaxReach(\n physicalAttributes,\n animationType,\n playerStance\n );\n\n // Determine body part from technique type using PhysicalReachCalculator\n const techniqueType = physicalReachCalculator.getTechniqueTypeFromAnimation(animationType);\n let bodyPart: string;\n \n switch (techniqueType) {\n case \"punch\":\n case \"elbow\":\n bodyPart = \"Arm (팔)\";\n break;\n case \"kick\":\n case \"knee\":\n bodyPart = \"Leg (다리)\";\n break;\n case \"pressure_point\":\n default:\n bodyPart = \"Body (몸통)\";\n break;\n }\n\n return {\n maxReach: (maxReach * 100).toFixed(1), // Convert to cm\n bodyPart,\n };\n }, [playerArchetype, playerStance, technique.animation]);\n\n // Calculate card size based on device\n const cardSize = useMemo(\n () => ({\n width: isMobile ? 70 : 90,\n height: isMobile ? 80 : 100,\n fontSize: isMobile ? 10 : 12,\n shortcutSize: isMobile ? 16 : 20,\n }),\n [isMobile]\n );\n\n // Format cooldown time\n const cooldownText = useMemo(() => {\n if (!remainingCooldown || remainingCooldown <= 0) return null;\n const seconds = Math.ceil(remainingCooldown / 1000);\n return `${seconds}s`;\n }, [remainingCooldown]);\n\n // Card background color based on state\n const backgroundColor = useMemo(() => {\n if (!isAvailable) return hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_LIGHT, 0.8);\n if (isSelected) return hexToRgbaString(KOREAN_COLORS.NEON_CYAN, 0.3);\n return hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.9);\n }, [isAvailable, isSelected]);\n\n // Border color based on state\n const borderColor = useMemo(() => {\n if (!isAvailable) return hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT);\n if (isSelected) return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n }, [isAvailable, isSelected]);\n\n // Pre-computed hex color strings for styling\n const primaryCyanHex = useMemo(\n () => hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n []\n );\n const accentGoldHex = useMemo(\n () => hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD),\n []\n );\n\n // Border glow effect for selected card\n const boxShadow = useMemo(() => {\n if (isSelected && isAvailable) {\n return `0 0 15px ${hexToRgbaString(KOREAN_COLORS.NEON_CYAN, 0.8)}, 0 0 25px ${hexToRgbaString(KOREAN_COLORS.NEON_CYAN, 0.5)}`;\n }\n if (isAvailable) {\n return `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}, 0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK, 0.5)}`;\n }\n return `0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK, 0.5)}`;\n }, [isSelected, isAvailable]);\n\n // Animation class based on availability state\n const animationClass = useMemo(\n () => (isAvailable ? \"hud-animated\" : \"\"),\n [isAvailable]\n );\n\n // Touch handler for mobile - provides immediate response without 300ms delay\n const handleTouch = useCallback(\n (e: React.TouchEvent) => {\n if (!isAvailable) return;\n e.preventDefault(); // Prevent ghost click on mobile\n triggerHaptic(\"light\");\n onClick();\n },\n [isAvailable, onClick]\n );\n\n return (\n <div\n role=\"button\"\n tabIndex={isAvailable ? 0 : -1}\n aria-label={`${technique.name.korean} (${technique.name.english}). Stamina: ${staminaCost}, Ki: ${kiCost}`}\n aria-disabled={!isAvailable}\n aria-describedby={showTooltip && isAvailable ? `tooltip-${technique.id}` : undefined}\n className={animationClass}\n style={{\n position: \"relative\",\n width: `${cardSize.width}px`,\n height: `${cardSize.height}px`,\n backgroundColor,\n border: `2px solid ${borderColor}`,\n borderRadius: \"8px\",\n boxShadow,\n cursor: isAvailable ? \"pointer\" : \"not-allowed\",\n transition: \"all 0.2s ease-in-out, transform 0.15s ease-out\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n padding: \"6px\",\n fontFamily: FONT_FAMILY.KOREAN,\n opacity: isAvailable ? 1 : 0.5,\n touchAction: \"manipulation\", // Disable double-tap zoom\n userSelect: \"none\", // Prevent text selection on touch\n animation: isSelected && isAvailable ? \"techniqueSelected 1.5s ease-in-out infinite\" : \n isAvailable ? \"techniqueGlow 2s ease-in-out infinite\" : \"none\",\n }}\n onClick={isAvailable ? onClick : undefined}\n onTouchEnd={handleTouch}\n onMouseEnter={() => {\n setShowTooltip(true);\n onHover(technique);\n }}\n onMouseLeave={() => {\n setShowTooltip(false);\n onHover(null);\n }}\n onFocus={() => {\n setShowTooltip(true);\n onHover(technique);\n }}\n onBlur={() => {\n setShowTooltip(false);\n onHover(null);\n }}\n data-testid={`technique-card-${technique.id}`}\n >\n {/* Keyboard Shortcut */}\n <div\n style={{\n position: \"absolute\",\n top: \"4px\",\n right: \"4px\",\n width: `${cardSize.shortcutSize}px`,\n height: `${cardSize.shortcutSize}px`,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n border: `1px solid ${hexColorToCSS(KOREAN_COLORS.UI_GRAY)}`,\n borderRadius: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: `${cardSize.fontSize}px`,\n fontWeight: \"bold\",\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n }}\n >\n {keyboardShortcut}\n </div>\n\n {/* Technique Name (Korean) */}\n <div\n style={{\n fontSize: `${cardSize.fontSize}px`,\n fontWeight: \"bold\",\n color: isAvailable ? accentGoldHex : hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n textAlign: \"center\",\n marginTop: \"20px\",\n lineHeight: \"1.2\",\n }}\n >\n {technique.name.korean}\n </div>\n\n {/* Technique Name (English) */}\n <div\n style={{\n fontSize: `${cardSize.fontSize - 2}px`,\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n textAlign: \"center\",\n marginTop: \"2px\",\n lineHeight: \"1.1\",\n }}\n >\n {technique.name.english}\n </div>\n\n {/* Resource Costs */}\n <div\n style={{\n display: \"flex\",\n gap: \"8px\",\n marginTop: \"auto\",\n fontSize: `${cardSize.fontSize - 2}px`,\n }}\n >\n {/* Stamina Cost */}\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"2px\",\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n }}\n >\n <span>⚡</span>\n <span>{staminaCost}</span>\n </div>\n\n {/* Ki Cost */}\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"2px\",\n color: isAvailable ? hexColorToCSS(KOREAN_COLORS.NEON_CYAN) : hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT),\n }}\n >\n <span>氣</span>\n <span>{kiCost}</span>\n </div>\n </div>\n\n {/* Cooldown Overlay */}\n {cooldownText && (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"6px\",\n fontSize: `${cardSize.shortcutSize}px`,\n fontWeight: \"bold\",\n color: hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED),\n }}\n >\n {cooldownText}\n </div>\n )}\n\n {/* Tooltip */}\n {showTooltip && isAvailable && (\n <div\n id={`tooltip-${technique.id}`}\n role=\"tooltip\"\n style={{\n position: \"absolute\",\n bottom: `${cardSize.height + 10}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n minWidth: \"200px\",\n maxWidth: \"300px\",\n padding: \"10px\",\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95),\n border: `2px solid ${primaryCyanHex}`,\n borderRadius: \"8px\",\n fontSize: \"12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n zIndex: 1000,\n pointerEvents: \"none\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n <div\n style={{\n fontWeight: \"bold\",\n marginBottom: \"6px\",\n color: accentGoldHex,\n }}\n >\n {technique.name.korean} | {technique.name.english}\n </div>\n <div\n style={{ fontSize: \"11px\", lineHeight: \"1.4\", marginBottom: \"8px\" }}\n >\n {technique.description.korean}\n </div>\n <div style={{ fontSize: \"11px\", lineHeight: \"1.4\", color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY) }}>\n {technique.description.english}\n </div>\n <div style={{ marginTop: \"8px\", fontSize: \"10px\", color: hexColorToCSS(KOREAN_COLORS.TEXT_TERTIARY) }}>\n <div>\n Damage: {technique.damage.min}-{technique.damage.max}\n </div>\n <div>Cooldown: {technique.cooldown / 1000}s</div>\n {technique.requiredStance && (\n <div>Stance: {technique.requiredStance}</div>\n )}\n {reachInfo && (\n <>\n <div style={{ marginTop: \"4px\", color: primaryCyanHex, fontWeight: \"bold\" }}>\n Reach: {reachInfo.maxReach}cm\n </div>\n <div style={{ fontSize: \"9px\", color: hexColorToCSS(KOREAN_COLORS.UI_GRAY) }}>\n {reachInfo.bodyPart}\n </div>\n </>\n )}\n </div>\n </div>\n )}\n </div>\n );\n};\n\nexport default TechniqueCard;\n"],"mappings":"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"}