blacktrigram 0.7.98 → 0.7.99
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.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioManager.js +2 -4
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +8 -9
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
- package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
- package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js +2 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js +3 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +16 -15
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js +0 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js +7 -6
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js +3 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js +7 -6
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +34 -30
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/Key3D.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js +2 -2
- package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js +2 -2
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js +2 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +2 -4
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +10 -9
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js +14 -13
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
- package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointMarker3D.js +8 -7
- package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js +15 -13
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js +3 -2
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +1 -2
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js +3 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js +59 -53
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js +19 -18
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js +22 -18
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js +4 -5
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js +29 -24
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js +3 -2
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js +3 -2
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js +6 -5
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js +2 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js +25 -24
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js +1 -3
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js +2 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js +69 -68
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js +0 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +12 -3
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/systems/CombatSystem.js +6 -2
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js +0 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js +10 -21
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js +1 -3
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js +3 -4
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js +2 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js +3 -9
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js +8 -5
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/types/facial.js.map +1 -1
- package/lib/utils/accessibility.js +0 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/effectUtils.js +1 -3
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/inputSystem.js +0 -2
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js +6 -3
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/playerUtils.js +4 -2
- package/lib/utils/playerUtils.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComboCounter.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ComboCounter.tsx"],"sourcesContent":["/**\n * ComboCounter - Combo counter display component\n *\n * Displays the current combo count with Korean-English bilingual text.\n * Animates on combo increment and shows milestone indicators.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/ui/ComboCounter\n * @category Shared UI\n * @korean 콤보카운터\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\n/**\n * Props for the ComboCounter component\n */\nexport interface ComboCounterProps {\n /** Current combo count */\n readonly combo: number;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Minimum combo to display (default: 2) */\n readonly minDisplayCombo?: number;\n}\n\n/**\n * Get combo tier color based on combo count\n */\nfunction getComboColor(combo: number): string {\n if (combo >= 10) {\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n }\n if (combo >= 7) {\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n }\n if (combo >= 5) {\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n }\n if (combo >= 3) {\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n}\n\n/**\n * Get glow color based on combo tier\n */\nfunction getGlowColor(combo: number): string {\n if (combo >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n }\n if (combo >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n }\n if (combo >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n }\n if (combo >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n}\n\n/**\n * Get combo milestone text\n */\nfunction getComboMilestone(\n combo: number\n): { korean: string; english: string } | null {\n if (combo === 5) {\n return { korean: \"훌륭합니다!\", english: \"Great!\" };\n }\n if (combo === 10) {\n return { korean: \"놀라운 연속 공격!\", english: \"Amazing!\" };\n }\n if (combo === 15) {\n return { korean: \"전설적인 공격!\", english: \"Legendary!\" };\n }\n if (combo === 20) {\n return { korean: \"신의 일격!\", english: \"GODLIKE!\" };\n }\n return null;\n}\n\n/**\n * ComboCounter Component\n *\n * Displays the current combo count with animations and milestone indicators.\n * Only visible when combo >= minDisplayCombo (default: 2).\n *\n * @example\n * ```tsx\n * <ComboCounter\n * combo={5}\n * isMobile={isMobile}\n * />\n * ```\n */\nexport const ComboCounter: React.FC<ComboCounterProps> = ({\n combo,\n isMobile = false,\n minDisplayCombo = 2,\n}) => {\n const [scale, setScale] = useState(1);\n const [showMilestone, setShowMilestone] = useState(false);\n const [prevCombo, setPrevCombo] = useState(combo);\n const milestoneTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null\n );\n const scaleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const position3D: [number, number, number] = useMemo(() => {\n return [0, 4.5, 0];\n }, []);\n\n useEffect(() => {\n if (combo > prevCombo) {\n setTimeout(() => setScale(1.3), 0);\n\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n scaleTimeoutRef.current = setTimeout(() => {\n setScale(1);\n }, 150);\n\n const milestone = getComboMilestone(combo);\n if (milestone) {\n setTimeout(() => setShowMilestone(true), 0);\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n milestoneTimeoutRef.current = setTimeout(() => {\n setShowMilestone(false);\n }, 1500);\n }\n }\n setTimeout(() => setPrevCombo(combo), 0);\n }, [combo, prevCombo]);\n\n useEffect(() => {\n return () => {\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n };\n }, []);\n\n if (combo < minDisplayCombo) {\n return null;\n }\n\n const milestone = getComboMilestone(combo);\n const comboColor = getComboColor(combo);\n const glowColor = getGlowColor(combo);\n const getMilestoneBackground = (comboVal: number): string => {\n if (comboVal >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.3);\n }\n if (comboVal >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.3);\n }\n if (comboVal >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n }\n if (comboVal >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.3);\n };\n\n const mainFontSize = isMobile ? 32 : 48;\n const subFontSize = isMobile ? 16 : 20;\n const milestoneFontSize = isMobile ? 20 : 28;\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"combo-counter\"\n className=\"hud-animated\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n transform: `scale(${scale})`,\n transition: \"transform 0.15s ease-out\",\n animation: combo >= 5 ? \"comboFlash 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {/* Main combo number */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n lineHeight: 1,\n }}\n >\n {combo}\n </div>\n\n {/* Korean text */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}, 2px 2px 4px rgba(0, 0, 0, 0.8)`,\n marginTop: \"4px\",\n }}\n >\n 연속 타격!\n </div>\n\n {/* English text */}\n <div\n style={{\n fontSize: `${subFontSize - 4}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {combo} HIT COMBO!\n </div>\n\n {/* Milestone indicator */}\n {showMilestone && milestone && (\n <div\n className=\"hud-animated\"\n style={{\n marginTop: \"8px\",\n padding: \"4px 16px\",\n background: getMilestoneBackground(combo),\n borderRadius: \"4px\",\n border: `2px solid ${comboColor}`,\n animation: \"comboFlash 1s ease-in-out infinite\",\n }}\n >\n <div\n style={{\n fontSize: `${milestoneFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}`,\n }}\n >\n {milestone.korean}\n </div>\n <div\n style={{\n fontSize: `${milestoneFontSize - 6}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n {milestone.english}\n </div>\n </div>\n )}\n </div>\n\n {/* CSS Animation - kept for backwards compatibility */}\n <style>{`\n @keyframes pulse {\n 0%, 100% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n }\n `}</style>\n </Html>\n );\n};\n\nexport default ComboCounter;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ComboCounter.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ComboCounter.tsx"],"sourcesContent":["/**\n * ComboCounter - Combo counter display component\n *\n * Displays the current combo count with Korean-English bilingual text.\n * Animates on combo increment and shows milestone indicators.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/ui/ComboCounter\n * @category Shared UI\n * @korean 콤보카운터\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\n/**\n * Props for the ComboCounter component\n */\nexport interface ComboCounterProps {\n /** Current combo count */\n readonly combo: number;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Minimum combo to display (default: 2) */\n readonly minDisplayCombo?: number;\n}\n\n/**\n * Get combo tier color based on combo count\n */\nfunction getComboColor(combo: number): string {\n if (combo >= 10) {\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n }\n if (combo >= 7) {\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n }\n if (combo >= 5) {\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n }\n if (combo >= 3) {\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n}\n\n/**\n * Get glow color based on combo tier\n */\nfunction getGlowColor(combo: number): string {\n if (combo >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n }\n if (combo >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n }\n if (combo >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n }\n if (combo >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n}\n\n/**\n * Get combo milestone text\n */\nfunction getComboMilestone(\n combo: number\n): { korean: string; english: string } | null {\n if (combo === 5) {\n return { korean: \"훌륭합니다!\", english: \"Great!\" };\n }\n if (combo === 10) {\n return { korean: \"놀라운 연속 공격!\", english: \"Amazing!\" };\n }\n if (combo === 15) {\n return { korean: \"전설적인 공격!\", english: \"Legendary!\" };\n }\n if (combo === 20) {\n return { korean: \"신의 일격!\", english: \"GODLIKE!\" };\n }\n return null;\n}\n\n/**\n * ComboCounter Component\n *\n * Displays the current combo count with animations and milestone indicators.\n * Only visible when combo >= minDisplayCombo (default: 2).\n *\n * @example\n * ```tsx\n * <ComboCounter\n * combo={5}\n * isMobile={isMobile}\n * />\n * ```\n */\nexport const ComboCounter: React.FC<ComboCounterProps> = ({\n combo,\n isMobile = false,\n minDisplayCombo = 2,\n}) => {\n const [scale, setScale] = useState(1);\n const [showMilestone, setShowMilestone] = useState(false);\n const [prevCombo, setPrevCombo] = useState(combo);\n const milestoneTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n null\n );\n const scaleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const position3D: [number, number, number] = useMemo(() => {\n return [0, 4.5, 0];\n }, []);\n\n useEffect(() => {\n if (combo > prevCombo) {\n setTimeout(() => setScale(1.3), 0);\n\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n scaleTimeoutRef.current = setTimeout(() => {\n setScale(1);\n }, 150);\n\n const milestone = getComboMilestone(combo);\n if (milestone) {\n setTimeout(() => setShowMilestone(true), 0);\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n milestoneTimeoutRef.current = setTimeout(() => {\n setShowMilestone(false);\n }, 1500);\n }\n }\n setTimeout(() => setPrevCombo(combo), 0);\n }, [combo, prevCombo]);\n\n useEffect(() => {\n return () => {\n if (milestoneTimeoutRef.current) {\n clearTimeout(milestoneTimeoutRef.current);\n }\n if (scaleTimeoutRef.current) {\n clearTimeout(scaleTimeoutRef.current);\n }\n };\n }, []);\n\n if (combo < minDisplayCombo) {\n return null;\n }\n\n const milestone = getComboMilestone(combo);\n const comboColor = getComboColor(combo);\n const glowColor = getGlowColor(combo);\n const getMilestoneBackground = (comboVal: number): string => {\n if (comboVal >= 10) {\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.3);\n }\n if (comboVal >= 7) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.3);\n }\n if (comboVal >= 5) {\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n }\n if (comboVal >= 3) {\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3);\n }\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.3);\n };\n\n const mainFontSize = isMobile ? 32 : 48;\n const subFontSize = isMobile ? 16 : 20;\n const milestoneFontSize = isMobile ? 20 : 28;\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"combo-counter\"\n className=\"hud-animated\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n transform: `scale(${scale})`,\n transition: \"transform 0.15s ease-out\",\n animation: combo >= 5 ? \"comboFlash 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {/* Main combo number */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n lineHeight: 1,\n }}\n >\n {combo}\n </div>\n\n {/* Korean text */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}, 2px 2px 4px rgba(0, 0, 0, 0.8)`,\n marginTop: \"4px\",\n }}\n >\n 연속 타격!\n </div>\n\n {/* English text */}\n <div\n style={{\n fontSize: `${subFontSize - 4}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n }}\n >\n {combo} HIT COMBO!\n </div>\n\n {/* Milestone indicator */}\n {showMilestone && milestone && (\n <div\n className=\"hud-animated\"\n style={{\n marginTop: \"8px\",\n padding: \"4px 16px\",\n background: getMilestoneBackground(combo),\n borderRadius: \"4px\",\n border: `2px solid ${comboColor}`,\n animation: \"comboFlash 1s ease-in-out infinite\",\n }}\n >\n <div\n style={{\n fontSize: `${milestoneFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: comboColor,\n textShadow: `0 0 10px ${glowColor}`,\n }}\n >\n {milestone.korean}\n </div>\n <div\n style={{\n fontSize: `${milestoneFontSize - 6}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n {milestone.english}\n </div>\n </div>\n )}\n </div>\n\n {/* CSS Animation - kept for backwards compatibility */}\n <style>{`\n @keyframes pulse {\n 0%, 100% { transform: scale(1); }\n 50% { transform: scale(1.05); }\n }\n `}</style>\n </Html>\n );\n};\n\nexport default ComboCounter;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAS,cAAc,OAAuB;CAC5C,IAAI,SAAS,IACX,OAAO,cAAc,cAAc,iBAAiB;CAEtD,IAAI,SAAS,GACX,OAAO,cAAc,cAAc,UAAU;CAE/C,IAAI,SAAS,GACX,OAAO,cAAc,cAAc,WAAW;CAEhD,IAAI,SAAS,GACX,OAAO,cAAc,cAAc,YAAY;CAEjD,OAAO,cAAc,cAAc,YAAY;AACjD;;;;AAKA,SAAS,aAAa,OAAuB;CAC3C,IAAI,SAAS,IACX,OAAO,gBAAgB,cAAc,mBAAmB,EAAG;CAE7D,IAAI,SAAS,GACX,OAAO,gBAAgB,cAAc,YAAY,EAAG;CAEtD,IAAI,SAAS,GACX,OAAO,gBAAgB,cAAc,aAAa,EAAG;CAEvD,IAAI,SAAS,GACX,OAAO,gBAAgB,cAAc,cAAc,EAAG;CAExD,OAAO,gBAAgB,cAAc,cAAc,EAAG;AACxD;;;;AAKA,SAAS,kBACP,OAC4C;CAC5C,IAAI,UAAU,GACZ,OAAO;EAAE,QAAQ;EAAU,SAAS;CAAS;CAE/C,IAAI,UAAU,IACZ,OAAO;EAAE,QAAQ;EAAc,SAAS;CAAW;CAErD,IAAI,UAAU,IACZ,OAAO;EAAE,QAAQ;EAAY,SAAS;CAAa;CAErD,IAAI,UAAU,IACZ,OAAO;EAAE,QAAQ;EAAU,SAAS;CAAW;CAEjD,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,IAAa,gBAA6C,EACxD,OACA,WAAW,OACX,kBAAkB,QACd;CACJ,MAAM,CAAC,OAAO,YAAY,SAAS,CAAC;CACpC,MAAM,CAAC,eAAe,oBAAoB,SAAS,KAAK;CACxD,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,sBAAsB,OAC1B,IACF;CACA,MAAM,kBAAkB,OAA6C,IAAI;CAEzE,MAAM,aAAuC,cAAc;EACzD,OAAO;GAAC;GAAG;GAAK;EAAC;CACnB,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,IAAI,QAAQ,WAAW;GACrB,iBAAiB,SAAS,GAAG,GAAG,CAAC;GAEjC,IAAI,gBAAgB,SAClB,aAAa,gBAAgB,OAAO;GAEtC,gBAAgB,UAAU,iBAAiB;IACzC,SAAS,CAAC;GACZ,GAAG,GAAG;GAGN,IADkB,kBAAkB,KAChC,GAAW;IACb,iBAAiB,iBAAiB,IAAI,GAAG,CAAC;IAC1C,IAAI,oBAAoB,SACtB,aAAa,oBAAoB,OAAO;IAE1C,oBAAoB,UAAU,iBAAiB;KAC7C,iBAAiB,KAAK;IACxB,GAAG,IAAI;GACT;EACF;EACA,iBAAiB,aAAa,KAAK,GAAG,CAAC;CACzC,GAAG,CAAC,OAAO,SAAS,CAAC;CAErB,gBAAgB;EACd,aAAa;GACX,IAAI,oBAAoB,SACtB,aAAa,oBAAoB,OAAO;GAE1C,IAAI,gBAAgB,SAClB,aAAa,gBAAgB,OAAO;EAExC;CACF,GAAG,CAAC,CAAC;CAEL,IAAI,QAAQ,iBACV,OAAO;CAGT,MAAM,YAAY,kBAAkB,KAAK;CACzC,MAAM,aAAa,cAAc,KAAK;CACtC,MAAM,YAAY,aAAa,KAAK;CACpC,MAAM,0BAA0B,aAA6B;EAC3D,IAAI,YAAY,IACd,OAAO,gBAAgB,cAAc,mBAAmB,EAAG;EAE7D,IAAI,YAAY,GACd,OAAO,gBAAgB,cAAc,YAAY,EAAG;EAEtD,IAAI,YAAY,GACd,OAAO,gBAAgB,cAAc,aAAa,EAAG;EAEvD,IAAI,YAAY,GACd,OAAO,gBAAgB,cAAc,cAAc,EAAG;EAExD,OAAO,gBAAgB,cAAc,cAAc,EAAG;CACxD;CAEA,MAAM,eAAe,WAAW,KAAK;CACrC,MAAM,cAAc,WAAW,KAAK;CACpC,MAAM,oBAAoB,WAAW,KAAK;CAE1C,OACE,qBAAC,MAAD;EACE,UAAU;EACV,QAAA;EACA,gBAAgB;EAChB,OAAO,EAAE,eAAe,OAAO;EAJjC,UAAA,CAME,qBAAC,OAAD;GACE,eAAY;GACZ,WAAU;GACV,OAAO;IACL,SAAS;IACT,eAAe;IACf,YAAY;IACZ,WAAW,SAAS,MAAM;IAC1B,YAAY;IACZ,WAAW,SAAS,IAAI,yCAAyC;GACnE;GAVF,UAAA;IAaE,oBAAC,OAAD;KACE,OAAO;MACL,UAAU,GAAG,aAAa;MAC1B,YAAY;MACZ,YAAY,YAAY;MACxB,OAAO;MACP,YAAY;yBACC,UAAU;yBACV,UAAU;;;MAGvB,YAAY;KACd;KAEC,UAAA;IACE,CAAA;IAGL,oBAAC,OAAD;KACE,OAAO;MACL,UAAU,GAAG,YAAY;MACzB,YAAY;MACZ,YAAY,YAAY;MACxB,OAAO;MACP,YAAY,YAAY,UAAU;MAClC,WAAW;KACb;KACD,UAAA;IAEI,CAAA;IAGL,qBAAC,OAAD;KACE,OAAO;MACL,UAAU,GAAG,cAAc,EAAE;MAC7B,YAAY;MACZ,YAAY,YAAY;MACxB,OAAO,cAAc,cAAc,cAAc;MACjD,YAAY;KACd;KAPF,UAAA,CASG,OAAM,aACJ;;IAGJ,iBAAiB,aAChB,qBAAC,OAAD;KACE,WAAU;KACV,OAAO;MACL,WAAW;MACX,SAAS;MACT,YAAY,uBAAuB,KAAK;MACxC,cAAc;MACd,QAAQ,aAAa;MACrB,WAAW;KACb;KATF,UAAA,CAWE,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,GAAG,kBAAkB;OAC/B,YAAY;OACZ,YAAY,YAAY;OACxB,OAAO;OACP,YAAY,YAAY;MAC1B;MAEC,UAAA,UAAU;KACR,CAAA,GACL,oBAAC,OAAD;MACE,OAAO;OACL,UAAU,GAAG,oBAAoB,EAAE;OACnC,YAAY;OACZ,YAAY,YAAY;OACxB,OAAO,cAAc,cAAc,cAAc;MACnD;MAEC,UAAA,UAAU;KACR,CAAA,CACF;;GAEJ;EAGL,CAAA,GAAA,oBAAC,SAAD,EAAA,UAAQ;;;;;QAKC,CAAA,CACL;;AAEV"}
|
|
@@ -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;
|
|
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;EAjBF,UAAA,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;GAVF,UAAA,CAYE,oBAAC,QAAD,EAAA,UAAM,cAAiB,CAAA,GACvB,qBAAC,QAAD;IAAM,eAAa,gBAAgB;IAAnC,UAAA;KACG,KAAK,KAAK,OAAO;KAAE;KAAE;IAClB;GACH,CAAA,CAAA;EAGL,CAAA,GAAA,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,0CAA0C;GACrE;GAEC,UAAA,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":"MenuList.js","names":[],"sources":["../../../../../src/components/shared/three/ui/MenuList.tsx"],"sourcesContent":["/**\n * MenuList - Three.js-compatible menu list component\n * \n * Navigation menu with Korean theming and keyboard support\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Menu item interface\n */\nexport interface MenuItem {\n readonly id: string;\n readonly korean: string;\n readonly english: string;\n readonly disabled?: boolean;\n}\n\n/**\n * Props for MenuList component\n */\nexport interface MenuListProps {\n readonly items: readonly MenuItem[];\n readonly onSelect: (id: string) => void;\n readonly selectedId?: string;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly testId?: string;\n}\n\n/**\n * MenuList Component\n * \n * A navigational menu component with Korean cyberpunk styling.\n * Supports keyboard navigation and hover states.\n * \n * @example\n * ```tsx\n * <MenuList\n * items={[\n * { id: \"combat\", korean: \"대전\", english: \"Combat\" },\n * { id: \"training\", korean: \"훈련\", english: \"Training\" }\n * ]}\n * onSelect={(id) => console.log(id)}\n * />\n * ```\n */\nexport const MenuList: React.FC<MenuListProps> = ({\n items,\n onSelect,\n selectedId,\n position = [0, 0, 0],\n width = 300,\n testId,\n}) => {\n const [hoveredId, setHoveredId] = useState<string | null>(null);\n\n const handleItemClick = useCallback(\n (id: string, disabled?: boolean) => {\n if (!disabled) {\n onSelect(id);\n }\n },\n [onSelect]\n );\n\n const handleMouseEnter = useCallback((id: string) => {\n setHoveredId(id);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setHoveredId(null);\n }, []);\n\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"8px\",\n padding: \"12px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"8px\",\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [width]\n );\n\n const getItemStyle = useCallback(\n (id: string, disabled?: boolean): React.CSSProperties => {\n const isSelected = selectedId === id;\n const isHovered = hoveredId === id;\n\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.5);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n } else if (isHovered && !disabled) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6);\n }\n\n return {\n padding: \"12px 16px\",\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"4px\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n transition: \"all 0.2s ease\",\n opacity: disabled ? 0.4 : 1,\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n transform: isHovered && !disabled ? \"translateX(4px)\" : \"translateX(0)\",\n };\n },\n [selectedId, hoveredId]\n );\n\n const getTextStyle = useCallback(\n (isKorean: boolean, isSelected: boolean): React.CSSProperties => ({\n display: \"block\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isKorean ? \"16px\" : \"14px\",\n fontWeight: isKorean ? \"bold\" : \"normal\",\n fontStyle: isKorean ? \"normal\" : \"italic\",\n color: hexToRgbaString(\n isSelected ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.TEXT_PRIMARY\n ),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div style={containerStyle} data-testid={testId ?? \"menu-list\"}>\n {items.map((item) => {\n const isSelected = selectedId === item.id;\n return (\n <div\n key={item.id}\n onClick={() => handleItemClick(item.id, item.disabled)}\n onMouseEnter={() => handleMouseEnter(item.id)}\n onMouseLeave={handleMouseLeave}\n style={getItemStyle(item.id, item.disabled)}\n data-testid={`menu-item-${item.id}`}\n >\n <span style={getTextStyle(true, isSelected)}>\n {item.korean}\n </span>\n <span style={getTextStyle(false, isSelected)}>\n {item.english}\n </span>\n </div>\n );\n })}\n </div>\n </Html>\n );\n};\n\nMenuList.displayName = \"MenuList\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,IAAa,YAAqC,EAChD,OACA,UACA,YACA,WAAW;CAAC;CAAG;CAAG;AAAC,GACnB,QAAQ,KACR,aACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAwB,IAAI;CAE9D,MAAM,kBAAkB,aACrB,IAAY,aAAuB;EAClC,IAAI,CAAC,UACH,SAAS,EAAE;CAEf,GACA,CAAC,QAAQ,CACX;CAEA,MAAM,mBAAmB,aAAa,OAAe;EACnD,aAAa,EAAE;CACjB,GAAG,CAAC,CAAC;CAEL,MAAM,mBAAmB,kBAAkB;EACzC,aAAa,IAAI;CACnB,GAAG,CAAC,CAAC;CAEL,MAAM,iBAAiB,eACd;EACL,OAAO,GAAG,MAAM;EAChB,SAAS;EACT,eAAe;EACf,KAAK;EACL,SAAS;EACT,YAAY,gBAAgB,cAAc,oBAAoB,EAAG;EACjE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAG;EACpE,cAAc;EACd,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;CACxE,IACA,CAAC,KAAK,CACR;CAEA,MAAM,eAAe,aAClB,IAAY,aAA4C;EACvD,MAAM,aAAa,eAAe;EAClC,MAAM,YAAY,cAAc;EAEhC,IAAI,aAAa,gBAAgB,cAAc,sBAAsB,EAAG;EACxE,IAAI,cAAc,gBAAgB,cAAc,aAAa,EAAG;EAEhE,IAAI,YAAY;GACd,aAAa,gBAAgB,cAAc,cAAc,EAAG;GAC5D,cAAc,gBAAgB,cAAc,cAAc,EAAG;EAC/D,OAAO,IAAI,aAAa,CAAC,UAAU;GACjC,aAAa,gBAAgB,cAAc,aAAa,GAAI;GAC5D,cAAc,gBAAgB,cAAc,aAAa,EAAG;EAC9D;EAEA,OAAO;GACL,SAAS;GACT;GACA,QAAQ,aAAa;GACrB,cAAc;GACd,QAAQ,WAAW,gBAAgB;GACnC,YAAY;GACZ,SAAS,WAAW,KAAM;GAC1B,YAAY;GACZ,kBAAkB;GAClB,WAAW,aAAa,CAAC,WAAW,oBAAoB;EAC1D;CACF,GACA,CAAC,YAAY,SAAS,CACxB;CAEA,MAAM,eAAe,aAClB,UAAmB,gBAA8C;EAChE,SAAS;EACT,YAAY,YAAY;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY,WAAW,SAAS;EAChC,WAAW,WAAW,WAAW;EACjC,OAAO,gBACL,aAAa,cAAc,cAAc,cAAc,YACzD;EACA,YAAY,aAAa,gBAAgB,cAAc,aAAa,EAAG;CACzE,IACA,CAAC,CACH;CAEA,OACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;
|
|
1
|
+
{"version":3,"file":"MenuList.js","names":[],"sources":["../../../../../src/components/shared/three/ui/MenuList.tsx"],"sourcesContent":["/**\n * MenuList - Three.js-compatible menu list component\n * \n * Navigation menu with Korean theming and keyboard support\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Menu item interface\n */\nexport interface MenuItem {\n readonly id: string;\n readonly korean: string;\n readonly english: string;\n readonly disabled?: boolean;\n}\n\n/**\n * Props for MenuList component\n */\nexport interface MenuListProps {\n readonly items: readonly MenuItem[];\n readonly onSelect: (id: string) => void;\n readonly selectedId?: string;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly testId?: string;\n}\n\n/**\n * MenuList Component\n * \n * A navigational menu component with Korean cyberpunk styling.\n * Supports keyboard navigation and hover states.\n * \n * @example\n * ```tsx\n * <MenuList\n * items={[\n * { id: \"combat\", korean: \"대전\", english: \"Combat\" },\n * { id: \"training\", korean: \"훈련\", english: \"Training\" }\n * ]}\n * onSelect={(id) => console.log(id)}\n * />\n * ```\n */\nexport const MenuList: React.FC<MenuListProps> = ({\n items,\n onSelect,\n selectedId,\n position = [0, 0, 0],\n width = 300,\n testId,\n}) => {\n const [hoveredId, setHoveredId] = useState<string | null>(null);\n\n const handleItemClick = useCallback(\n (id: string, disabled?: boolean) => {\n if (!disabled) {\n onSelect(id);\n }\n },\n [onSelect]\n );\n\n const handleMouseEnter = useCallback((id: string) => {\n setHoveredId(id);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setHoveredId(null);\n }, []);\n\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"8px\",\n padding: \"12px\",\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"8px\",\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [width]\n );\n\n const getItemStyle = useCallback(\n (id: string, disabled?: boolean): React.CSSProperties => {\n const isSelected = selectedId === id;\n const isHovered = hoveredId === id;\n\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 0.5);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3);\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n } else if (isHovered && !disabled) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6);\n }\n\n return {\n padding: \"12px 16px\",\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"4px\",\n cursor: disabled ? \"not-allowed\" : \"pointer\",\n transition: \"all 0.2s ease\",\n opacity: disabled ? 0.4 : 1,\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n transform: isHovered && !disabled ? \"translateX(4px)\" : \"translateX(0)\",\n };\n },\n [selectedId, hoveredId]\n );\n\n const getTextStyle = useCallback(\n (isKorean: boolean, isSelected: boolean): React.CSSProperties => ({\n display: \"block\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isKorean ? \"16px\" : \"14px\",\n fontWeight: isKorean ? \"bold\" : \"normal\",\n fontStyle: isKorean ? \"normal\" : \"italic\",\n color: hexToRgbaString(\n isSelected ? KOREAN_COLORS.ACCENT_GOLD : KOREAN_COLORS.TEXT_PRIMARY\n ),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div style={containerStyle} data-testid={testId ?? \"menu-list\"}>\n {items.map((item) => {\n const isSelected = selectedId === item.id;\n return (\n <div\n key={item.id}\n onClick={() => handleItemClick(item.id, item.disabled)}\n onMouseEnter={() => handleMouseEnter(item.id)}\n onMouseLeave={handleMouseLeave}\n style={getItemStyle(item.id, item.disabled)}\n data-testid={`menu-item-${item.id}`}\n >\n <span style={getTextStyle(true, isSelected)}>\n {item.korean}\n </span>\n <span style={getTextStyle(false, isSelected)}>\n {item.english}\n </span>\n </div>\n );\n })}\n </div>\n </Html>\n );\n};\n\nMenuList.displayName = \"MenuList\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,IAAa,YAAqC,EAChD,OACA,UACA,YACA,WAAW;CAAC;CAAG;CAAG;AAAC,GACnB,QAAQ,KACR,aACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAwB,IAAI;CAE9D,MAAM,kBAAkB,aACrB,IAAY,aAAuB;EAClC,IAAI,CAAC,UACH,SAAS,EAAE;CAEf,GACA,CAAC,QAAQ,CACX;CAEA,MAAM,mBAAmB,aAAa,OAAe;EACnD,aAAa,EAAE;CACjB,GAAG,CAAC,CAAC;CAEL,MAAM,mBAAmB,kBAAkB;EACzC,aAAa,IAAI;CACnB,GAAG,CAAC,CAAC;CAEL,MAAM,iBAAiB,eACd;EACL,OAAO,GAAG,MAAM;EAChB,SAAS;EACT,eAAe;EACf,KAAK;EACL,SAAS;EACT,YAAY,gBAAgB,cAAc,oBAAoB,EAAG;EACjE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAG;EACpE,cAAc;EACd,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;CACxE,IACA,CAAC,KAAK,CACR;CAEA,MAAM,eAAe,aAClB,IAAY,aAA4C;EACvD,MAAM,aAAa,eAAe;EAClC,MAAM,YAAY,cAAc;EAEhC,IAAI,aAAa,gBAAgB,cAAc,sBAAsB,EAAG;EACxE,IAAI,cAAc,gBAAgB,cAAc,aAAa,EAAG;EAEhE,IAAI,YAAY;GACd,aAAa,gBAAgB,cAAc,cAAc,EAAG;GAC5D,cAAc,gBAAgB,cAAc,cAAc,EAAG;EAC/D,OAAO,IAAI,aAAa,CAAC,UAAU;GACjC,aAAa,gBAAgB,cAAc,aAAa,GAAI;GAC5D,cAAc,gBAAgB,cAAc,aAAa,EAAG;EAC9D;EAEA,OAAO;GACL,SAAS;GACT;GACA,QAAQ,aAAa;GACrB,cAAc;GACd,QAAQ,WAAW,gBAAgB;GACnC,YAAY;GACZ,SAAS,WAAW,KAAM;GAC1B,YAAY;GACZ,kBAAkB;GAClB,WAAW,aAAa,CAAC,WAAW,oBAAoB;EAC1D;CACF,GACA,CAAC,YAAY,SAAS,CACxB;CAEA,MAAM,eAAe,aAClB,UAAmB,gBAA8C;EAChE,SAAS;EACT,YAAY,YAAY;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY,WAAW,SAAS;EAChC,WAAW,WAAW,WAAW;EACjC,OAAO,gBACL,aAAa,cAAc,cAAc,cAAc,YACzD;EACA,YAAY,aAAa,gBAAgB,cAAc,aAAa,EAAG;CACzE,IACA,CAAC,CACH;CAEA,OACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;EACxB,UAAA,oBAAC,OAAD;GAAK,OAAO;GAAgB,eAAa,UAAU;GAChD,UAAA,MAAM,KAAK,SAAS;IACnB,MAAM,aAAa,eAAe,KAAK;IACvC,OACE,qBAAC,OAAD;KAEE,eAAe,gBAAgB,KAAK,IAAI,KAAK,QAAQ;KACrD,oBAAoB,iBAAiB,KAAK,EAAE;KAC5C,cAAc;KACd,OAAO,aAAa,KAAK,IAAI,KAAK,QAAQ;KAC1C,eAAa,aAAa,KAAK;KANjC,UAAA,CAQE,oBAAC,QAAD;MAAM,OAAO,aAAa,MAAM,UAAU;MACvC,UAAA,KAAK;KACF,CAAA,GACN,oBAAC,QAAD;MAAM,OAAO,aAAa,OAAO,UAAU;MACxC,UAAA,KAAK;KACF,CAAA,CACH;IAbE,GAAA,KAAK,EAaP;GAET,CAAC;EACE,CAAA;CACD,CAAA;AAEV;AAEA,SAAS,cAAc"}
|
|
@@ -80,7 +80,8 @@ var PlayerHUDComponent = ({ player, position, isMobile, laterality }) => {
|
|
|
80
80
|
horizontal: isMobile ? "8px" : "12px"
|
|
81
81
|
}), [isMobile]);
|
|
82
82
|
const archetypeImagePath = useMemo(() => {
|
|
83
|
-
|
|
83
|
+
const archetypeKey = player.archetype.toLowerCase();
|
|
84
|
+
return ARCHETYPE_ASSETS[archetypeKey]?.image ?? "/assets/visual/logo/black-trigram-256.png";
|
|
84
85
|
}, [player.archetype]);
|
|
85
86
|
const containerStyle = useMemo(() => ({
|
|
86
87
|
position: "relative",
|
|
@@ -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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgressBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ProgressBar.tsx"],"sourcesContent":["/**\n * ProgressBar - Three.js-compatible progress bar component\n * \n * Displays health, ki, stamina with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Progress bar type\n */\nexport type ProgressBarType = \"health\" | \"ki\" | \"stamina\";\n\n/**\n * Props for ProgressBar component\n */\nexport interface ProgressBarProps {\n readonly type: ProgressBarType;\n readonly current: number;\n readonly max: number;\n readonly label?: { korean: string; english: string };\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly height?: number;\n readonly showText?: boolean;\n readonly animated?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ProgressBar Component\n * \n * A progress bar component for displaying health, ki, and stamina.\n * Uses Korean cyberpunk theming with gradient fills.\n * \n * @example\n * ```tsx\n * <ProgressBar\n * type=\"health\"\n * current={75}\n * max={100}\n * label={{ korean: \"체력\", english: \"Health\" }}\n * />\n * ```\n */\nexport const ProgressBar: React.FC<ProgressBarProps> = ({\n type,\n current,\n max,\n label,\n position = [0, 0, 0],\n width = 200,\n height = 24,\n showText = true,\n animated = true,\n testId,\n}) => {\n const percentage = useMemo(\n () => Math.max(0, Math.min(1, max > 0 ? current / max : 0)),\n [current, max]\n );\n\n const colors = useMemo(() => {\n switch (type) {\n case \"health\":\n if (percentage > 0.6) {\n return {\n start: KOREAN_COLORS.HEALTH_FULL,\n end: KOREAN_COLORS.HEALTH_MEDIUM,\n glow: KOREAN_COLORS.POSITIVE_GREEN,\n };\n } else if (percentage > 0.3) {\n return {\n start: KOREAN_COLORS.HEALTH_MEDIUM,\n end: KOREAN_COLORS.HEALTH_LOW,\n glow: KOREAN_COLORS.WARNING_ORANGE,\n };\n } else {\n return {\n start: KOREAN_COLORS.HEALTH_LOW,\n end: KOREAN_COLORS.HEALTH_CRITICAL,\n glow: KOREAN_COLORS.ACCENT_RED,\n };\n }\n case \"ki\":\n return {\n start: KOREAN_COLORS.KI_FULL,\n end: KOREAN_COLORS.KI_MEDIUM,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n case \"stamina\":\n return {\n start: KOREAN_COLORS.STAMINA_FULL,\n end: KOREAN_COLORS.STAMINA_MEDIUM,\n glow: KOREAN_COLORS.SECONDARY_YELLOW,\n };\n default:\n return {\n start: KOREAN_COLORS.PRIMARY_CYAN,\n end: KOREAN_COLORS.ACCENT_BLUE,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n }\n }, [type, percentage]);\n\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n [width]\n );\n\n const labelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }),\n []\n );\n\n const barContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: \"100%\",\n height: `${height}px`,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.UI_BORDER, 0.6)}`,\n borderRadius: \"4px\",\n overflow: \"hidden\",\n position: \"relative\",\n }),\n [height]\n );\n\n const fillStyle = useMemo<React.CSSProperties>(() => {\n return {\n width: `${percentage * 100}%`,\n height: \"100%\",\n background: `linear-gradient(to right, ${hexToRgbaString(colors.start)}, ${hexToRgbaString(colors.end)})`,\n transition: animated ? \"width 0.3s ease\" : \"none\",\n position: \"relative\",\n boxShadow: animated\n ? `0 0 10px ${hexToRgbaString(colors.glow, 0.5)}`\n : \"none\",\n };\n }, [percentage, colors, animated]);\n\n const shineStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"0\",\n left: \"0\",\n width: \"60%\",\n height: \"40%\",\n background: hexToRgbaString(KOREAN_COLORS.WHITE_SOLID, 0.3),\n borderRadius: \"4px\",\n margin: \"2px\",\n }),\n []\n );\n\n const textStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"11px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `\n 0 1px 2px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.8)},\n 0 0 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.6)}\n `,\n whiteSpace: \"nowrap\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div \n style={containerStyle} \n data-testid={testId ?? `progress-bar-${type}`}\n data-current={current}\n data-max={max}\n data-percentage={Math.round(percentage * 100)}\n >\n {/* Label */}\n {label && showText && (\n <div style={labelStyle}>\n <span>\n {label.korean} | {label.english}\n </span>\n <span>\n {Math.ceil(current)} / {max}\n </span>\n </div>\n )}\n\n {/* Bar Container */}\n <div style={barContainerStyle}>\n {/* Fill */}\n <div style={fillStyle}>\n {/* Shine effect */}\n <div style={shineStyle} />\n </div>\n\n {/* Percentage Text Overlay */}\n {showText && (\n <div style={textStyle}>\n {Math.round(percentage * 100)}%\n </div>\n )}\n </div>\n </div>\n </Html>\n );\n};\n\nProgressBar.displayName = \"ProgressBar\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,eAA2C,EACtD,MACA,SACA,KACA,OACA,WAAW;CAAC;CAAG;CAAG;AAAC,GACnB,QAAQ,KACR,SAAS,IACT,WAAW,MACX,WAAW,MACX,aACI;CACJ,MAAM,aAAa,cACX,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,UAAU,MAAM,CAAC,CAAC,GAC1D,CAAC,SAAS,GAAG,CACf;CAEA,MAAM,SAAS,cAAc;EAC3B,QAAQ,MAAR;GACE,KAAK,UACH,IAAI,aAAa,IACf,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;QACK,IAAI,aAAa,IACtB,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;QAEA,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;GAEJ,KAAK,MACH,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;GACF,KAAK,WACH,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;GACF,SACE,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;EACJ;CACF,GAAG,CAAC,MAAM,UAAU,CAAC;CAErB,MAAM,iBAAiB,eACd;EACL,OAAO,GAAG,MAAM;EAChB,SAAS;EACT,eAAe;EACf,KAAK;CACP,IACA,CAAC,KAAK,CACR;CAEA,MAAM,aAAa,eACV;EACL,YAAY,YAAY;EACxB,UAAU;EACV,YAAY;EACZ,OAAO,gBAAgB,cAAc,YAAY;EACjD,YAAY,aAAa,gBAAgB,cAAc,aAAa,EAAG;EACvE,SAAS;EACT,gBAAgB;EAChB,YAAY;CACd,IACA,CAAC,CACH;CAEA,MAAM,oBAAoB,eACjB;EACL,OAAO;EACP,QAAQ,GAAG,OAAO;EAClB,YAAY,gBAAgB,cAAc,oBAAoB,EAAG;EACjE,QAAQ,aAAa,gBAAgB,cAAc,WAAW,EAAG;EACjE,cAAc;EACd,UAAU;EACV,UAAU;CACZ,IACA,CAAC,MAAM,CACT;CAEA,MAAM,YAAY,cAAmC;EACnD,OAAO;GACL,OAAO,GAAG,aAAa,IAAI;GAC3B,QAAQ;GACR,YAAY,6BAA6B,gBAAgB,OAAO,KAAK,EAAE,IAAI,gBAAgB,OAAO,GAAG,EAAE;GACvG,YAAY,WAAW,oBAAoB;GAC3C,UAAU;GACV,WAAW,WACP,YAAY,gBAAgB,OAAO,MAAM,EAAG,MAC5C;EACN;CACF,GAAG;EAAC;EAAY;EAAQ;CAAQ,CAAC;CAEjC,MAAM,aAAa,eACV;EACL,UAAU;EACV,KAAK;EACL,MAAM;EACN,OAAO;EACP,QAAQ;EACR,YAAY,gBAAgB,cAAc,aAAa,EAAG;EAC1D,cAAc;EACd,QAAQ;CACV,IACA,CAAC,CACH;CAEA,MAAM,YAAY,eACT;EACL,UAAU;EACV,KAAK;EACL,MAAM;EACN,WAAW;EACX,YAAY,YAAY;EACxB,UAAU;EACV,YAAY;EACZ,OAAO,gBAAgB,cAAc,YAAY;EACjD,YAAY;oBACE,gBAAgB,cAAc,aAAa,EAAG,EAAE;kBAClD,gBAAgB,cAAc,aAAa,EAAG,EAAE;;EAE5D,YAAY;CACd,IACA,CAAC,CACH;CAEA,OACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;
|
|
1
|
+
{"version":3,"file":"ProgressBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ProgressBar.tsx"],"sourcesContent":["/**\n * ProgressBar - Three.js-compatible progress bar component\n * \n * Displays health, ki, stamina with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Progress bar type\n */\nexport type ProgressBarType = \"health\" | \"ki\" | \"stamina\";\n\n/**\n * Props for ProgressBar component\n */\nexport interface ProgressBarProps {\n readonly type: ProgressBarType;\n readonly current: number;\n readonly max: number;\n readonly label?: { korean: string; english: string };\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly height?: number;\n readonly showText?: boolean;\n readonly animated?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ProgressBar Component\n * \n * A progress bar component for displaying health, ki, and stamina.\n * Uses Korean cyberpunk theming with gradient fills.\n * \n * @example\n * ```tsx\n * <ProgressBar\n * type=\"health\"\n * current={75}\n * max={100}\n * label={{ korean: \"체력\", english: \"Health\" }}\n * />\n * ```\n */\nexport const ProgressBar: React.FC<ProgressBarProps> = ({\n type,\n current,\n max,\n label,\n position = [0, 0, 0],\n width = 200,\n height = 24,\n showText = true,\n animated = true,\n testId,\n}) => {\n const percentage = useMemo(\n () => Math.max(0, Math.min(1, max > 0 ? current / max : 0)),\n [current, max]\n );\n\n const colors = useMemo(() => {\n switch (type) {\n case \"health\":\n if (percentage > 0.6) {\n return {\n start: KOREAN_COLORS.HEALTH_FULL,\n end: KOREAN_COLORS.HEALTH_MEDIUM,\n glow: KOREAN_COLORS.POSITIVE_GREEN,\n };\n } else if (percentage > 0.3) {\n return {\n start: KOREAN_COLORS.HEALTH_MEDIUM,\n end: KOREAN_COLORS.HEALTH_LOW,\n glow: KOREAN_COLORS.WARNING_ORANGE,\n };\n } else {\n return {\n start: KOREAN_COLORS.HEALTH_LOW,\n end: KOREAN_COLORS.HEALTH_CRITICAL,\n glow: KOREAN_COLORS.ACCENT_RED,\n };\n }\n case \"ki\":\n return {\n start: KOREAN_COLORS.KI_FULL,\n end: KOREAN_COLORS.KI_MEDIUM,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n case \"stamina\":\n return {\n start: KOREAN_COLORS.STAMINA_FULL,\n end: KOREAN_COLORS.STAMINA_MEDIUM,\n glow: KOREAN_COLORS.SECONDARY_YELLOW,\n };\n default:\n return {\n start: KOREAN_COLORS.PRIMARY_CYAN,\n end: KOREAN_COLORS.ACCENT_BLUE,\n glow: KOREAN_COLORS.PRIMARY_CYAN,\n };\n }\n }, [type, percentage]);\n\n const containerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: `${width}px`,\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n [width]\n );\n\n const labelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }),\n []\n );\n\n const barContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n width: \"100%\",\n height: `${height}px`,\n background: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.8),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.UI_BORDER, 0.6)}`,\n borderRadius: \"4px\",\n overflow: \"hidden\",\n position: \"relative\",\n }),\n [height]\n );\n\n const fillStyle = useMemo<React.CSSProperties>(() => {\n return {\n width: `${percentage * 100}%`,\n height: \"100%\",\n background: `linear-gradient(to right, ${hexToRgbaString(colors.start)}, ${hexToRgbaString(colors.end)})`,\n transition: animated ? \"width 0.3s ease\" : \"none\",\n position: \"relative\",\n boxShadow: animated\n ? `0 0 10px ${hexToRgbaString(colors.glow, 0.5)}`\n : \"none\",\n };\n }, [percentage, colors, animated]);\n\n const shineStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"0\",\n left: \"0\",\n width: \"60%\",\n height: \"40%\",\n background: hexToRgbaString(KOREAN_COLORS.WHITE_SOLID, 0.3),\n borderRadius: \"4px\",\n margin: \"2px\",\n }),\n []\n );\n\n const textStyle = useMemo<React.CSSProperties>(\n () => ({\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"11px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n textShadow: `\n 0 1px 2px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.8)},\n 0 0 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.6)}\n `,\n whiteSpace: \"nowrap\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div \n style={containerStyle} \n data-testid={testId ?? `progress-bar-${type}`}\n data-current={current}\n data-max={max}\n data-percentage={Math.round(percentage * 100)}\n >\n {/* Label */}\n {label && showText && (\n <div style={labelStyle}>\n <span>\n {label.korean} | {label.english}\n </span>\n <span>\n {Math.ceil(current)} / {max}\n </span>\n </div>\n )}\n\n {/* Bar Container */}\n <div style={barContainerStyle}>\n {/* Fill */}\n <div style={fillStyle}>\n {/* Shine effect */}\n <div style={shineStyle} />\n </div>\n\n {/* Percentage Text Overlay */}\n {showText && (\n <div style={textStyle}>\n {Math.round(percentage * 100)}%\n </div>\n )}\n </div>\n </div>\n </Html>\n );\n};\n\nProgressBar.displayName = \"ProgressBar\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,eAA2C,EACtD,MACA,SACA,KACA,OACA,WAAW;CAAC;CAAG;CAAG;AAAC,GACnB,QAAQ,KACR,SAAS,IACT,WAAW,MACX,WAAW,MACX,aACI;CACJ,MAAM,aAAa,cACX,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,UAAU,MAAM,CAAC,CAAC,GAC1D,CAAC,SAAS,GAAG,CACf;CAEA,MAAM,SAAS,cAAc;EAC3B,QAAQ,MAAR;GACE,KAAK,UACH,IAAI,aAAa,IACf,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;QACK,IAAI,aAAa,IACtB,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;QAEA,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;GAEJ,KAAK,MACH,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;GACF,KAAK,WACH,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;GACF,SACE,OAAO;IACL,OAAO,cAAc;IACrB,KAAK,cAAc;IACnB,MAAM,cAAc;GACtB;EACJ;CACF,GAAG,CAAC,MAAM,UAAU,CAAC;CAErB,MAAM,iBAAiB,eACd;EACL,OAAO,GAAG,MAAM;EAChB,SAAS;EACT,eAAe;EACf,KAAK;CACP,IACA,CAAC,KAAK,CACR;CAEA,MAAM,aAAa,eACV;EACL,YAAY,YAAY;EACxB,UAAU;EACV,YAAY;EACZ,OAAO,gBAAgB,cAAc,YAAY;EACjD,YAAY,aAAa,gBAAgB,cAAc,aAAa,EAAG;EACvE,SAAS;EACT,gBAAgB;EAChB,YAAY;CACd,IACA,CAAC,CACH;CAEA,MAAM,oBAAoB,eACjB;EACL,OAAO;EACP,QAAQ,GAAG,OAAO;EAClB,YAAY,gBAAgB,cAAc,oBAAoB,EAAG;EACjE,QAAQ,aAAa,gBAAgB,cAAc,WAAW,EAAG;EACjE,cAAc;EACd,UAAU;EACV,UAAU;CACZ,IACA,CAAC,MAAM,CACT;CAEA,MAAM,YAAY,cAAmC;EACnD,OAAO;GACL,OAAO,GAAG,aAAa,IAAI;GAC3B,QAAQ;GACR,YAAY,6BAA6B,gBAAgB,OAAO,KAAK,EAAE,IAAI,gBAAgB,OAAO,GAAG,EAAE;GACvG,YAAY,WAAW,oBAAoB;GAC3C,UAAU;GACV,WAAW,WACP,YAAY,gBAAgB,OAAO,MAAM,EAAG,MAC5C;EACN;CACF,GAAG;EAAC;EAAY;EAAQ;CAAQ,CAAC;CAEjC,MAAM,aAAa,eACV;EACL,UAAU;EACV,KAAK;EACL,MAAM;EACN,OAAO;EACP,QAAQ;EACR,YAAY,gBAAgB,cAAc,aAAa,EAAG;EAC1D,cAAc;EACd,QAAQ;CACV,IACA,CAAC,CACH;CAEA,MAAM,YAAY,eACT;EACL,UAAU;EACV,KAAK;EACL,MAAM;EACN,WAAW;EACX,YAAY,YAAY;EACxB,UAAU;EACV,YAAY;EACZ,OAAO,gBAAgB,cAAc,YAAY;EACjD,YAAY;oBACE,gBAAgB,cAAc,aAAa,EAAG,EAAE;kBAClD,gBAAgB,cAAc,aAAa,EAAG,EAAE;;EAE5D,YAAY;CACd,IACA,CAAC,CACH;CAEA,OACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;EACxB,UAAA,qBAAC,OAAD;GACE,OAAO;GACP,eAAa,UAAU,gBAAgB;GACvC,gBAAc;GACd,YAAU;GACV,mBAAiB,KAAK,MAAM,aAAa,GAAG;GAL9C,UAAA,CAQG,SAAS,YACR,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA,CACE,qBAAC,QAAD,EAAA,UAAA;KACG,MAAM;KAAO;KAAI,MAAM;IACpB,EAAA,CAAA,GACN,qBAAC,QAAD,EAAA,UAAA;KACG,KAAK,KAAK,OAAO;KAAE;KAAI;IACpB,EAAA,CAAA,CACH;GAIP,CAAA,GAAA,qBAAC,OAAD;IAAK,OAAO;IAAZ,UAAA,CAEE,oBAAC,OAAD;KAAK,OAAO;KAEV,UAAA,oBAAC,OAAD,EAAK,OAAO,WAAa,CAAA;IACtB,CAAA,GAGJ,YACC,qBAAC,OAAD;KAAK,OAAO;KAAZ,UAAA,CACG,KAAK,MAAM,aAAa,GAAG,GAAE,GAC3B;IAEJ,CAAA,CAAA;GACF,CAAA,CAAA;;CACD,CAAA;AAEV;AAEA,YAAY,cAAc"}
|
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAS,cAAc,cAA8B;CACnD,IAAI;CAEJ,IAAI,gBAAgB,KAClB,aAAa,cAAc;MACtB,IAAI,gBAAgB,IACzB,aAAa,cAAc;MACtB,IAAI,gBAAgB,IACzB,aAAa,cAAc;MACtB,IAAI,gBAAgB,IACzB,aAAa,cAAc;MAE3B,aAAa,cAAc;CAG7B,OAAO,IAAI,WAAW,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;AACpD;;;;AAKA,SAAS,cAAc,cAGrB;CACA,IAAI,gBAAgB,KAClB,OAAO;EAAE,QAAQ;EAAM,SAAS;CAAU;MACrC,IAAI,gBAAgB,IACzB,OAAO;EAAE,QAAQ;EAAM,SAAS;CAAO;MAClC,IAAI,gBAAgB,IACzB,OAAO;EAAE,QAAQ;EAAM,SAAS;CAAU;MACrC,IAAI,gBAAgB,IACzB,OAAO;EAAE,QAAQ;EAAM,SAAS;CAAS;MAEzC,OAAO;EAAE,QAAQ;EAAM,SAAS;CAAW;AAE/C;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,qBAAuD,EAClE,YACA,WACA,UACA,UACA,UAAU,WACN;CACJ,MAAM,YAAY,cAAc;EAC9B,MAAM,eAAe,YAAY,IAAK,aAAa,YAAa,MAAM;EACtE,MAAM,QAAQ,cAAc,YAAY;EACxC,MAAM,QAAQ,cAAc,YAAY;EAExC,OAAO;GACL,cAAc,KAAK,MAAM,YAAY;GACrC;GACA;EACF;CACF,GAAG,CAAC,YAAY,SAAS,CAAC;CAE1B,MAAM,iBAAiB,cAAc;EACnC,MAAM,aAAa,UAAU,gBAAgB;EAC7C,OAAO;GACL,UAAU;GACV,SAAS,UAAU,SAAS;GAC5B,eAAe;GACf,YAAY;GACZ,KAAK,WAAW,QAAQ;GACxB,OAAO;GACP,SAAS,WAAW,aAAa;GACjC,iBAAiB;GACjB,QAAQ,aAAa,UAAU;GAC/B,cAAc;GACd,WAAW,aACP,YAAY,UAAU,MAAM,aAAa,UAAU,MAAM,MACzD,YAAY,UAAU;GAC1B,eAAe;GACf,YAAY;GACZ,WAAW,aAAa,wCAAwC;EAClE;CACF,GAAG;EAAC;EAAU;EAAS,UAAU;EAAO,UAAU;CAAY,CAAC;CAE/D,MAAM,eAAe,eACZ;EACL,YAAY,YAAY;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY;EACZ,OAAO,UAAU;EACjB,YAAY,WAAW,UAAU;EACjC,YAAY;EACZ,QAAQ;CACV,IACA,CAAC,UAAU,UAAU,KAAK,CAC5B;CAEA,MAAM,aAAa,eACV;EACL,YAAY,YAAY;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY;EACZ,OAAO,UAAU;EACjB,SAAS;EACT,eAAe;EACf,YAAY;EACZ,QAAQ;CACV,IACA,CAAC,UAAU,UAAU,KAAK,CAC5B;CAEA,MAAM,mBAAmB,eAChB;EACL,YAAY,YAAY;EACxB,UAAU,WAAW,SAAS;EAC9B,YAAY;EACZ,OAAO,UAAU;EACjB,SAAS;EACT,eAAe;EACf,YAAY;EACZ,QAAQ;CACV,IACA,CAAC,UAAU,UAAU,KAAK,CAC5B;CAEA,OACE,qBAAC,OAAD;EACE,eAAa,mBAAmB;EAChC,WAAU;EACV,OAAO;EACP,cAAY,GAAG,UAAU,MAAM,OAAO,KAAK,UAAU,MAAM,QAAQ,IAAI,UAAU,aAAa;EAC9F,MAAK;EACL,aAAU;
|
|
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;
|
|
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;EAjBF,UAAA,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;GAVF,UAAA,CAYE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,GACxB,qBAAC,QAAD;IAAM,eAAa,iBAAiB;IAApC,UAAA;KACG,KAAK,KAAK,OAAO;KAAE;KAAE;IAClB;GACH,CAAA,CAAA;EAGL,CAAA,GAAA,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,2CAA2C;GACtE;GAEC,UAAA,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"}
|
|
@@ -64,31 +64,32 @@ var TechniqueBar = ({ techniques, player, selectedIndex, cooldowns, onTechniqueS
|
|
|
64
64
|
return hasResources(tech) && !onCooldown;
|
|
65
65
|
};
|
|
66
66
|
const bottomOffset = isMobile ? 100 : 120;
|
|
67
|
+
const containerStyle = embedded ? {
|
|
68
|
+
position: "relative",
|
|
69
|
+
display: "flex",
|
|
70
|
+
justifyContent: layout.shouldScroll ? "flex-start" : "center",
|
|
71
|
+
width: "100%",
|
|
72
|
+
maxWidth: "100%",
|
|
73
|
+
height: `${layout.cardHeight * layout.visualScale}px`,
|
|
74
|
+
pointerEvents: "auto",
|
|
75
|
+
overflowX: layout.shouldScroll ? "auto" : "visible",
|
|
76
|
+
overflowY: "visible",
|
|
77
|
+
scrollSnapType: layout.shouldScroll ? "x proximity" : void 0,
|
|
78
|
+
WebkitOverflowScrolling: layout.shouldScroll ? "touch" : void 0
|
|
79
|
+
} : {
|
|
80
|
+
position: "absolute",
|
|
81
|
+
left: "50%",
|
|
82
|
+
bottom: `${bottomOffset}px`,
|
|
83
|
+
transform: "translateX(-50%)",
|
|
84
|
+
width: `${layout.totalWidth}px`,
|
|
85
|
+
height: `${layout.cardHeight}px`,
|
|
86
|
+
display: "flex",
|
|
87
|
+
gap: `${layout.gap}px`,
|
|
88
|
+
pointerEvents: "auto",
|
|
89
|
+
zIndex: 100
|
|
90
|
+
};
|
|
67
91
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
68
|
-
style:
|
|
69
|
-
position: "relative",
|
|
70
|
-
display: "flex",
|
|
71
|
-
justifyContent: layout.shouldScroll ? "flex-start" : "center",
|
|
72
|
-
width: "100%",
|
|
73
|
-
maxWidth: "100%",
|
|
74
|
-
height: `${layout.cardHeight * layout.visualScale}px`,
|
|
75
|
-
pointerEvents: "auto",
|
|
76
|
-
overflowX: layout.shouldScroll ? "auto" : "visible",
|
|
77
|
-
overflowY: "visible",
|
|
78
|
-
scrollSnapType: layout.shouldScroll ? "x proximity" : void 0,
|
|
79
|
-
WebkitOverflowScrolling: layout.shouldScroll ? "touch" : void 0
|
|
80
|
-
} : {
|
|
81
|
-
position: "absolute",
|
|
82
|
-
left: "50%",
|
|
83
|
-
bottom: `${bottomOffset}px`,
|
|
84
|
-
transform: "translateX(-50%)",
|
|
85
|
-
width: `${layout.totalWidth}px`,
|
|
86
|
-
height: `${layout.cardHeight}px`,
|
|
87
|
-
display: "flex",
|
|
88
|
-
gap: `${layout.gap}px`,
|
|
89
|
-
pointerEvents: "auto",
|
|
90
|
-
zIndex: 100
|
|
91
|
-
},
|
|
92
|
+
style: containerStyle,
|
|
92
93
|
"data-testid": "technique-bar",
|
|
93
94
|
children: /* @__PURE__ */ jsx("div", {
|
|
94
95
|
style: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TechniqueBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/TechniqueBar.tsx"],"sourcesContent":["/**\n * TechniqueBar Component\n *\n * **Korean**: 기술 바 컴포넌트 (Technique Bar Component)\n *\n * Horizontal bar displaying 3-5 technique cards at the bottom-center of combat HUD.\n * Manages technique selection, resource availability, and cooldown states.\n *\n * @module components/shared/three/ui/TechniqueBar\n * @category Shared UI\n * @korean 기술바\n */\n\nimport React, { useMemo } from \"react\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { Technique } from \"../../../../types\";\nimport {\n HUD_SIDE_CONTROL_RESERVES,\n TECHNIQUE_BAR_MIN_READABLE_SCALE,\n} from \"../../../../types/constants/layout\";\nimport { TechniqueCard } from \"./TechniqueCard\";\n\n/**\n * Props for TechniqueBar component.\n */\nexport interface TechniqueBarProps {\n /** Available techniques for player */\n readonly techniques: readonly Technique[];\n\n /** Player state with resources */\n readonly player: PlayerState;\n\n /** Index of currently selected technique */\n readonly selectedIndex: number;\n\n /** Active cooldowns map (techniqueId -> remaining ms) */\n readonly cooldowns: ReadonlyMap<string, number>;\n\n /** Callback when technique is selected */\n readonly onTechniqueSelect: (index: number) => void;\n\n /** Callback when hovering technique (for tooltip) */\n readonly onTechniqueHover: (technique: Technique | null) => void;\n\n /** Whether rendering for mobile device */\n readonly isMobile: boolean;\n\n /** Screen width for positioning */\n readonly screenWidth: number;\n\n /** Screen height for positioning */\n readonly screenHeight: number;\n\n /** Whether to use embedded mode (relative positioning, no absolute) */\n readonly embedded?: boolean;\n\n /**\n * Actual available pixel width of the container when in embedded mode.\n *\n * Embedded parents (TrainingBottomHUD, CombatBottomHUD) reserve space for\n * side controls via margins/padding, so the real container width is smaller\n * than `screenWidth`. Passing the pre-computed pixel width here ensures\n * the rawScale / shouldScroll decision is accurate and prevents cards from\n * overflowing back under side controls.\n *\n * When omitted in embedded mode, the component falls back to\n * `screenWidth − 2 × HUD_SIDE_CONTROL_RESERVES` (previous behaviour).\n */\n readonly containerWidth?: number;\n}\n\n/**\n * TechniqueBar Component\n *\n * Displays a horizontal bar of technique cards positioned at the bottom-center\n * of the combat screen. Each card shows technique details and availability.\n *\n * @param props - Component props\n * @returns TechniqueBar component\n */\nexport const TechniqueBar: React.FC<TechniqueBarProps> = ({\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n onTechniqueHover,\n isMobile,\n screenWidth,\n screenHeight,\n embedded = false,\n containerWidth,\n}) => {\n const layout = useMemo(() => {\n const cardWidth = isMobile ? 70 : 90;\n const cardHeight = isMobile ? 80 : 100;\n const gap = isMobile ? 8 : 12;\n const totalWidth = Math.max(\n 0,\n techniques.length * cardWidth + (techniques.length - 1) * gap,\n );\n\n let availableWidth: number;\n if (embedded && containerWidth !== undefined) {\n availableWidth = Math.max(cardWidth, containerWidth);\n } else {\n const reservedSideWidth = embedded\n ? (isMobile\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_DESKTOP)\n : 0;\n availableWidth = Math.max(cardWidth, screenWidth - reservedSideWidth * 2);\n }\n const rawScale =\n totalWidth > 0 ? Math.min(1, availableWidth / totalWidth) : 1;\n const shouldScroll =\n embedded && rawScale < TECHNIQUE_BAR_MIN_READABLE_SCALE;\n const visualScale = shouldScroll ? 1 : rawScale;\n\n return {\n cardWidth,\n cardHeight,\n gap,\n totalWidth,\n visualScale,\n shouldScroll,\n startX: (screenWidth - totalWidth) / 2,\n startY: screenHeight - cardHeight - (isMobile ? 100 : 120),\n };\n }, [techniques.length, isMobile, screenWidth, screenHeight, embedded, containerWidth]);\n\n const hasResources = (tech: Technique): boolean => {\n return player.stamina >= tech.staminaCost && player.ki >= tech.kiCost;\n };\n\n const isAvailable = (tech: Technique): boolean => {\n const onCooldown = (cooldowns.get(tech.id) ?? 0) > 0;\n return hasResources(tech) && !onCooldown;\n };\n\n const bottomOffset = isMobile ? 100 : 120;\n\n const containerStyle: React.CSSProperties = embedded\n ? {\n position: \"relative\",\n display: \"flex\",\n justifyContent: layout.shouldScroll ? \"flex-start\" : \"center\",\n width: \"100%\",\n maxWidth: \"100%\",\n height: `${layout.cardHeight * layout.visualScale}px`,\n pointerEvents: \"auto\",\n overflowX: layout.shouldScroll ? \"auto\" : \"visible\",\n overflowY: \"visible\",\n scrollSnapType: layout.shouldScroll ? \"x proximity\" : undefined,\n WebkitOverflowScrolling: layout.shouldScroll ? \"touch\" : undefined,\n }\n : {\n position: \"absolute\",\n left: \"50%\",\n bottom: `${bottomOffset}px`,\n transform: \"translateX(-50%)\",\n width: `${layout.totalWidth}px`,\n height: `${layout.cardHeight}px`,\n display: \"flex\",\n gap: `${layout.gap}px`,\n pointerEvents: \"auto\",\n zIndex: 100,\n };\n\n return (\n <>\n {/* Technique Bar Container */}\n <div style={containerStyle} data-testid=\"technique-bar\">\n <div\n style={{\n display: \"flex\",\n gap: `${layout.gap}px`,\n justifyContent: \"center\",\n transform: embedded ? `scale(${layout.visualScale})` : undefined,\n transformOrigin: embedded\n ? layout.shouldScroll\n ? \"left center\"\n : \"center bottom\"\n : undefined,\n paddingInline: layout.shouldScroll ? \"8px\" : undefined,\n flexShrink: 0,\n }}\n >\n {techniques.map((technique, index) => {\n const cooldownRemaining = cooldowns.get(technique.id) ?? 0;\n const available = isAvailable(technique);\n\n return (\n <div\n key={technique.id}\n data-testid={`technique-slot-${index}`}\n style={layout.shouldScroll ? { scrollSnapAlign: \"start\" } : undefined}\n >\n <TechniqueCard\n technique={technique}\n isSelected={selectedIndex === index}\n isAvailable={available}\n staminaCost={technique.staminaCost}\n kiCost={technique.kiCost}\n remainingCooldown={cooldownRemaining}\n keyboardShortcut={technique.keyboardShortcut}\n onClick={() => onTechniqueSelect(index)}\n onHover={onTechniqueHover}\n isMobile={isMobile}\n playerArchetype={player.archetype}\n playerStance={player.currentStance}\n />\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Keyboard Hints - only shown in embedded mode or non-mobile */}\n {!isMobile && (\n <div\n style={{\n position: embedded ? \"relative\" : \"absolute\",\n left: embedded ? undefined : \"50%\",\n bottom: embedded\n ? undefined\n : `${bottomOffset - layout.cardHeight - 20}px`,\n transform: embedded ? undefined : \"translateX(-50%)\",\n width: embedded ? \"100%\" : `${layout.totalWidth}px`,\n textAlign: \"center\",\n fontSize: \"11px\",\n color: \"#aaa\",\n fontFamily: \"monospace\",\n pointerEvents: \"none\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n marginTop: embedded ? \"8px\" : undefined,\n }}\n >\n 기술 실행: Q-E-R-T-Y-F-G-Z-X-C | Press technique keys to execute\n </div>\n )}\n </>\n );\n};\n\nexport default TechniqueBar;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TechniqueBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/TechniqueBar.tsx"],"sourcesContent":["/**\n * TechniqueBar Component\n *\n * **Korean**: 기술 바 컴포넌트 (Technique Bar Component)\n *\n * Horizontal bar displaying 3-5 technique cards at the bottom-center of combat HUD.\n * Manages technique selection, resource availability, and cooldown states.\n *\n * @module components/shared/three/ui/TechniqueBar\n * @category Shared UI\n * @korean 기술바\n */\n\nimport React, { useMemo } from \"react\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { Technique } from \"../../../../types\";\nimport {\n HUD_SIDE_CONTROL_RESERVES,\n TECHNIQUE_BAR_MIN_READABLE_SCALE,\n} from \"../../../../types/constants/layout\";\nimport { TechniqueCard } from \"./TechniqueCard\";\n\n/**\n * Props for TechniqueBar component.\n */\nexport interface TechniqueBarProps {\n /** Available techniques for player */\n readonly techniques: readonly Technique[];\n\n /** Player state with resources */\n readonly player: PlayerState;\n\n /** Index of currently selected technique */\n readonly selectedIndex: number;\n\n /** Active cooldowns map (techniqueId -> remaining ms) */\n readonly cooldowns: ReadonlyMap<string, number>;\n\n /** Callback when technique is selected */\n readonly onTechniqueSelect: (index: number) => void;\n\n /** Callback when hovering technique (for tooltip) */\n readonly onTechniqueHover: (technique: Technique | null) => void;\n\n /** Whether rendering for mobile device */\n readonly isMobile: boolean;\n\n /** Screen width for positioning */\n readonly screenWidth: number;\n\n /** Screen height for positioning */\n readonly screenHeight: number;\n\n /** Whether to use embedded mode (relative positioning, no absolute) */\n readonly embedded?: boolean;\n\n /**\n * Actual available pixel width of the container when in embedded mode.\n *\n * Embedded parents (TrainingBottomHUD, CombatBottomHUD) reserve space for\n * side controls via margins/padding, so the real container width is smaller\n * than `screenWidth`. Passing the pre-computed pixel width here ensures\n * the rawScale / shouldScroll decision is accurate and prevents cards from\n * overflowing back under side controls.\n *\n * When omitted in embedded mode, the component falls back to\n * `screenWidth − 2 × HUD_SIDE_CONTROL_RESERVES` (previous behaviour).\n */\n readonly containerWidth?: number;\n}\n\n/**\n * TechniqueBar Component\n *\n * Displays a horizontal bar of technique cards positioned at the bottom-center\n * of the combat screen. Each card shows technique details and availability.\n *\n * @param props - Component props\n * @returns TechniqueBar component\n */\nexport const TechniqueBar: React.FC<TechniqueBarProps> = ({\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n onTechniqueHover,\n isMobile,\n screenWidth,\n screenHeight,\n embedded = false,\n containerWidth,\n}) => {\n const layout = useMemo(() => {\n const cardWidth = isMobile ? 70 : 90;\n const cardHeight = isMobile ? 80 : 100;\n const gap = isMobile ? 8 : 12;\n const totalWidth = Math.max(\n 0,\n techniques.length * cardWidth + (techniques.length - 1) * gap,\n );\n\n let availableWidth: number;\n if (embedded && containerWidth !== undefined) {\n availableWidth = Math.max(cardWidth, containerWidth);\n } else {\n const reservedSideWidth = embedded\n ? (isMobile\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_DESKTOP)\n : 0;\n availableWidth = Math.max(cardWidth, screenWidth - reservedSideWidth * 2);\n }\n const rawScale =\n totalWidth > 0 ? Math.min(1, availableWidth / totalWidth) : 1;\n const shouldScroll =\n embedded && rawScale < TECHNIQUE_BAR_MIN_READABLE_SCALE;\n const visualScale = shouldScroll ? 1 : rawScale;\n\n return {\n cardWidth,\n cardHeight,\n gap,\n totalWidth,\n visualScale,\n shouldScroll,\n startX: (screenWidth - totalWidth) / 2,\n startY: screenHeight - cardHeight - (isMobile ? 100 : 120),\n };\n }, [techniques.length, isMobile, screenWidth, screenHeight, embedded, containerWidth]);\n\n const hasResources = (tech: Technique): boolean => {\n return player.stamina >= tech.staminaCost && player.ki >= tech.kiCost;\n };\n\n const isAvailable = (tech: Technique): boolean => {\n const onCooldown = (cooldowns.get(tech.id) ?? 0) > 0;\n return hasResources(tech) && !onCooldown;\n };\n\n const bottomOffset = isMobile ? 100 : 120;\n\n const containerStyle: React.CSSProperties = embedded\n ? {\n position: \"relative\",\n display: \"flex\",\n justifyContent: layout.shouldScroll ? \"flex-start\" : \"center\",\n width: \"100%\",\n maxWidth: \"100%\",\n height: `${layout.cardHeight * layout.visualScale}px`,\n pointerEvents: \"auto\",\n overflowX: layout.shouldScroll ? \"auto\" : \"visible\",\n overflowY: \"visible\",\n scrollSnapType: layout.shouldScroll ? \"x proximity\" : undefined,\n WebkitOverflowScrolling: layout.shouldScroll ? \"touch\" : undefined,\n }\n : {\n position: \"absolute\",\n left: \"50%\",\n bottom: `${bottomOffset}px`,\n transform: \"translateX(-50%)\",\n width: `${layout.totalWidth}px`,\n height: `${layout.cardHeight}px`,\n display: \"flex\",\n gap: `${layout.gap}px`,\n pointerEvents: \"auto\",\n zIndex: 100,\n };\n\n return (\n <>\n {/* Technique Bar Container */}\n <div style={containerStyle} data-testid=\"technique-bar\">\n <div\n style={{\n display: \"flex\",\n gap: `${layout.gap}px`,\n justifyContent: \"center\",\n transform: embedded ? `scale(${layout.visualScale})` : undefined,\n transformOrigin: embedded\n ? layout.shouldScroll\n ? \"left center\"\n : \"center bottom\"\n : undefined,\n paddingInline: layout.shouldScroll ? \"8px\" : undefined,\n flexShrink: 0,\n }}\n >\n {techniques.map((technique, index) => {\n const cooldownRemaining = cooldowns.get(technique.id) ?? 0;\n const available = isAvailable(technique);\n\n return (\n <div\n key={technique.id}\n data-testid={`technique-slot-${index}`}\n style={layout.shouldScroll ? { scrollSnapAlign: \"start\" } : undefined}\n >\n <TechniqueCard\n technique={technique}\n isSelected={selectedIndex === index}\n isAvailable={available}\n staminaCost={technique.staminaCost}\n kiCost={technique.kiCost}\n remainingCooldown={cooldownRemaining}\n keyboardShortcut={technique.keyboardShortcut}\n onClick={() => onTechniqueSelect(index)}\n onHover={onTechniqueHover}\n isMobile={isMobile}\n playerArchetype={player.archetype}\n playerStance={player.currentStance}\n />\n </div>\n );\n })}\n </div>\n </div>\n\n {/* Keyboard Hints - only shown in embedded mode or non-mobile */}\n {!isMobile && (\n <div\n style={{\n position: embedded ? \"relative\" : \"absolute\",\n left: embedded ? undefined : \"50%\",\n bottom: embedded\n ? undefined\n : `${bottomOffset - layout.cardHeight - 20}px`,\n transform: embedded ? undefined : \"translateX(-50%)\",\n width: embedded ? \"100%\" : `${layout.totalWidth}px`,\n textAlign: \"center\",\n fontSize: \"11px\",\n color: \"#aaa\",\n fontFamily: \"monospace\",\n pointerEvents: \"none\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n marginTop: embedded ? \"8px\" : undefined,\n }}\n >\n 기술 실행: Q-E-R-T-Y-F-G-Z-X-C | Press technique keys to execute\n </div>\n )}\n </>\n );\n};\n\nexport default TechniqueBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,IAAa,gBAA6C,EACxD,YACA,QACA,eACA,WACA,mBACA,kBACA,UACA,aACA,cACA,WAAW,OACX,qBACI;CACJ,MAAM,SAAS,cAAc;EAC3B,MAAM,YAAY,WAAW,KAAK;EAClC,MAAM,aAAa,WAAW,KAAK;EACnC,MAAM,MAAM,WAAW,IAAI;EAC3B,MAAM,aAAa,KAAK,IACtB,GACA,WAAW,SAAS,aAAa,WAAW,SAAS,KAAK,GAC5D;EAEA,IAAI;EACJ,IAAI,YAAY,mBAAmB,KAAA,GACjC,iBAAiB,KAAK,IAAI,WAAW,cAAc;OAC9C;GACL,MAAM,oBAAoB,WACrB,WACG,0BAA0B,uBAC1B,0BAA0B,wBAC9B;GACJ,iBAAiB,KAAK,IAAI,WAAW,cAAc,oBAAoB,CAAC;EAC1E;EACA,MAAM,WACJ,aAAa,IAAI,KAAK,IAAI,GAAG,iBAAiB,UAAU,IAAI;EAC9D,MAAM,eACJ,YAAY,WAAA;EAGd,OAAO;GACL;GACA;GACA;GACA;GACA,aAPkB,eAAe,IAAI;GAQrC;GACA,SAAS,cAAc,cAAc;GACrC,QAAQ,eAAe,cAAc,WAAW,MAAM;EACxD;CACF,GAAG;EAAC,WAAW;EAAQ;EAAU;EAAa;EAAc;EAAU;CAAc,CAAC;CAErF,MAAM,gBAAgB,SAA6B;EACjD,OAAO,OAAO,WAAW,KAAK,eAAe,OAAO,MAAM,KAAK;CACjE;CAEA,MAAM,eAAe,SAA6B;EAChD,MAAM,cAAc,UAAU,IAAI,KAAK,EAAE,KAAK,KAAK;EACnD,OAAO,aAAa,IAAI,KAAK,CAAC;CAChC;CAEA,MAAM,eAAe,WAAW,MAAM;CAEtC,MAAM,iBAAsC,WACxC;EACE,UAAU;EACV,SAAS;EACT,gBAAgB,OAAO,eAAe,eAAe;EACrD,OAAO;EACP,UAAU;EACV,QAAQ,GAAG,OAAO,aAAa,OAAO,YAAY;EAClD,eAAe;EACf,WAAW,OAAO,eAAe,SAAS;EAC1C,WAAW;EACX,gBAAgB,OAAO,eAAe,gBAAgB,KAAA;EACtD,yBAAyB,OAAO,eAAe,UAAU,KAAA;CAC3D,IACA;EACE,UAAU;EACV,MAAM;EACN,QAAQ,GAAG,aAAa;EACxB,WAAW;EACX,OAAO,GAAG,OAAO,WAAW;EAC5B,QAAQ,GAAG,OAAO,WAAW;EAC7B,SAAS;EACT,KAAK,GAAG,OAAO,IAAI;EACnB,eAAe;EACf,QAAQ;CACV;CAEJ,OACE,qBAAA,UAAA,EAAA,UAAA,CAEE,oBAAC,OAAD;EAAK,OAAO;EAAgB,eAAY;EACtC,UAAA,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK,GAAG,OAAO,IAAI;IACnB,gBAAgB;IAChB,WAAW,WAAW,SAAS,OAAO,YAAY,KAAK,KAAA;IACvD,iBAAiB,WACb,OAAO,eACL,gBACA,kBACF,KAAA;IACJ,eAAe,OAAO,eAAe,QAAQ,KAAA;IAC7C,YAAY;GACd;GAEC,UAAA,WAAW,KAAK,WAAW,UAAU;IACpC,MAAM,oBAAoB,UAAU,IAAI,UAAU,EAAE,KAAK;IACzD,MAAM,YAAY,YAAY,SAAS;IAEvC,OACE,oBAAC,OAAD;KAEE,eAAa,kBAAkB;KAC/B,OAAO,OAAO,eAAe,EAAE,iBAAiB,QAAQ,IAAI,KAAA;KAE5D,UAAA,oBAAC,eAAD;MACa;MACX,YAAY,kBAAkB;MAC9B,aAAa;MACb,aAAa,UAAU;MACvB,QAAQ,UAAU;MAClB,mBAAmB;MACnB,kBAAkB,UAAU;MAC5B,eAAe,kBAAkB,KAAK;MACtC,SAAS;MACC;MACV,iBAAiB,OAAO;MACxB,cAAc,OAAO;KACtB,CAAA;IACE,GAlBE,UAAU,EAkBZ;GAET,CAAC;EACE,CAAA;CACF,CAAA,GAGJ,CAAC,YACA,oBAAC,OAAD;EACE,OAAO;GACL,UAAU,WAAW,aAAa;GAClC,MAAM,WAAW,KAAA,IAAY;GAC7B,QAAQ,WACJ,KAAA,IACA,GAAG,eAAe,OAAO,aAAa,GAAG;GAC7C,WAAW,WAAW,KAAA,IAAY;GAClC,OAAO,WAAW,SAAS,GAAG,OAAO,WAAW;GAChD,WAAW;GACX,UAAU;GACV,OAAO;GACP,YAAY;GACZ,eAAe;GACf,YAAY;GACZ,WAAW,WAAW,QAAQ,KAAA;EAChC;EACD,UAAA;CAEI,CAAA,CAEP,EAAA,CAAA;AAEN"}
|
|
@@ -50,9 +50,7 @@ var TechniqueCard = ({ technique, isSelected, isAvailable, staminaCost, kiCost,
|
|
|
50
50
|
case "knee":
|
|
51
51
|
bodyPart = "Leg (다리)";
|
|
52
52
|
break;
|
|
53
|
-
default:
|
|
54
|
-
bodyPart = "Body (몸통)";
|
|
55
|
-
break;
|
|
53
|
+
default: bodyPart = "Body (몸통)";
|
|
56
54
|
}
|
|
57
55
|
return {
|
|
58
56
|
maxReach: (maxReach * 100).toFixed(1),
|
|
@@ -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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"}
|