blacktrigram 0.7.68 → 0.7.69

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 (219) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/assets/index.css +987 -987
  3. package/lib/audio/AudioAssetLoader.js.map +1 -1
  4. package/lib/audio/AudioAssetRegistry.js +2 -2
  5. package/lib/audio/AudioAssetRegistry.js.map +1 -1
  6. package/lib/audio/AudioCache.js.map +1 -1
  7. package/lib/audio/AudioManager.js +1 -1
  8. package/lib/audio/AudioManager.js.map +1 -1
  9. package/lib/audio/AudioMonitor.js.map +1 -1
  10. package/lib/audio/AudioPool.js.map +1 -1
  11. package/lib/audio/AudioProvider.js.map +1 -1
  12. package/lib/audio/AudioUtils.js.map +1 -1
  13. package/lib/audio/BoneImpactAudioMap.js.map +1 -1
  14. package/lib/components/screens/combat/CombatScreen3D.js +3 -3
  15. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  18. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  19. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  20. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  25. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  26. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  27. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  28. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  29. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  30. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  31. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  32. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  33. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  34. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  35. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  36. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  37. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  38. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  39. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  40. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  41. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  42. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  43. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  44. package/lib/components/screens/training/TrainingScreen3D.js +1 -1
  45. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  46. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  47. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  48. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  49. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  50. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  51. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  52. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  53. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  54. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  55. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  56. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  57. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  58. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  59. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  60. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  61. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  62. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  63. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  64. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  65. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  66. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  67. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  68. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  69. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  70. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  71. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  72. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  73. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  74. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  75. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  76. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  77. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  78. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  79. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  80. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  81. package/lib/components/shared/ui/SplashScreen.js +2 -2
  82. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  83. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  84. package/lib/data/techniques.js.map +1 -1
  85. package/lib/hooks/useCombatTimer.js.map +1 -1
  86. package/lib/hooks/useKeyboardControls.js.map +1 -1
  87. package/lib/hooks/useSkeletalAnimation.js +2 -2
  88. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  89. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  90. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  91. package/lib/index.js +2 -2
  92. package/lib/systems/CombatSystem.js.map +1 -1
  93. package/lib/systems/EffectCalculator.js.map +1 -1
  94. package/lib/systems/ResponsiveScaling.js.map +1 -1
  95. package/lib/systems/TrigramSystem.js.map +1 -1
  96. package/lib/systems/ai/AIPersonality.js.map +1 -1
  97. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  98. package/lib/systems/ai/DecisionTree.js.map +1 -1
  99. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  100. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  101. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  102. package/lib/systems/animation/builders/SkeletonRig.js +1 -1
  103. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  104. package/lib/systems/animation/catalogs/AttackAnimations.js +1 -1
  105. package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
  106. package/lib/systems/animation/catalogs/BasicAnimations.js +1 -1
  107. package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
  108. package/lib/systems/animation/catalogs/ComboAnimations.js +1 -1
  109. package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
  110. package/lib/systems/animation/catalogs/DarkOpsAnimations.js +1 -1
  111. package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
  112. package/lib/systems/animation/catalogs/DefensiveAnimations.js +2 -11
  113. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  114. package/lib/systems/animation/catalogs/ElbowKneeAnimations.js +1 -1
  115. package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
  116. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js +1 -1
  117. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  118. package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
  119. package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -1
  120. package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
  121. package/lib/systems/animation/catalogs/GanStanceAnimations.js +1 -1
  122. package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
  123. package/lib/systems/animation/catalogs/GanTechniqueAnimations.js +1 -1
  124. package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
  125. package/lib/systems/animation/catalogs/GeonStanceAnimations.js +1 -1
  126. package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
  127. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +1 -1
  128. package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -1
  129. package/lib/systems/animation/catalogs/GrapplingAnimations.js +1 -1
  130. package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
  131. package/lib/systems/animation/catalogs/JinStanceAnimations.js +1 -1
  132. package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
  133. package/lib/systems/animation/catalogs/JinTechniqueAnimations.js +1 -1
  134. package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
  135. package/lib/systems/animation/catalogs/KickAnimations.js +1 -1
  136. package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
  137. package/lib/systems/animation/catalogs/LiStanceAnimations.js +1 -1
  138. package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
  139. package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
  140. package/lib/systems/animation/catalogs/MovementAnimations.js +1 -1
  141. package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
  142. package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
  143. package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
  144. package/lib/systems/animation/catalogs/SonStanceAnimations.js +1 -1
  145. package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
  146. package/lib/systems/animation/catalogs/SonTechniqueAnimations.js +1 -1
  147. package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
  148. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js +1 -1
  149. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
  150. package/lib/systems/animation/catalogs/StanceAnimations.js +1 -1
  151. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  152. package/lib/systems/animation/catalogs/StanceAttackAnimations.js +215 -272
  153. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  154. package/lib/systems/animation/catalogs/StanceIdleAnimations.js +2 -11
  155. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  156. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js +2 -2
  157. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  158. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js +1 -1
  159. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  160. package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
  161. package/lib/systems/animation/catalogs/TaeStanceAnimations.js +1 -1
  162. package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
  163. package/lib/systems/animation/core/AnimationOptimizations.js +1 -1
  164. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  165. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  166. package/lib/systems/animation/core/AnimationRegistry.js +11 -209
  167. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  168. package/lib/systems/animation/core/AnimationStateMachine.js +2 -2
  169. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  170. package/lib/systems/animation/core/TechniqueAnimationMapper.js +1 -1
  171. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  172. package/lib/systems/animation/core/TechniqueAnimationMapping.js +1 -1
  173. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  174. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  175. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  176. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  177. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  178. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  179. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  180. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  181. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  182. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  183. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  184. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  185. package/lib/systems/combat/typeGuards.js.map +1 -1
  186. package/lib/systems/index.js +2 -2
  187. package/lib/systems/movement/integration.js.map +1 -1
  188. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  189. package/lib/systems/physics/CollisionDetection.js +1 -1
  190. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  191. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  192. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  193. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  194. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  195. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  196. package/lib/systems/trigram/StanceManager.js.map +1 -1
  197. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  198. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  199. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  200. package/lib/systems/trigram/techniques/index.js.map +1 -1
  201. package/lib/systems/trigram/types.js.map +1 -1
  202. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  203. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  204. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  205. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  206. package/lib/types/constants/designSystem.js.map +1 -1
  207. package/lib/utils/colorHelpers.js.map +1 -1
  208. package/lib/utils/colorUtils.js.map +1 -1
  209. package/lib/utils/controlMapping.js.map +1 -1
  210. package/lib/utils/deviceDetection.js.map +1 -1
  211. package/lib/utils/effectUtils.js.map +1 -1
  212. package/lib/utils/fabricTextures.js.map +1 -1
  213. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  214. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  215. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  216. package/lib/utils/safeAreaUtils.js.map +1 -1
  217. package/lib/utils/stanceHelpers.js.map +1 -1
  218. package/lib/utils/visualEffects.js.map +1 -1
  219. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"BodyPartHealthDisplay.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BodyPartHealthDisplay.tsx"],"sourcesContent":["/**\n * BodyPartHealthDisplay Component - Individual body part health visualization\n *\n * Displays health bars for all 8 body parts:\n * - Head (두부)\n * - Neck (경부)\n * - Torso Upper (상부 몸통)\n * - Torso Lower (하부 몸통)\n * - Left Arm (좌팔)\n * - Right Arm (우팔)\n * - Left Leg (좌다리)\n * - Right Leg (우다리)\n *\n * @module components/shared/three/ui/BodyPartHealthDisplay\n * @category Shared UI\n * @korean 신체부위체력표시\n */\n\nimport React, { useMemo } from \"react\";\nimport { BodyPart, BodyPartHealth } from \"../../../../systems/bodypart/types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\nexport interface BodyPartHealthDisplayProps {\n /** Body part health data */\n readonly bodyPartHealth: BodyPartHealth;\n /** Player identifier for test IDs */\n readonly playerId: string;\n /** 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}\n\n/**\n * Body part names in Korean and English\n */\nconst BODY_PART_NAMES: Record<BodyPart, { korean: string; english: string }> = {\n [BodyPart.HEAD]: { korean: \"두부\", english: \"Head\" },\n [BodyPart.NECK]: { korean: \"경부\", english: \"Neck\" },\n [BodyPart.TORSO_UPPER]: { korean: \"상부\", english: \"Upper\" },\n [BodyPart.TORSO_LOWER]: { korean: \"하부\", english: \"Lower\" },\n [BodyPart.ARM_LEFT]: { korean: \"좌팔\", english: \"L.Arm\" },\n [BodyPart.ARM_RIGHT]: { korean: \"우팔\", english: \"R.Arm\" },\n [BodyPart.LEG_LEFT]: { korean: \"좌다리\", english: \"L.Leg\" },\n [BodyPart.LEG_RIGHT]: { korean: \"우다리\", english: \"R.Leg\" },\n};\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (health: number): number => {\n if (health >= 80) return KOREAN_COLORS.HEALTH_FULL;\n if (health >= 60) return KOREAN_COLORS.ACCENT_GOLD;\n if (health >= 40) return KOREAN_COLORS.WARNING_ORANGE;\n if (health >= 20) return KOREAN_COLORS.PAIN_INDICATOR;\n return KOREAN_COLORS.NEGATIVE_RED_DARK;\n};\n\n/**\n * BodyPartHealthDisplay - Shows health bars for all 8 body parts\n *\n * @example\n * ```tsx\n * <BodyPartHealthDisplay\n * bodyPartHealth={player.bodyPartHealth}\n * playerId=\"player-1\"\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BodyPartHealthDisplay: React.FC<BodyPartHealthDisplayProps> = ({\n bodyPartHealth,\n playerId,\n position,\n isMobile,\n}) => {\n const isLeft = position === \"left\";\n\n const barWidth = isMobile ? 100 : 140;\n const barHeight = isMobile ? 8 : 10;\n const fontSize = isMobile ? 9 : 10;\n const gap = isMobile ? \"4px\" : \"5px\";\n\n const bodyPartGroups = useMemo(\n () => [\n {\n label: \"상체 | Upper\",\n parts: [\n BodyPart.HEAD,\n BodyPart.NECK,\n BodyPart.TORSO_UPPER,\n BodyPart.TORSO_LOWER,\n ],\n },\n { label: \"팔 | Arms\", parts: [BodyPart.ARM_LEFT, BodyPart.ARM_RIGHT] },\n { label: \"다리 | Legs\", parts: [BodyPart.LEG_LEFT, BodyPart.LEG_RIGHT] },\n ],\n [],\n );\n\n return (\n <div\n data-testid={`body-part-health-${playerId}`}\n style={{\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n borderRadius: \"8px\",\n padding: isMobile ? \"6px\" : \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)}`,\n boxShadow: `0 0 12px ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n pointerEvents: \"none\",\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n fontSize: isMobile ? \"9px\" : \"10px\",\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: isLeft ? \"left\" : \"right\",\n marginBottom: \"2px\",\n }}\n >\n 신체 | Body Parts\n </div>\n\n {/* Body part groups */}\n {bodyPartGroups.map((group) => (\n <div\n key={group.label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"3px\",\n }}\n >\n {/* Group label - optional, can be hidden for compact display */}\n <div\n style={{\n fontSize: `${fontSize - 1}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 0.8),\n textAlign: isLeft ? \"left\" : \"right\",\n marginTop: group.label === bodyPartGroups[0].label ? \"0\" : \"4px\",\n }}\n >\n {group.label}\n </div>\n\n {/* Body parts in group */}\n {group.parts.map((part) => {\n const health = bodyPartHealth[part];\n const names = BODY_PART_NAMES[part];\n const healthColor = getHealthColor(health);\n const shouldPulse = health < 20;\n\n return (\n <div\n key={part}\n data-testid={`body-part-${playerId}-${part}`}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Body part name and value */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(healthColor, 1),\n }}\n >\n <span style={{ fontWeight: health < 40 ? \"bold\" : \"normal\" }}>\n {isMobile\n ? names.korean\n : `${names.korean} | ${names.english}`}\n </span>\n <span style={{ fontSize: `${fontSize - 1}px` }}>\n {Math.round(health)}%\n </span>\n </div>\n\n {/* Health bar */}\n <div\n style={{\n width: `${barWidth}px`,\n height: `${barHeight}px`,\n backgroundColor: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 1,\n ),\n borderRadius: \"2px\",\n overflow: \"hidden\",\n animation: shouldPulse\n ? \"healthPulse 0.8s infinite\"\n : \"none\",\n }}\n >\n <div\n style={{\n width: `${health}%`,\n height: \"100%\",\n backgroundColor: hexToRgbaString(healthColor, 1),\n transition:\n \"width 0.3s ease-in-out, background-color 0.3s ease-in-out\",\n boxShadow: `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`,\n }}\n />\n </div>\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport default BodyPartHealthDisplay;\n"],"mappings":"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"}
1
+ {"version":3,"file":"BodyPartHealthDisplay.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BodyPartHealthDisplay.tsx"],"sourcesContent":["/**\n * BodyPartHealthDisplay Component - Individual body part health visualization\n *\n * Displays health bars for all 8 body parts:\n * - Head (두부)\n * - Neck (경부)\n * - Torso Upper (상부 몸통)\n * - Torso Lower (하부 몸통)\n * - Left Arm (좌팔)\n * - Right Arm (우팔)\n * - Left Leg (좌다리)\n * - Right Leg (우다리)\n *\n * @module components/shared/three/ui/BodyPartHealthDisplay\n * @category Shared UI\n * @korean 신체부위체력표시\n */\n\nimport React, { useMemo } from \"react\";\nimport { BodyPart, BodyPartHealth } from \"../../../../systems/bodypart/types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\nexport interface BodyPartHealthDisplayProps {\n /** Body part health data */\n readonly bodyPartHealth: BodyPartHealth;\n /** Player identifier for test IDs */\n readonly playerId: string;\n /** 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}\n\n/**\n * Body part names in Korean and English\n */\nconst BODY_PART_NAMES: Record<BodyPart, { korean: string; english: string }> = {\n [BodyPart.HEAD]: { korean: \"두부\", english: \"Head\" },\n [BodyPart.NECK]: { korean: \"경부\", english: \"Neck\" },\n [BodyPart.TORSO_UPPER]: { korean: \"상부\", english: \"Upper\" },\n [BodyPart.TORSO_LOWER]: { korean: \"하부\", english: \"Lower\" },\n [BodyPart.ARM_LEFT]: { korean: \"좌팔\", english: \"L.Arm\" },\n [BodyPart.ARM_RIGHT]: { korean: \"우팔\", english: \"R.Arm\" },\n [BodyPart.LEG_LEFT]: { korean: \"좌다리\", english: \"L.Leg\" },\n [BodyPart.LEG_RIGHT]: { korean: \"우다리\", english: \"R.Leg\" },\n};\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (health: number): number => {\n if (health >= 80) return KOREAN_COLORS.HEALTH_FULL;\n if (health >= 60) return KOREAN_COLORS.ACCENT_GOLD;\n if (health >= 40) return KOREAN_COLORS.WARNING_ORANGE;\n if (health >= 20) return KOREAN_COLORS.PAIN_INDICATOR;\n return KOREAN_COLORS.NEGATIVE_RED_DARK;\n};\n\n/**\n * BodyPartHealthDisplay - Shows health bars for all 8 body parts\n *\n * @example\n * ```tsx\n * <BodyPartHealthDisplay\n * bodyPartHealth={player.bodyPartHealth}\n * playerId=\"player-1\"\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BodyPartHealthDisplay: React.FC<BodyPartHealthDisplayProps> = ({\n bodyPartHealth,\n playerId,\n position,\n isMobile,\n}) => {\n const isLeft = position === \"left\";\n\n const barWidth = isMobile ? 100 : 140;\n const barHeight = isMobile ? 8 : 10;\n const fontSize = isMobile ? 9 : 10;\n const gap = isMobile ? \"4px\" : \"5px\";\n\n const bodyPartGroups = useMemo(\n () => [\n {\n label: \"상체 | Upper\",\n parts: [\n BodyPart.HEAD,\n BodyPart.NECK,\n BodyPart.TORSO_UPPER,\n BodyPart.TORSO_LOWER,\n ],\n },\n { label: \"팔 | Arms\", parts: [BodyPart.ARM_LEFT, BodyPart.ARM_RIGHT] },\n { label: \"다리 | Legs\", parts: [BodyPart.LEG_LEFT, BodyPart.LEG_RIGHT] },\n ],\n [],\n );\n\n return (\n <div\n data-testid={`body-part-health-${playerId}`}\n style={{\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n borderRadius: \"8px\",\n padding: isMobile ? \"6px\" : \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)}`,\n boxShadow: `0 0 12px ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n pointerEvents: \"none\",\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n fontSize: isMobile ? \"9px\" : \"10px\",\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: isLeft ? \"left\" : \"right\",\n marginBottom: \"2px\",\n }}\n >\n 신체 | Body Parts\n </div>\n\n {/* Body part groups */}\n {bodyPartGroups.map((group) => (\n <div\n key={group.label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"3px\",\n }}\n >\n {/* Group label - optional, can be hidden for compact display */}\n <div\n style={{\n fontSize: `${fontSize - 1}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 0.8),\n textAlign: isLeft ? \"left\" : \"right\",\n marginTop: group.label === bodyPartGroups[0].label ? \"0\" : \"4px\",\n }}\n >\n {group.label}\n </div>\n\n {/* Body parts in group */}\n {group.parts.map((part) => {\n const health = bodyPartHealth[part];\n const names = BODY_PART_NAMES[part];\n const healthColor = getHealthColor(health);\n const shouldPulse = health < 20;\n\n return (\n <div\n key={part}\n data-testid={`body-part-${playerId}-${part}`}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Body part name and value */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(healthColor, 1),\n }}\n >\n <span style={{ fontWeight: health < 40 ? \"bold\" : \"normal\" }}>\n {isMobile\n ? names.korean\n : `${names.korean} | ${names.english}`}\n </span>\n <span style={{ fontSize: `${fontSize - 1}px` }}>\n {Math.round(health)}%\n </span>\n </div>\n\n {/* Health bar */}\n <div\n style={{\n width: `${barWidth}px`,\n height: `${barHeight}px`,\n backgroundColor: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 1,\n ),\n borderRadius: \"2px\",\n overflow: \"hidden\",\n animation: shouldPulse\n ? \"healthPulse 0.8s infinite\"\n : \"none\",\n }}\n >\n <div\n style={{\n width: `${health}%`,\n height: \"100%\",\n backgroundColor: hexToRgbaString(healthColor, 1),\n transition:\n \"width 0.3s ease-in-out, background-color 0.3s ease-in-out\",\n boxShadow: `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`,\n }}\n />\n </div>\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport default BodyPartHealthDisplay;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CombatReadinessBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/CombatReadinessBar.tsx"],"sourcesContent":["/**\n * CombatReadinessBar Component - 10-bar segmented combat readiness display\n * \n * Displays comprehensive combat readiness with:\n * - 10 segmented bars representing 10% each\n * - Color transitions: Green (>80%), Yellow (60-79%), Orange (40-59%), Red (20-39%), Dark Red (<20%)\n * - Smooth 0.3s transition animations\n * - Korean/English bilingual labels\n * - Numeric percentage display\n * - Responsive sizing for mobile/tablet/desktop\n * - Real-time calculation from body health, pain, consciousness, and balance\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { \n calculateCombatReadiness, \n getCombatReadinessColor, \n getCombatReadinessLabel,\n getCombatReadinessBars\n} from \"../../../../utils/combatReadiness\";\nimport type { PlayerState } from \"../../../../systems/player\";\n\nexport interface CombatReadinessBarProps {\n /** Player state containing all combat factors */\n readonly player: PlayerState;\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 * CombatReadinessBar - 10-bar segmented combat readiness display with Korean theming\n * \n * Calculates and displays overall combat readiness from multiple factors:\n * - Body part health (40% weight)\n * - Pain level (20% weight)\n * - Consciousness (20% weight)\n * - Balance state (20% weight)\n * \n * @example\n * ```tsx\n * <CombatReadinessBar \n * player={playerState} \n * playerId=\"player-1\"\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReadinessBar: React.FC<CombatReadinessBarProps> = ({\n player,\n playerId,\n isMobile,\n}) => {\n const readiness = useMemo(\n () => calculateCombatReadiness(player),\n [player]\n );\n\n const segments = 10;\n const filledSegments = getCombatReadinessBars(readiness, segments);\n const readinessColor = getCombatReadinessColor(readiness);\n const readinessLabel = getCombatReadinessLabel(readiness);\n const shouldPulse = readiness < 20;\n\n const barWidth = isMobile ? 180 : 250;\n const barHeight = isMobile ? 16 : 20;\n const fontSize = isMobile ? 11 : 13;\n const padding = isMobile ? \"8px 12px\" : \"12px 16px\";\n\n const statusText = `${readiness}% ${readinessLabel.korean}`;\n\n return (\n <div\n data-testid={`combat-readiness-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"전투 준비도 | Combat Readiness\"\n aria-valuenow={readiness}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuetext={`${readiness}% Combat Readiness - ${readinessLabel.english}`}\n style={{\n width: `${barWidth}px`,\n 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 }}\n >\n {/* Label and numeric/status display */}\n <div\n style={{\n fontSize: `${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 }}\n >\n <span>전투 준비도 | Combat Readiness</span>\n <span data-testid={`combat-readiness-value-${playerId}`}>\n {statusText}\n </span>\n </div>\n\n {/* 10-segment combat readiness bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`combat-readiness-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(readinessColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.3s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(readinessColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n\n {/* Breakdown tooltip (hover) - optional enhancement */}\n <div\n data-testid={`combat-readiness-breakdown-${playerId}`}\n style={{\n display: \"none\", // Hidden by default, can be shown on hover\n fontSize: `${fontSize - 2}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n marginTop: \"4px\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {/* Detailed breakdown for debugging/advanced players */}\n <div>Body: {player.bodyPartHealth ? \"tracked\" : \"aggregate\"}</div>\n <div>Pain: {Math.round(player.pain)}%</div>\n <div>Consciousness: {Math.round(player.consciousness)}%</div>\n <div>Balance: {Math.round(player.balance)}%</div>\n </div>\n </div>\n );\n};\n\nexport default CombatReadinessBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,sBAAyD,EACpE,QACA,UACA,eACI;CACJ,MAAM,YAAY,cACV,yBAAyB,MAAM,GACrC,CAAC,MAAM,CACT;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,uBAAuB,WAAW,QAAQ;CACjE,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,cAAc,YAAY;CAEhC,MAAM,WAAW,WAAW,MAAM;CAClC,MAAM,YAAY,WAAW,KAAK;CAClC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,aAAa;CAExC,MAAM,aAAa,GAAG,UAAU,IAAI,eAAe;CAEnD,OACE,qBAAC,OAAD;EACE,eAAa,wBAAwB;EACrC,MAAK;EACL,cAAW;EACX,iBAAe;EACf,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,UAAU,uBAAuB,eAAe;EACnE,OAAO;GACL,OAAO,GAAG,SAAS;GACnB;GACA,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,CAAC;GAClE,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;EACxE;YAfF;GAkBE,qBAAC,OAAD;IACE,OAAO;KACL,UAAU,GAAG,SAAS;KACtB,OAAO,gBAAgB,cAAc,cAAc,CAAC;KACpD,YAAY,YAAY;KACxB,cAAc;KACd,SAAS;KACT,gBAAgB;KAChB,YAAY;IACd;cATF,CAWE,oBAAC,QAAD,EAAA,UAAM,4BAA+B,CAAA,GACrC,oBAAC,QAAD;KAAM,eAAa,0BAA0B;eAC1C;IACG,CAAA,CACH;;GAGL,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,KAAK;KACL,QAAQ,GAAG,UAAU;KACrB,WAAW,cAAc,8BAA8B;IACzD;cAEC,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,EAAE,KAAK,GAAG,UACxC,oBAAC,OAAD;KAEE,eAAa,4BAA4B,SAAS,GAAG;KACrD,OAAO;MACL,MAAM;MACN,iBACE,QAAQ,iBACJ,gBAAgB,gBAAgB,CAAC,IACjC,gBAAgB,cAAc,sBAAsB,CAAC;MAC3D,cAAc;MACd,YAAY;MACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,gBAAgB,EAAG,MAC9C;KACR;IACD,GAfM,KAeN,CACF;GACE,CAAA;GAGL,qBAAC,OAAD;IACE,eAAa,8BAA8B;IAC3C,OAAO;KACL,SAAS;KACT,UAAU,GAAG,WAAW,EAAE;KAC1B,OAAO,gBAAgB,cAAc,gBAAgB,CAAC;KACtD,WAAW;KACX,YAAY,YAAY;IAC1B;cARF;KAWE,qBAAC,OAAD,EAAA,UAAA,CAAK,UAAO,OAAO,iBAAiB,YAAY,WAAiB,EAAA,CAAA;KACjE,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAO,KAAK,MAAM,OAAO,IAAI;MAAE;KAAM,EAAA,CAAA;KAC1C,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAgB,KAAK,MAAM,OAAO,aAAa;MAAE;KAAM,EAAA,CAAA;KAC5D,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAU,KAAK,MAAM,OAAO,OAAO;MAAE;KAAM,EAAA,CAAA;IAC7C;;EACF;;AAET"}
1
+ {"version":3,"file":"CombatReadinessBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/CombatReadinessBar.tsx"],"sourcesContent":["/**\n * CombatReadinessBar Component - 10-bar segmented combat readiness display\n * \n * Displays comprehensive combat readiness with:\n * - 10 segmented bars representing 10% each\n * - Color transitions: Green (>80%), Yellow (60-79%), Orange (40-59%), Red (20-39%), Dark Red (<20%)\n * - Smooth 0.3s transition animations\n * - Korean/English bilingual labels\n * - Numeric percentage display\n * - Responsive sizing for mobile/tablet/desktop\n * - Real-time calculation from body health, pain, consciousness, and balance\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { \n calculateCombatReadiness, \n getCombatReadinessColor, \n getCombatReadinessLabel,\n getCombatReadinessBars\n} from \"../../../../utils/combatReadiness\";\nimport type { PlayerState } from \"../../../../systems/player\";\n\nexport interface CombatReadinessBarProps {\n /** Player state containing all combat factors */\n readonly player: PlayerState;\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 * CombatReadinessBar - 10-bar segmented combat readiness display with Korean theming\n * \n * Calculates and displays overall combat readiness from multiple factors:\n * - Body part health (40% weight)\n * - Pain level (20% weight)\n * - Consciousness (20% weight)\n * - Balance state (20% weight)\n * \n * @example\n * ```tsx\n * <CombatReadinessBar \n * player={playerState} \n * playerId=\"player-1\"\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReadinessBar: React.FC<CombatReadinessBarProps> = ({\n player,\n playerId,\n isMobile,\n}) => {\n const readiness = useMemo(\n () => calculateCombatReadiness(player),\n [player]\n );\n\n const segments = 10;\n const filledSegments = getCombatReadinessBars(readiness, segments);\n const readinessColor = getCombatReadinessColor(readiness);\n const readinessLabel = getCombatReadinessLabel(readiness);\n const shouldPulse = readiness < 20;\n\n const barWidth = isMobile ? 180 : 250;\n const barHeight = isMobile ? 16 : 20;\n const fontSize = isMobile ? 11 : 13;\n const padding = isMobile ? \"8px 12px\" : \"12px 16px\";\n\n const statusText = `${readiness}% ${readinessLabel.korean}`;\n\n return (\n <div\n data-testid={`combat-readiness-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"전투 준비도 | Combat Readiness\"\n aria-valuenow={readiness}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuetext={`${readiness}% Combat Readiness - ${readinessLabel.english}`}\n style={{\n width: `${barWidth}px`,\n 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 }}\n >\n {/* Label and numeric/status display */}\n <div\n style={{\n fontSize: `${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 }}\n >\n <span>전투 준비도 | Combat Readiness</span>\n <span data-testid={`combat-readiness-value-${playerId}`}>\n {statusText}\n </span>\n </div>\n\n {/* 10-segment combat readiness bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`combat-readiness-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(readinessColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.3s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(readinessColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n\n {/* Breakdown tooltip (hover) - optional enhancement */}\n <div\n data-testid={`combat-readiness-breakdown-${playerId}`}\n style={{\n display: \"none\", // Hidden by default, can be shown on hover\n fontSize: `${fontSize - 2}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n marginTop: \"4px\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {/* Detailed breakdown for debugging/advanced players */}\n <div>Body: {player.bodyPartHealth ? \"tracked\" : \"aggregate\"}</div>\n <div>Pain: {Math.round(player.pain)}%</div>\n <div>Consciousness: {Math.round(player.consciousness)}%</div>\n <div>Balance: {Math.round(player.balance)}%</div>\n </div>\n </div>\n );\n};\n\nexport default CombatReadinessBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,sBAAyD,EACpE,QACA,UACA,eACI;CACJ,MAAM,YAAY,cACV,yBAAyB,MAAM,GACrC,CAAC,MAAM,CACT;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,uBAAuB,WAAW,QAAQ;CACjE,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,cAAc,YAAY;CAEhC,MAAM,WAAW,WAAW,MAAM;CAClC,MAAM,YAAY,WAAW,KAAK;CAClC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,aAAa;CAExC,MAAM,aAAa,GAAG,UAAU,IAAI,eAAe;CAEnD,OACE,qBAAC,OAAD;EACE,eAAa,wBAAwB;EACrC,MAAK;EACL,cAAW;EACX,iBAAe;EACf,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,UAAU,uBAAuB,eAAe;EACnE,OAAO;GACL,OAAO,GAAG,SAAS;GACnB;GACA,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,CAAC;GAClE,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;EACxE;YAfF;GAkBE,qBAAC,OAAD;IACE,OAAO;KACL,UAAU,GAAG,SAAS;KACtB,OAAO,gBAAgB,cAAc,cAAc,CAAC;KACpD,YAAY,YAAY;KACxB,cAAc;KACd,SAAS;KACT,gBAAgB;KAChB,YAAY;IACd;cATF,CAWE,oBAAC,QAAD,EAAA,UAAM,4BAA+B,CAAA,GACrC,oBAAC,QAAD;KAAM,eAAa,0BAA0B;eAC1C;IACG,CAAA,CACH;;GAGL,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,KAAK;KACL,QAAQ,GAAG,UAAU;KACrB,WAAW,cAAc,8BAA8B;IACzD;cAEC,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;KAEE,eAAa,4BAA4B,SAAS,GAAG;KACrD,OAAO;MACL,MAAM;MACN,iBACE,QAAQ,iBACJ,gBAAgB,gBAAgB,CAAC,IACjC,gBAAgB,cAAc,sBAAsB,CAAC;MAC3D,cAAc;MACd,YAAY;MACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,gBAAgB,EAAG,MAC9C;KACR;IACD,GAfM,KAeN,CACF;GACE,CAAA;GAGL,qBAAC,OAAD;IACE,eAAa,8BAA8B;IAC3C,OAAO;KACL,SAAS;KACT,UAAU,GAAG,WAAW,EAAE;KAC1B,OAAO,gBAAgB,cAAc,gBAAgB,CAAC;KACtD,WAAW;KACX,YAAY,YAAY;IAC1B;cARF;KAWE,qBAAC,OAAD,EAAA,UAAA,CAAK,UAAO,OAAO,iBAAiB,YAAY,WAAiB,EAAA,CAAA;KACjE,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAO,KAAK,MAAM,OAAO,IAAI;MAAE;KAAM,EAAA,CAAA;KAC1C,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAgB,KAAK,MAAM,OAAO,aAAa;MAAE;KAAM,EAAA,CAAA;KAC5D,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAU,KAAK,MAAM,OAAO,OAAO;MAAE;KAAM,EAAA,CAAA;IAC7C;;EACF;;AAET"}
@@ -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 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 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;AACvB;;;;;AAMA,IAAa,YAAsC,MAAM,MAAM,EAC7D,SACA,KACA,UACA,eACI;CACJ,MAAM,gBAAgB,cACd,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,GAAG,CAAC,GACtD,CAAC,SAAS,GAAG,CACf;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,gBAAgB,MAAO,QAAQ;CACjE,MAAM,cAAc,eAAe,aAAa;CAChD,MAAM,cAAc,gBAAgB;CAEpC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,aAAa;CACnC,IAAI,CAAC,QAAQ,CAAC;CAEd,OACE,qBAAC,OAAD;EACE,eAAa,cAAc;EAC3B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,OAAO;EAChC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,OAAO,EAAE,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,CAAC;GAClE,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;GACtE,YAAY;EACd;YAjBF,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,cAAc,CAAC;IACpD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;GACd;aAVF,CAYE,oBAAC,QAAD,EAAA,UAAM,cAAiB,CAAA,GACvB,qBAAC,QAAD;IAAM,eAAa,gBAAgB;cAAnC;KACG,KAAK,KAAK,OAAO;KAAE;KAAE;IAClB;KACH;MAGL,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,0CAA0C;GACrE;aAEC,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,EAAE,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,kBAAkB,SAAS,GAAG;IAC3C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,aAAa,CAAC,IAC9B,gBAAgB,cAAc,sBAAsB,CAAC;KAC3D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,aAAa,EAAG,MAC3C;IACR;GACD,GAfM,KAeN,CACF;EACE,CAAA,CACF;;AAET,CAAC;AAED,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 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 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;AACvB;;;;;AAMA,IAAa,YAAsC,MAAM,MAAM,EAC7D,SACA,KACA,UACA,eACI;CACJ,MAAM,gBAAgB,cACd,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,GAAG,CAAC,GACtD,CAAC,SAAS,GAAG,CACf;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,gBAAgB,MAAO,QAAQ;CACjE,MAAM,cAAc,eAAe,aAAa;CAChD,MAAM,cAAc,gBAAgB;CAEpC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,aAAa;CACnC,IAAI,CAAC,QAAQ,CAAC;CAEd,OACE,qBAAC,OAAD;EACE,eAAa,cAAc;EAC3B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,OAAO;EAChC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,OAAO,EAAE,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,CAAC;GAClE,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;GACtE,YAAY;EACd;YAjBF,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,cAAc,CAAC;IACpD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;GACd;aAVF,CAYE,oBAAC,QAAD,EAAA,UAAM,cAAiB,CAAA,GACvB,qBAAC,QAAD;IAAM,eAAa,gBAAgB;cAAnC;KACG,KAAK,KAAK,OAAO;KAAE;KAAE;IAClB;KACH;MAGL,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,0CAA0C;GACrE;aAEC,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,kBAAkB,SAAS,GAAG;IAC3C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,aAAa,CAAC,IAC9B,gBAAgB,cAAc,sBAAsB,CAAC;KAC3D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,aAAa,EAAG,MAC3C;IACR;GACD,GAfM,KAeN,CACF;EACE,CAAA,CACF;;AAET,CAAC;AAED,UAAU,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 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 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 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 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 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 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 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 const positionSame = prevProps.position === nextProps.position;\n const mobileSame = prevProps.isMobile === nextProps.isMobile;\n const lateralitySame = prevProps.laterality === nextProps.laterality;\n\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 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 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 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 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 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 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 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 const positionSame = prevProps.position === nextProps.position;\n const mobileSame = prevProps.isMobile === nextProps.isMobile;\n const lateralitySame = prevProps.laterality === nextProps.laterality;\n\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,IAAM,sBAGD,MAAM,MAAM,EAAE,YAAY,eAAe;CAC5C,MAAM,SAAS,eAAe;CAE9B,MAAM,aAAa,eACV;EACL,SAAS;EACT,YAAY;EACZ,KAAK,WAAW,QAAQ;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY,YAAY;EACxB,OAAO,gBAAgB,cAAc,aAAa,CAAC;CACrD,IACA,CAAC,QAAQ,CACX;CAEA,MAAM,aAAa,eACV;EACL,SAAS,WAAW,YAAY;EAChC,YAAY,gBAAgB,cAAc,oBAAoB,EAAG;EACjE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAG;EACpE,cAAc;EACd,YAAY;EACZ,UAAU,WAAW,SAAS;EAC9B,WAAW;CACb,IACA,CAAC,QAAQ,CACX;CAEA,MAAM,YAAY,eACT;EACL,SAAS;EACT,YAAY;CACd,IACA,CAAC,CACH;CAEA,OACE,qBAAC,OAAD;EAAK,OAAO;EAAY,eAAY;YAApC,CACE,oBAAC,QAAD;GAAM,OAAO;GAAY,eAAY;aAClC,SAAS,MAAM;EACZ,CAAA,GACN,oBAAC,QAAD;GAAM,OAAO;GAAW,eAAY;aACjC,SAAS,SAAS;EACf,CAAA,CACH;;AAET,CAAC;AACD,oBAAoB,cAAc;;;;;AAMlC,IAAM,sBAAgD,EACpD,QACA,UACA,UACA,iBACI;CACJ,MAAM,WAAW,OAAO;CACxB,MAAM,SAAS,aAAa;CAE5B,MAAM,SAAS,eACN;EACL,UAAU,WAAW,KAAK;EAC1B,KAAK,WAAW,QAAQ;EACxB,UAAU,WAAW,KAAK;EAC1B,KAAK,WAAW,QAAQ;EACxB,YAAY,WAAW,QAAQ;CACjC,IACA,CAAC,QAAQ,CACX;CAEA,MAAM,qBAAqB,cAAc;EAIvC,OADE,iBAFmB,OAAO,UAAU,YAEnB,EACZ,EAAQ,SAAA;CACjB,GAAG,CAAC,OAAO,SAAS,CAAC;CAErB,MAAM,iBAAiB,eACd;EACL,UAAU;EACV,SAAS;EACT,eAAe;EACf,KAAK,OAAO;EACZ,eAAe;EACf,OAAO;CACT,IACA,CAAC,MAAM,CACT;CAEA,MAAM,qBAAqB,cAAc;EAEvC,OAAO;GACL,SAAS;GACT,YAAY;GACZ,KAAK;GACL,eALgB,SAAS,QAAQ;EAMnC;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,YAAY,eACT;EACL,OAAO,GAAG,OAAO,SAAS;EAC1B,QAAQ,GAAG,OAAO,SAAS;EAC3B,cAAc;EACd,UAAU;EACV,QAAQ,aAAa,gBAAgB,cAAc,aAAa,CAAC;EACjE,WAAW,YAAY,gBAAgB,cAAc,aAAa,EAAG;EACrE,YAAY;CACd,IACA,CAAC,OAAO,QAAQ,CAClB;CAEA,MAAM,YAAY,cAAc;EAC9B,MAAM,YAAY,SAAS,SAAS;EACpC,OAAO;GACL,UAAU,GAAG,OAAO,SAAS;GAC7B,YAAY;GACZ,YAAY,YAAY;GACxB,OAAO,gBAAgB,cAAc,aAAa,CAAC;GACxC;GACX,YAAY;GACZ,SAAS;GACT,YAAY,gBAAgB,cAAc,oBAAoB,EAAG;GACjE,cAAc;GACd,YAAY;EACd;CACF,GAAG,CAAC,OAAO,UAAU,MAAM,CAAC;CAE5B,MAAM,cAAc,cAAc;EAChC,MAAM,YAAY,SAAS,SAAS;EACpC,OAAO;GACL,UAAU,WAAW,SAAS;GAC9B,YAAY,YAAY;GACxB,OAAO,gBAAgB,cAAc,aAAa,CAAC;GACxC;GACX,YAAY;GACZ,SAAS;GACT,iBAAiB,gBAAgB,cAAc,oBAAoB,EAAG;GACtE,cAAc;GACd,WAAW;EACb;CACF,GAAG,CAAC,UAAU,MAAM,CAAC;CAErB,MAAM,mBAAmB,aACtB,MAA8C;EAC7C,MAAM,SAAS,EAAE;EACjB,IAAI,CAAC,OAAO,IAAI,SAAA,2CAAiC,GAC/C,OAAO,MAAM;CAEjB,GACA,CAAC,CACH;CAEA,OACE,qBAAC,OAAD;EAAK,eAAa,cAAc;EAAY,OAAO;YAAnD;GAEE,qBAAC,OAAD;IAAK,eAAa,eAAe;IAAY,OAAO;cAApD,CAEE,oBAAC,OAAD;KAAK,eAAa,kBAAkB;KAAY,OAAO;eACrD,oBAAC,OAAD;MACE,KAAK;MACL,KAAK,GAAG,OAAO,KAAK,QAAQ;MAC5B,OAAO;OACL,OAAO;OACP,QAAQ;OACR,WAAW;MACb;MACA,SAAS;KACV,CAAA;IACE,CAAA,GAEL,qBAAC,OAAD;KAAK,OAAO;eAAZ;MACG,OAAO,KAAK;MAAO;MAAI,OAAO,KAAK;KACjC;MACF;;GAGL,oBAAC,oBAAD;IACU;IACE;IACA;GACX,CAAA;GAGD,oBAAC,WAAD;IACE,SAAS,OAAO;IAChB,KAAK,OAAO;IACF;IACA;GACX,CAAA;GAGD,oBAAC,YAAD;IACE,SAAS,OAAO;IAChB,KAAK,OAAO;IACF;IACA;GACX,CAAA;GAGD,oBAAC,oBAAD;IAA4B;IAAkB;GAAW,CAAA;GAGxD,cACC,oBAAC,qBAAD;IAAiC;IAAsB;GAAW,CAAA;GAIpE,qBAAC,OAAD;IAAK,eAAa,oBAAoB;IAAY,OAAO;cAAzD,CAAsE,iBACtD,OAAO,aAClB;;EACF;;AAET;;;;;AAMA,IAAa,YAAY,MAAM,KAC7B,qBACC,WAAW,cAAc;CACxB,MAAM,aAAa,UAAU,OAAO,WAAW,UAAU,OAAO;CAChE,MAAM,gBACJ,UAAU,OAAO,cAAc,UAAU,OAAO;CAClD,MAAM,cAAc,UAAU,OAAO,YAAY,UAAU,OAAO;CAClE,MAAM,iBACJ,UAAU,OAAO,eAAe,UAAU,OAAO;CACnD,MAAM,gBACJ,UAAU,OAAO,cAAc,UAAU,OAAO;CAClD,MAAM,aACJ,UAAU,OAAO,kBAAkB,UAAU,OAAO;CACtD,MAAM,SAAS,UAAU,OAAO,OAAO,UAAU,OAAO;CACxD,MAAM,WACJ,UAAU,OAAO,KAAK,WAAW,UAAU,OAAO,KAAK,UACvD,UAAU,OAAO,KAAK,YAAY,UAAU,OAAO,KAAK;CAE1D,MAAM,oBACJ,UAAU,OAAO,cAAc,WAC7B,UAAU,OAAO,cAAc,UACjC,UAAU,OAAO,cAAc,OAC5B,QAAQ,UAAU,WAAW,UAAU,OAAO,cAAc,MAC/D;CAEF,MAAM,qBACJ,UAAU,OAAO,mBAAmB,UAAU,OAAO;CACvD,MAAM,WAAW,UAAU,OAAO,SAAS,UAAU,OAAO;CAC5D,MAAM,oBACJ,UAAU,OAAO,kBAAkB,UAAU,OAAO;CACtD,MAAM,cAAc,UAAU,OAAO,YAAY,UAAU,OAAO;CAElE,MAAM,eAAe,UAAU,aAAa,UAAU;CACtD,MAAM,aAAa,UAAU,aAAa,UAAU;CACpD,MAAM,iBAAiB,UAAU,eAAe,UAAU;CAE1D,OACE,cACA,iBACA,eACA,kBACA,iBACA,cACA,UACA,YACA,qBACA,sBACA,YACA,qBACA,eACA,gBACA,cACA;AAEJ,CACF"}
@@ -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 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 * @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 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 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 * @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 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 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 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;CACJ,MAAM,iBAAiB,cACf,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,GAAG,CAAC,GACtD,CAAC,SAAS,GAAG,CACf;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,iBAAiB,MAAO,QAAQ;CAClE,MAAM,cAAc,iBAAiB;CAErC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,YAAY;CAClC,IAAI,CAAC,QAAQ,CAAC;CAEd,OACE,qBAAC,OAAD;EACE,eAAa,eAAe;EAC5B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,OAAO;EAChC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,OAAO,EAAE,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,aAAa,CAAC;GACjE,WAAW,WAAW,gBAAgB,cAAc,aAAa,EAAG;GACpE,YAAY;EACd;YAjBF,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,aAAa,CAAC;IACnD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;GACd;aAVF,CAYE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,GACxB,qBAAC,QAAD;IAAM,eAAa,iBAAiB;cAApC;KACG,KAAK,KAAK,OAAO;KAAE;KAAE;IAClB;KACH;MAGL,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,2CAA2C;GACtE;aAEC,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,EAAE,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,mBAAmB,SAAS,GAAG;IAC5C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,cAAc,aAAa,CAAC,IAC5C,gBAAgB,cAAc,sBAAsB,CAAC;KAC3D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,cAAc,aAAa,EAAG,MACzD;IACR;GACD,GAfM,KAeN,CACF;EACE,CAAA,CACF;;AAET,CAAC;AAED,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 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 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;CACJ,MAAM,iBAAiB,cACf,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,GAAG,CAAC,GACtD,CAAC,SAAS,GAAG,CACf;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,iBAAiB,MAAO,QAAQ;CAClE,MAAM,cAAc,iBAAiB;CAErC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,YAAY;CAClC,IAAI,CAAC,QAAQ,CAAC;CAEd,OACE,qBAAC,OAAD;EACE,eAAa,eAAe;EAC5B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,OAAO;EAChC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,OAAO,EAAE,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,aAAa,CAAC;GACjE,WAAW,WAAW,gBAAgB,cAAc,aAAa,EAAG;GACpE,YAAY;EACd;YAjBF,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,aAAa,CAAC;IACnD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;GACd;aAVF,CAYE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,GACxB,qBAAC,QAAD;IAAM,eAAa,iBAAiB;cAApC;KACG,KAAK,KAAK,OAAO;KAAE;KAAE;IAClB;KACH;MAGL,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,2CAA2C;GACtE;aAEC,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,mBAAmB,SAAS,GAAG;IAC5C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,cAAc,aAAa,CAAC,IAC5C,gBAAgB,cAAc,sBAAsB,CAAC;KAC3D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,cAAc,aAAa,EAAG,MACzD;IACR;GACD,GAfM,KAeN,CACF;EACE,CAAA,CACF;;AAET,CAAC;AAED,WAAW,cAAc"}
@@ -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 const reachInfo = useMemo(() => {\n if (!playerArchetype || !playerStance || !technique.animation?.type) {\n return null;\n }\n\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 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 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 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 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 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 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 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 const animationClass = useMemo(\n () => (isAvailable ? \"hud-animated\" : \"\"),\n [isAvailable]\n );\n\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 const reachInfo = useMemo(() => {\n if (!playerArchetype || !playerStance || !technique.animation?.type) {\n return null;\n }\n\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 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 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 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 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 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 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 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 const animationClass = useMemo(\n () => (isAvailable ? \"hud-animated\" : \"\"),\n [isAvailable]\n );\n\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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"}