blacktrigram 0.7.45 → 0.7.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioAssetLoader.js.map +1 -1
- package/lib/audio/AudioAssetRegistry.js.map +1 -1
- package/lib/audio/AudioCache.js.map +1 -1
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioMonitor.js.map +1 -1
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/AudioProvider.js.map +1 -1
- package/lib/audio/AudioUtils.js.map +1 -1
- package/lib/audio/BoneImpactAudioMap.js.map +1 -1
- package/lib/audio/VariantSelector.js.map +1 -1
- package/lib/audio/types.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.d.ts.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +22 -11
- 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.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ParticleAudio3D.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.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/MobileControlsWrapper.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.d.ts.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.js +4 -2
- package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
- package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatState.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.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- 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/controls/constants/ControlsConstants.js.map +1 -1
- package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js.map +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.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- 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.d.ts.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +1 -0
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.d.ts +1 -0
- package/lib/components/screens/training/hooks/useTrainingActions.d.ts.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +6 -4
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
- package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.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/TouchOptimizer.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- 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.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- 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.map +1 -1
- package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/HapticFeedback.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.d.ts.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js +7 -5
- 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/BackgroundScene3D.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.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/KoreanButton.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -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.map +1 -1
- 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.map +1 -1
- 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/constants/bodyDimensions.js.map +1 -1
- package/lib/constants/bodyRenderingConstants.js.map +1 -1
- package/lib/data/archetypeClothing.js.map +1 -1
- package/lib/data/archetypePhysicalAttributes.js.map +1 -1
- package/lib/data/techniqueMappings.js.map +1 -1
- package/lib/data/techniques.js.map +1 -1
- package/lib/hooks/useActionFeedback.js.map +1 -1
- package/lib/hooks/useBalanceAnimations.js.map +1 -1
- package/lib/hooks/useCombatTimer.js.map +1 -1
- package/lib/hooks/useDebounce.js.map +1 -1
- package/lib/hooks/useHUDLayout.js.map +1 -1
- package/lib/hooks/useHandPoseTransitions.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useMatchCountdown.js.map +1 -1
- package/lib/hooks/useMuscleActivation.js.map +1 -1
- package/lib/hooks/usePauseMenu.js.map +1 -1
- package/lib/hooks/usePlayerAnimation.js.map +1 -1
- package/lib/hooks/useResponsiveLayout.js.map +1 -1
- package/lib/hooks/useRoundTransition.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.d.ts.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +1 -1
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTechniqueSelection.js.map +1 -1
- package/lib/hooks/useThrottle.js.map +1 -1
- package/lib/hooks/useTouchControls.js.map +1 -1
- package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
- package/lib/hooks/useWindowSize.js.map +1 -1
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/EffectCalculator.js.map +1 -1
- package/lib/systems/LayoutSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ResponsiveScaling.js.map +1 -1
- package/lib/systems/TrigramSystem.js.map +1 -1
- package/lib/systems/VitalPointSystem.js.map +1 -1
- package/lib/systems/ai/AIPersonality.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js +16 -16
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
- package/lib/systems/ai/ComboSystem.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/ai/TrainingAI.js.map +1 -1
- package/lib/systems/ai/types.js.map +1 -1
- package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/HandPoses.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.d.ts +6 -0
- package/lib/systems/animation/builders/KickPhaseApplicator.d.ts.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.js +16 -9
- package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/KoreanGuardPositions.d.ts +4 -4
- package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js +5 -5
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.d.ts +112 -71
- package/lib/systems/animation/builders/MartialArtsConstants.d.ts.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.js +113 -72
- package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
- package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
- package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
- package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/EnhancedAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/EnhancedElbowKneeAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamStanceAnimations.js +21 -0
- package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -0
- package/lib/systems/animation/catalogs/GamTechniqueAnimations.js +34 -2
- package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts +9 -0
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts.map +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +288 -0
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -0
- package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/KickAnimations.d.ts +2 -2
- package/lib/systems/animation/catalogs/KickAnimations.js +2 -2
- package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js +14 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.d.ts +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceGuardPoses.d.ts +6 -6
- package/lib/systems/animation/catalogs/StanceGuardPoses.js +36 -36
- package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
- package/lib/systems/animation/constants/AnatomicalLimits.js.map +1 -1
- package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
- package/lib/systems/animation/core/AnimationPriority.js +15 -15
- package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.d.ts +30 -0
- package/lib/systems/animation/core/AnimationRegistry.d.ts.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.js +74 -12
- package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js +16 -16
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.d.ts.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.js +34 -0
- package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
- package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
- package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
- package/lib/systems/animation/core/index.d.ts +1 -1
- package/lib/systems/animation/core/index.d.ts.map +1 -1
- package/lib/systems/animation/core/types.d.ts +24 -0
- package/lib/systems/animation/core/types.d.ts.map +1 -1
- package/lib/systems/animation/core/types.js +27 -11
- package/lib/systems/animation/core/types.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
- package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
- package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
- package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
- package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
- package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/bodypart/types.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js +19 -19
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/breathing/feedback.js.map +1 -1
- package/lib/systems/breathing/integration.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js +19 -19
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js +17 -17
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js +24 -24
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/FallIntegration.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js +21 -21
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
- package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
- package/lib/systems/combat/typeGuards.js.map +1 -1
- package/lib/systems/effects.js.map +1 -1
- package/lib/systems/game.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
- package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
- package/lib/systems/movement/integration.js.map +1 -1
- package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
- package/lib/systems/physics/CollisionDetection.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
- package/lib/systems/physics/SpeedModifierSystem.js +6 -6
- package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
- package/lib/systems/trigram/KoreanCulture.js.map +1 -1
- package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
- package/lib/systems/trigram/StanceManager.js.map +1 -1
- package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
- package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
- package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GeonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/JinTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/LiTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TaeTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
- package/lib/systems/trigram/techniques/index.js.map +1 -1
- package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
- package/lib/systems/trigram/types.js.map +1 -1
- package/lib/systems/types.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
- package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
- package/lib/systems/vitalpoint/VitalPointsData.js.map +1 -1
- package/lib/types/AccessibilityTypes.js.map +1 -1
- package/lib/types/LayoutTypes.js.map +1 -1
- package/lib/types/PhysicsTypes.js.map +1 -1
- package/lib/types/common.js.map +1 -1
- package/lib/types/constants/animations.js.map +1 -1
- package/lib/types/constants/colors.js.map +1 -1
- package/lib/types/constants/designSystem.js.map +1 -1
- package/lib/types/constants/index.js.map +1 -1
- package/lib/types/constants/layout.js.map +1 -1
- package/lib/types/constants/performance.js.map +1 -1
- package/lib/types/constants/typography.js.map +1 -1
- package/lib/types/constants/ui.js.map +1 -1
- package/lib/types/facial.js +19 -19
- package/lib/types/facial.js.map +1 -1
- package/lib/types/hand-animation.js.map +1 -1
- package/lib/types/injury.js.map +1 -1
- package/lib/types/muscle.js.map +1 -1
- package/lib/types/physics.js.map +1 -1
- package/lib/types/physicsConstants.js.map +1 -1
- package/lib/types/player-visual.d.ts +1 -1
- package/lib/types/player-visual.d.ts.map +1 -1
- package/lib/types/skeletal.js.map +1 -1
- package/lib/types/techniqueId.js.map +1 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/arenaWorldDimensions.js.map +1 -1
- package/lib/utils/assetConfig.js.map +1 -1
- package/lib/utils/characterScaling.js.map +1 -1
- package/lib/utils/colorHelpers.js.map +1 -1
- package/lib/utils/colorUtils.js.map +1 -1
- package/lib/utils/combatReadiness.js.map +1 -1
- package/lib/utils/controlMapping.js.map +1 -1
- package/lib/utils/deviceDetection.js +6 -7
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/fabricTextures.js.map +1 -1
- package/lib/utils/hapticFeedback.js.map +1 -1
- package/lib/utils/haptics.js.map +1 -1
- package/lib/utils/htmlOverlayHelpers.js.map +1 -1
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/math.js.map +1 -1
- package/lib/utils/mobileLayoutHelpers.js.map +1 -1
- package/lib/utils/mobileUIUtils.js.map +1 -1
- package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
- package/lib/utils/performanceOptimization.js.map +1 -1
- package/lib/utils/player3DHelpers.js.map +1 -1
- package/lib/utils/playerUtils.js.map +1 -1
- package/lib/utils/responsiveLayout.js.map +1 -1
- package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
- package/lib/utils/responsiveOrientationConstants.js.map +1 -1
- package/lib/utils/safeAreaUtils.js.map +1 -1
- package/lib/utils/sharedPhysicsConfig.js.map +1 -1
- package/lib/utils/skeletonScaling.js.map +1 -1
- package/lib/utils/stanceHelpers.js.map +1 -1
- package/lib/utils/threeObjectPool.js.map +1 -1
- package/lib/utils/visualEffects.js.map +1 -1
- package/package.json +4 -4
|
@@ -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,
|
|
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,
|
|
1
|
+
{"version":3,"file":"StaminaBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/StaminaBar.tsx"],"sourcesContent":["/**\n * StaminaBar Component - Segmented stamina display with Korean theming\n * \n * Displays player stamina with:\n * - 5 segmented bars\n * - Consistent cyan/blue theming\n * - Pulse animation when stamina <20%\n * - Korean/English bilingual labels\n * - Numeric value display (e.g., \"45/50\")\n * - Responsive sizing for mobile/tablet/desktop\n * - Smooth transitions and glow effects\n * \n * Performance: Uses React.memo with shallow comparison for 60fps optimization.\n * Note: React.memo uses shallow comparison by default, which works correctly\n * for this component since all props are primitives (number, string, boolean).\n * If object or function props are added in the future, consider adding a\n * custom comparison function or using useCallback/useMemo for prop stability.\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./HUDAnimations.css\";\n\nexport interface StaminaBarProps {\n /** Current stamina value */\n readonly current: number;\n /** Maximum stamina capacity */\n readonly max: number;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * StaminaBar - Segmented stamina display with Korean theming\n * Performance optimized with React.memo\n */\nexport const StaminaBar: React.FC<StaminaBarProps> = React.memo(({\n current,\n max,\n playerId,\n isMobile,\n}) => {\n const staminaPercent = useMemo(\n () => Math.max(0, Math.min(100, (current / max) * 100)),\n [current, max]\n );\n\n const segments = 5;\n const filledSegments = Math.ceil((staminaPercent / 100) * segments);\n const shouldPulse = staminaPercent < 20;\n\n const layout = useMemo(() => ({\n barWidth: isMobile ? 180 : 250,\n barHeight: isMobile ? 10 : 12,\n fontSize: isMobile ? 10 : 11,\n padding: isMobile ? \"6px 8px\" : \"8px 12px\",\n }), [isMobile]);\n\n return (\n <div\n data-testid={`stamina-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"기력 | Stamina\"\n aria-valuenow={Math.ceil(current)}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuetext={`${Math.ceil(current)} out of ${max}`}\n className=\"hud-animated\"\n style={{\n width: `${layout.barWidth}px`,\n padding: layout.padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)}`,\n boxShadow: `0 0 8px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.2)}`,\n transition: \"box-shadow 0.3s ease-in-out, border-color 0.3s ease-in-out\",\n }}\n >\n {/* Label and numeric display */}\n <div\n style={{\n fontSize: `${layout.fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"3px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n transition: \"color 0.2s ease-in-out\",\n }}\n >\n <span>기력 | Stamina</span>\n <span data-testid={`stamina-value-${playerId}`}>\n {Math.ceil(current)}/{max}\n </span>\n </div>\n\n {/* Segmented stamina bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"4px\",\n height: `${layout.barHeight}px`,\n animation: shouldPulse ? \"staminaPulse 0.8s ease-in-out infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`stamina-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.2s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 6px ${hexToRgbaString(KOREAN_COLORS.ACCENT_BLUE, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n </div>\n );\n});\n\nStaminaBar.displayName = \"StaminaBar\";\n\nexport default StaminaBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,aAAwC,MAAM,MAAM,EAC/D,SACA,KACA,UACA,eACI;CACJ,MAAM,iBAAiB,cACf,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,UAAU,MAAO,GAAG,CAAC,GACtD,CAAC,SAAS,GAAG,CACf;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,KAAK,KAAM,iBAAiB,MAAO,QAAQ;CAClE,MAAM,cAAc,iBAAiB;CAErC,MAAM,SAAS,eAAe;EAC5B,UAAU,WAAW,MAAM;EAC3B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,YAAY;CAClC,IAAI,CAAC,QAAQ,CAAC;CAEd,OACE,qBAAC,OAAD;EACE,eAAa,eAAe;EAC5B,MAAK;EACL,cAAW;EACX,iBAAe,KAAK,KAAK,OAAO;EAChC,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,KAAK,KAAK,OAAO,EAAE,UAAU;EAChD,WAAU;EACV,OAAO;GACL,OAAO,GAAG,OAAO,SAAS;GAC1B,SAAS,OAAO;GAChB,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,aAAa,CAAC;GACjE,WAAW,WAAW,gBAAgB,cAAc,aAAa,EAAG;GACpE,YAAY;EACd;YAjBF,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,GAAG,OAAO,SAAS;IAC7B,OAAO,gBAAgB,cAAc,aAAa,CAAC;IACnD,YAAY,YAAY;IACxB,cAAc;IACd,SAAS;IACT,gBAAgB;IAChB,YAAY;IACZ,YAAY;GACd;aAVF,CAYE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,GACxB,qBAAC,QAAD;IAAM,eAAa,iBAAiB;cAApC;KACG,KAAK,KAAK,OAAO;KAAE;KAAE;IAClB;KACH;MAGL,oBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,KAAK;IACL,QAAQ,GAAG,OAAO,UAAU;IAC5B,WAAW,cAAc,2CAA2C;GACtE;aAEC,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,EAAE,KAAK,GAAG,UACxC,oBAAC,OAAD;IAEE,eAAa,mBAAmB,SAAS,GAAG;IAC5C,OAAO;KACL,MAAM;KACN,iBACE,QAAQ,iBACJ,gBAAgB,cAAc,aAAa,CAAC,IAC5C,gBAAgB,cAAc,sBAAsB,CAAC;KAC3D,cAAc;KACd,YAAY;KACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,cAAc,aAAa,EAAG,MACzD;IACR;GACD,GAfM,KAeN,CACF;EACE,CAAA,CACF;;AAET,CAAC;AAED,WAAW,cAAc"}
|
|
@@ -1 +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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TechniqueCard.js","names":[],"sources":["../../../../../src/components/shared/three/ui/TechniqueCard.tsx"],"sourcesContent":["/**\n * TechniqueCard Component\n *\n * **Korean**: 기술 카드 컴포넌트 (Technique Card Component)\n *\n * Individual technique card displaying technique name, stamina cost, keyboard shortcut,\n * and availability state. Shows detailed tooltip on hover/focus with technique description.\n *\n * Uses Html overlay from @react-three/drei for positioning over 3D scene.\n *\n * @module components/shared/three/ui/TechniqueCard\n * @category Shared UI\n * @korean 기술카드\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { Technique } from \"../../../../types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { triggerHaptic } from \"../../../../utils/haptics\";\nimport { PlayerArchetype, TrigramStance } from \"../../../../types/common\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { AnimationType } from \"../../../../systems/animation\";\nimport \"./HUDAnimations.css\";\n\n/**\n * Props for TechniqueCard component.\n */\nexport interface TechniqueCardProps {\n /** Technique to display */\n readonly technique: Technique;\n\n /** Whether technique is currently selected */\n readonly isSelected: boolean;\n\n /** Whether technique is available (sufficient resources and no cooldown) */\n readonly isAvailable: boolean;\n\n /** Stamina cost percentage (0-100) */\n readonly staminaCost: number;\n\n /** Ki cost percentage (0-100) */\n readonly kiCost: number;\n\n /** Remaining cooldown in milliseconds */\n readonly remainingCooldown?: number;\n\n /** Keyboard shortcut key */\n readonly keyboardShortcut: string;\n\n /** Click handler */\n readonly onClick: () => void;\n\n /** Hover handler */\n readonly onHover: (technique: Technique | null) => void;\n\n /** Whether rendering for mobile device */\n readonly isMobile: boolean;\n\n /** Player archetype for reach calculation (optional) */\n readonly playerArchetype?: PlayerArchetype;\n\n /** Player stance for reach calculation (optional) */\n readonly playerStance?: TrigramStance;\n}\n\n/**\n * TechniqueCard Component\n *\n * Displays a single technique card with Korean/English names, resource costs,\n * keyboard shortcut, and availability indicators.\n *\n * @param props - Component props\n * @returns TechniqueCard component\n */\nexport const TechniqueCard: React.FC<TechniqueCardProps> = ({\n technique,\n isSelected,\n isAvailable,\n staminaCost,\n kiCost,\n remainingCooldown,\n keyboardShortcut,\n onClick,\n onHover,\n isMobile,\n playerArchetype,\n playerStance,\n}) => {\n const [showTooltip, setShowTooltip] = useState(false);\n\n 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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitalPointOverlayControlsHtml.js","names":[],"sources":["../../../../../src/components/shared/three/ui/VitalPointOverlayControlsHtml.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsHtml - UI controls for vital point visualization\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/three/ui/VitalPointOverlayControlsHtml\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsHtml component\n */\nexport interface VitalPointOverlayControlsProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the Html overlay container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n\n/**\n * Convert numeric color to CSS hex string\n */\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsHtml Component\n * Provides comprehensive UI for vital point visualization control\n */\nexport const VitalPointOverlayControlsHtml: React.FC<\n VitalPointOverlayControlsProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile]\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange]\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40, inset 0 0 20px ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: 200,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )} 0%, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_BLUE\n )} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}44`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n </Html>\n );\n};\n\nexport default VitalPointOverlayControlsHtml;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VitalPointOverlayControlsHtml.js","names":[],"sources":["../../../../../src/components/shared/three/ui/VitalPointOverlayControlsHtml.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsHtml - UI controls for vital point visualization\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/three/ui/VitalPointOverlayControlsHtml\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsHtml component\n */\nexport interface VitalPointOverlayControlsProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the Html overlay container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n\n/**\n * Convert numeric color to CSS hex string\n */\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsHtml Component\n * Provides comprehensive UI for vital point visualization control\n */\nexport const VitalPointOverlayControlsHtml: React.FC<\n VitalPointOverlayControlsProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile]\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange]\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40, inset 0 0 20px ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: 200,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )} 0%, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_BLUE\n )} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN\n )}44`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n </Html>\n );\n};\n\nexport default VitalPointOverlayControlsHtml;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,IAAM,oBAAoB,aAAyC;CACjE,QAAQ,UAAR;EACE,KAAK,mBAAmB,QACtB,OAAO,cAAc,cAAc,YAAY;EACjD,KAAK,mBAAmB,UACtB,OAAO,cAAc,cAAc,UAAU;EAC/C,KAAK,mBAAmB,OACtB,OAAO,cAAc,cAAc,WAAW;EAChD,KAAK,mBAAmB,UACtB,OAAO,cAAc,cAAc,oBAAoB;EACzD,KAAK,mBAAmB,OACtB,OAAO,cAAc,cAAc,cAAc;EACnD,SACE,OAAO,cAAc,cAAc,SAAS;CAChD;AACF;;;;;AAMA,IAAa,iCAER,EACH,SACA,iBACA,iBACA,yBACA,cACA,sBACA,aAAa,qBACb,qBACA,YACA,oBACA,UACA,kBACA,OACA,eACA,WAAW,OACX,qBACI;CACJ,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAE9C,MAAM,CAAC,qBAAqB,0BAA0B,SAAS,EAAE;CACjE,MAAM,cAAc,uBAAuB;CAC3C,MAAM,iBAAiB,uBAAuB;CAE9C,MAAM,QAAQ,cAAc,oBAAoB,GAAG,CAAC,CAAC;CAErD,MAAM,kBAKF,eACK;EACL,KAAK,WAAW,UAAU;EAC1B,MAAM,WAAW,SAAS;CAC5B,IACA,CAAC,QAAQ,CACX;CAEA,MAAM,gBAAgB,kBAAkB;CAExC,MAAM,gBAAgB,cAAc;EAClC,IAAI,SAAS,CAAC,GAAG,mBAAmB;EAEpC,IAAI,gBAAgB,SAAS,GAC3B,SAAS,OAAO,QAAQ,OAAO,gBAAgB,SAAS,GAAG,QAAQ,CAAC;EAGtE,IAAI,iBAAiB,OACnB,IAAI,iBAAiB,QACnB,SAAS,OAAO,QACb,OACC,GAAG,GAAG,WAAW,WAAW,KAAK,GAAG,GAAG,WAAW,YAAY,CAClE;OACK,IAAI,iBAAiB,QAC1B,SAAS,OAAO,QACb,OACC,GAAG,GAAG,WAAW,WAAW,KAAK,GAAG,GAAG,WAAW,YAAY,CAClE;OACK;GACL,MAAM,SAAS,GAAG,aAAa;GAC/B,SAAS,OAAO,QAAQ,OAAO,GAAG,GAAG,WAAW,MAAM,CAAC;EACzD;EAGF,IAAI,aAAa;GACf,MAAM,QAAQ,YAAY,YAAY;GACtC,SAAS,OAAO,QACb,OACC,GAAG,MAAM,OAAO,YAAY,EAAE,SAAS,KAAK,KAC5C,GAAG,MAAM,QAAQ,YAAY,EAAE,SAAS,KAAK,KAC7C,GAAG,MAAM,UAAU,YAAY,EAAE,SAAS,KAAK,KAC/C,GAAG,GAAG,YAAY,EAAE,SAAS,KAAK,CACtC;EACF;EAEA,OAAO,OAAO;CAChB,GAAG;EAAC;EAAiB;EAAc;CAAW,CAAC;CAE/C,MAAM,uBAAuB,aAC1B,aAAiC;EAIhC,wBAHmB,gBAAgB,SAAS,QAAQ,IAChD,gBAAgB,QAAQ,MAAM,MAAM,QAAQ,IAC5C,CAAC,GAAG,iBAAiB,QAAQ,CACC;CACpC,GACA,CAAC,iBAAiB,uBAAuB,CAC3C;CAEA,MAAM,kBAAwC;EAC5C,mBAAmB;EACnB,mBAAmB;EACnB,mBAAmB;EACnB,mBAAmB;EACnB,mBAAmB;CACrB;CAEA,MAAM,gBAIA;EACJ;GAAE,OAAO;GAAO,OAAO;GAAe,QAAQ;EAAK;EACnD;GAAE,OAAO;GAAQ,OAAO;GAAQ,QAAQ;EAAK;EAC7C;GAAE,OAAO;GAAS,OAAO;GAAS,QAAQ;EAAK;EAC/C;GAAE,OAAO;GAAQ,OAAO;GAAQ,QAAQ;EAAI;EAC5C;GAAE,OAAO;GAAQ,OAAO;GAAQ,QAAQ;EAAK;CAC/C;CAEA,MAAM,aAAa,WAAW,MAAM;CACpC,MAAM,eAAe,WAAW,KAAK;CACrC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,gBAAgB,WAAW,IAAI;CAErC,OACE,oBAAC,MAAD;EAAM,YAAA;EAAW,OAAO,EAAE,eAAe,OAAO;YAC9C,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,KAAK,cAAc;IACnB,MAAM,cAAc;IACpB,OAAO,cAAc;IACrB,QAAQ,cAAc;IACtB,OAAO;IACP,YAAY,GAAG,cAAc,cAAc,kBAAkB,EAAE;IAC/D,QAAQ,aAAa,cAAc,cAAc,YAAY;IAC7D,cAAc;IACd,SAAS,WAAW,SAAS;IAC7B,YAAY,YAAY;IACxB,OAAO,cAAc,cAAc,YAAY;IAC/C,WAAW,YAAY,cACrB,cAAc,YAChB,EAAE,qBAAqB,cACrB,cAAc,kBAChB,EAAE;IACF,YAAY;IACZ,eAAe;IACf,QAAQ;GACV;GACA,eAAY;aAvBd;IA0BE,qBAAC,OAAD;KACE,OAAO;MACL,SAAS;MACT,gBAAgB;MAChB,YAAY;MACZ,cAAc;MACd,eAAe;MACf,cAAc,aAAa,cACzB,cAAc,YAChB,EAAE;MACF,YAAY,0BAA0B,cACpC,cAAc,kBAChB,EAAE,SAAS,cACT,cAAc,YAChB,EAAE,UAAU,cAAc,cAAc,kBAAkB,EAAE;KAC9D;eAfF,CAiBE,qBAAC,OAAD,EAAA,UAAA,CACE,oBAAC,OAAD;MAAK,OAAO;OAAE,UAAU,WAAW,KAAK;OAAI,YAAY;MAAO;gBAAG;KAE7D,CAAA,GACL,qBAAC,OAAD;MACE,OAAO;OACL,UAAU;OACV,OAAO,cAAc,cAAc,cAAc;OACjD,WAAW;MACb;gBALF;OAOG;OAAc;OAAI,MAAM;OAAM;MAC5B;OACF,EAAA,CAAA,GACL,oBAAC,UAAD;MACE,eAAe,YAAY,CAAC,QAAQ;MACpC,OAAO;OACL,YAAY,2BAA2B,cACrC,cAAc,oBAChB,EAAE,OAAO,cAAc,cAAc,kBAAkB,EAAE;OACzD,QAAQ,aAAa,cAAc,cAAc,YAAY;OAC7D,cAAc;OACd,SAAS;OACT,OAAO,cAAc,cAAc,YAAY;OAC/C;OACA,QAAQ;OACR,YAAY;OACZ,WAAW,aAAa,cACtB,cAAc,YAChB,EAAE;MACJ;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,cAAc,cAC9C,cAAc,YAChB,EAAE;MACJ;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,aAAa,cAC7C,cAAc,YAChB,EAAE;MACJ;MACA,eAAY;gBAEX,WAAW,MAAM;KACZ,CAAA,CACL;;IAGL,oBAAC,OAAD;KAAK,OAAO,EAAE,cAAc,OAAO;eACjC,oBAAC,UAAD;MACE,eAAe,gBAAgB,CAAC,OAAO;MACvC,OAAO;OACL,OAAO;OACP,QAAQ;OACR,YAAY,UACR,2BAA2B,cACzB,cAAc,WAChB,EAAE,OAAO,cAAc,cAAc,gBAAgB,EAAE,UACvD,2BAA2B,cACzB,cAAc,oBAChB,EAAE,OAAO,cAAc,cAAc,kBAAkB,EAAE;OAC7D,QAAQ,aACN,UACI,cAAc,cAAc,WAAW,IACvC,cAAc,cAAc,YAAY;OAE9C,cAAc;OACd,OAAO,UAAU,cAAc,cAAc,YAAY,IAAI,cAAc,cAAc,YAAY;OACrG,UAAU,WAAW,KAAK;OAC1B,YAAY;OACZ,QAAQ;OACR,YAAY;OACZ,WAAW,UACP,cAAc,cACZ,cAAc,WAChB,EAAE,sBAAsB,gBAAgB,cAAc,cAAc,EAAG,MACvE,aAAa,cAAc,cAAc,YAAY,EAAE;OAC3D,eAAe;OACf,eAAe;MACjB;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,UAC9B,cAAc,cAAc,cAAc,WAAW,EAAE,MACvD,cAAc,cAAc,cAAc,YAAY,EAAE;MAC9D;MACA,eAAe,MAAM;OACnB,EAAE,cAAc,MAAM,YAAY;OAClC,EAAE,cAAc,MAAM,YAAY,UAC9B,cAAc,cAAc,cAAc,WAAW,EAAE,MACvD,aAAa,cAAc,cAAc,YAAY,EAAE;MAC7D;MACA,eAAY;gBAEX,UAAU,oBAAoB;KACzB,CAAA;IACL,CAAA;IAGJ,YAAY,WACX,qBAAA,UAAA,EAAA,UAAA;KAEE,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;gBAAnC,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;iBACD;MAEI,CAAA,GACL,oBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,UAAU;QACV,KAAK;OACP;iBAEC,gBAAgB,KAAK,aAAa;QACjC,MAAM,WAAW,gBAAgB,SAAS,QAAQ;QAClD,MAAM,gBAAgB,iBAAiB,QAAQ;QAC/C,OACE,oBAAC,UAAD;SAEE,eAAe,qBAAqB,QAAQ;SAC5C,OAAO;UACL,YAAY,WACR,2BAA2B,cAAc,OAAO,cAAc,YAC9D,GAAG,cAAc,cAAc,oBAAoB;UACvD,QAAQ,aAAa;UACrB,cAAc;UACd,SAAS;UACT,OAAO,cAAc,cAAc,YAAY;UAC/C,UAAU;UACV,QAAQ;UACR,SAAS,WAAW,IAAI;UACxB,YAAY;UACZ,YAAY,WAAW,SAAS;UAChC,WAAW,WACP,aAAa,cAAc,MAC3B;SACN;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU;UAChC,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU,WAAW,MAAM;UACjD,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAa,mBAAmB;mBAE/B;QACK,GA9BD,QA8BC;OAEZ,CAAC;MACE,CAAA,CACF;;KAGL,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;gBAAnC,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;iBACD;MAEI,CAAA,GACL,oBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,UAAU;QACV,KAAK;OACP;iBAEC,cAAc,KAAK,WAAW;QAC7B,MAAM,WAAW,iBAAiB,OAAO;QACzC,OACE,qBAAC,UAAD;SAEE,eAAe,qBAAqB,OAAO,KAAK;SAChD,OAAO;UACL,YAAY,WACR,2BAA2B,cACzB,cAAc,YAChB,EAAE,OAAO,cACP,cAAc,WAChB,EAAE,UACF,GAAG,cAAc,cAAc,oBAAoB;UACvD,QAAQ,aAAa,cACnB,cAAc,YAChB;UACA,cAAc;UACd,SAAS;UACT,OAAO,cAAc,cAAc,YAAY;UAC/C,UAAU;UACV,QAAQ;UACR,SAAS,WAAW,IAAI;UACxB,YAAY;UACZ,YAAY,WAAW,SAAS;UAChC,WAAW,WACP,aAAa,cACX,cAAc,YAChB,EAAE,MACF;SACN;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU;UAChC,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAe,MAAM;UACnB,EAAE,cAAc,MAAM,UAAU,WAAW,MAAM;UACjD,EAAE,cAAc,MAAM,YAAY;SACpC;SACA,eAAa,iBAAiB,OAAO;mBApCvC;UAsCG,OAAO;UAAO;UAAI,OAAO;SACpB;WAtCD,OAAO,KAsCN;OAEZ,CAAC;MACE,CAAA,CACF;;KAGL,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;gBAAnC,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;iBACD;MAEI,CAAA,GACL,qBAAC,OAAD;OAAK,OAAO,EAAE,UAAU,WAAW;iBAAnC,CACE,oBAAC,SAAD;QACE,MAAK;QACL,OAAO;QACP,WAAW,MAAM,eAAe,EAAE,OAAO,KAAK;QAC9C,aAAY;QACZ,OAAO;SACL,OAAO;SACP,QAAQ;SACR,YAAY,GAAG,cACb,cAAc,oBAChB;SACA,QAAQ,aAAa,cACnB,cAAc,YAChB,EAAE;SACF,cAAc;SACd,SAAS;SACT,OAAO,cAAc,cAAc,YAAY;SAC/C;SACA,YAAY,YAAY;SACxB,YAAY;SACZ,SAAS;QACX;QACA,UAAU,MAAM;SACd,EAAE,cAAc,MAAM,cAAc,cAClC,cAAc,YAChB;SACA,EAAE,cAAc,MAAM,YAAY,YAAY,cAC5C,cAAc,YAChB,EAAE;QACJ;QACA,SAAS,MAAM;SACb,EAAE,cAAc,MAAM,cAAc,GAAG,cACrC,cAAc,YAChB,EAAE;SACF,EAAE,cAAc,MAAM,YAAY;QACpC;QACA,eAAY;OACb,CAAA,GAEA,eACC,oBAAC,UAAD;QACE,eAAe,eAAe,EAAE;QAChC,OAAO;SACL,UAAU;SACV,OAAO;SACP,KAAK;SACL,WAAW;SACX,YAAY;SACZ,QAAQ;SACR,OAAO,cAAc,cAAc,cAAc;SACjD,QAAQ;SACR,UAAU;SACV,SAAS;SACT,SAAS;SACT,YAAY;SACZ,gBAAgB;SAChB,cAAc;SACd,YAAY;QACd;QACA,eAAe,MAAM;SACnB,EAAE,cAAc,MAAM,QAAQ,cAC5B,cAAc,UAChB;SACA,EAAE,cAAc,MAAM,aAAa,GAAG,cACpC,cAAc,oBAChB;QACF;QACA,eAAe,MAAM;SACnB,EAAE,cAAc,MAAM,QAAQ,cAC5B,cAAc,cAChB;SACA,EAAE,cAAc,MAAM,aAAa;QACrC;QACA,eAAY;QACZ,OAAM;kBACP;OAEO,CAAA,CAEP;QACF;;KAGL,qBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;gBAAnC,CACE,oBAAC,OAAD;OACE,OAAO;QACL;QACA,cAAc;QACd,OAAO,cAAc,cAAc,WAAW;QAC9C,YAAY;QACZ,eAAe;QACf,eAAe;OACjB;iBACD;MAEI,CAAA,GACL,qBAAC,OAAD;OACE,OAAO;QAAE,SAAS;QAAQ,eAAe;QAAU,KAAK;OAAM;iBADhE,CAGE,qBAAC,SAAD;QACE,OAAO;SACL,SAAS;SACT,YAAY;SACZ,KAAK;SACL,QAAQ;QACV;kBANF,CAQE,oBAAC,SAAD;SACE,MAAK;SACL,SAAS;SACT,WAAW,MAAM,mBAAmB,EAAE,OAAO,OAAO;SACpD,OAAO;UACL,OAAO;UACP,QAAQ;UACR,aAAa,cAAc,cAAc,YAAY;SACvD;SACA,eAAY;QACb,CAAA,GACD,oBAAC,QAAD;SAAM,OAAO,EAAE,SAAS;mBAAG;QAAyB,CAAA,CAC/C;WACP,qBAAC,SAAD;QACE,OAAO;SACL,SAAS;SACT,YAAY;SACZ,KAAK;SACL,QAAQ;QACV;kBANF,CAQE,oBAAC,SAAD;SACE,MAAK;SACL,SAAS;SACT,WAAW,MAAM,iBAAiB,EAAE,OAAO,OAAO;SAClD,OAAO;UACL,OAAO;UACP,QAAQ;UACR,aAAa,cAAc,cAAc,YAAY;SACvD;SACA,eAAY;QACb,CAAA,GACD,oBAAC,QAAD;SAAM,OAAO,EAAE,SAAS;mBAAG;QAAwB,CAAA,CAC9C;SACJ;QACF;;MAGH,gBAAgB,SAAS,KACzB,iBAAiB,SACjB,gBAAgB,OAChB,oBAAC,OAAD;MAAK,OAAO,EAAE,cAAc,OAAO;gBACjC,oBAAC,UAAD;OACE,eAAe;QACb,wBAAwB,CAAC,CAAC;QAC1B,qBAAqB,KAAK;QAC1B,eAAe,EAAE;OACnB;OACA,OAAO;QACL,OAAO;QACP,QAAQ,eAAe;QACvB,YAAY,GAAG,cACb,cAAc,oBAChB;QACA,QAAQ,aAAa,cACnB,cAAc,aAChB;QACA,cAAc;QACd,OAAO,cAAc,cAAc,aAAa;QAChD,UAAU;QACV,QAAQ;QACR,YAAY;QACZ,YAAY;OACd;OACA,eAAe,MAAM;QACnB,EAAE,cAAc,MAAM,aAAa,GAAG,cACpC,cAAc,aAChB,EAAE;QACF,EAAE,cAAc,MAAM,YAAY;OACpC;OACA,eAAe,MAAM;QACnB,EAAE,cAAc,MAAM,aAAa,GAAG,cACpC,cAAc,oBAChB;QACA,EAAE,cAAc,MAAM,YAAY;OACpC;OACA,eAAY;iBACb;MAEO,CAAA;KACL,CAAA;KAIP,qBAAC,OAAD,EAAA,UAAA,CACE,qBAAC,OAAD;MACE,OAAO;OACL;OACA,cAAc;OACd,OAAO,cAAc,cAAc,WAAW;MAChD;gBALF;OAMC;OACc,MAAM,QAAQ,CAAC;OAAE;MAC3B;SACL,oBAAC,SAAD;MACE,MAAK;MACL,KAAI;MACJ,KAAI;MACJ,MAAK;MACL,OAAO;MACP,WAAW,MAAM,cAAc,WAAW,EAAE,OAAO,KAAK,CAAC;MACzD,OAAO;OACL,OAAO;OACP,aAAa,cAAc,cAAc,YAAY;MACvD;MACA,eAAY;KACb,CAAA,CACE,EAAA,CAAA;KAGL,qBAAC,OAAD;MACE,OAAO;OACL,WAAW;OACX,YAAY;OACZ,WAAW,aAAa,cACtB,cAAc,YAChB,EAAE;OACF,UAAU;OACV,OAAO,cAAc,cAAc,cAAc;MACnD;gBATF,CAWE,qBAAC,OAAD,EAAA,UAAA;OAAK;OACE,MAAM,SAAS;OAAK;OAAQ,MAAM,SAAS;MAC7C,EAAA,CAAA,GACL,qBAAC,OAAD,EAAA,UAAA;OAAK;OACC,MAAM,SAAS;OAAK;OAAQ,MAAM,SAAS;MAC5C,EAAA,CAAA,CACF;;IACL,EAAA,CAAA;GAED;;CACD,CAAA;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BackButton.js","names":[],"sources":["../../../../src/components/shared/ui/BackButton.tsx"],"sourcesContent":["/**\n * BackButton - Shared back/return button for screens\n * \n * Provides a consistent bilingual button for returning to menu or previous screen.\n * Uses BaseButtonOverlayHtml for consistency and maintainability.\n * \n * @module components/shared/ui\n * @category UI Components\n * @korean 뒤로가기버튼\n */\n\nimport React from \"react\";\nimport { BaseButtonOverlayHtml } from \"../../shared/base/BaseButtonOverlayHtml\";\n\nexport interface BackButtonProps {\n /** Callback when button is clicked */\n readonly onClick: () => void;\n /** Korean text for the button */\n readonly korean?: string;\n /** English text for the button */\n readonly english?: string;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Test ID for the button */\n readonly testId?: string;\n}\n\n/**\n * BackButton Component\n * \n * Reusable bilingual back/return button using BaseButtonOverlayHtml.\n * Provides consistent Korean theming and responsive sizing.\n * \n * Refactored to use BaseButtonOverlayHtml for better consistency.\n * \n * @example\n * ```tsx\n * <BackButton\n * onClick={() => navigate('/menu')}\n * korean=\"돌아가기\"\n * english=\"Return\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BackButton: React.FC<BackButtonProps> = ({\n onClick,\n korean = \"돌아가기\",\n english = \"Return\",\n isMobile,\n testId = \"back-button\",\n}) => {\n return (\n <BaseButtonOverlayHtml\n korean={korean}\n english={english}\n onClick={onClick}\n variant=\"primary\"\n size=\"md\"\n isMobile={isMobile}\n testId={testId}\n />\n );\n};\n\nexport interface LinkButtonProps {\n /** Callback when button is clicked */\n readonly onClick: () => void;\n /** Korean text for the button */\n readonly korean: string;\n /** English text for the button */\n readonly english: string;\n /** Icon/emoji to display */\n readonly icon?: string;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Test ID for the button */\n readonly testId?: string;\n}\n\n/**\n * LinkButton Component\n * \n * Secondary action button using BaseButtonOverlayHtml with secondary variant.\n * Used for links and secondary actions like ISMS policy links.\n * \n * Refactored to use BaseButtonOverlayHtml for better consistency.\n * \n * @example\n * ```tsx\n * <LinkButton\n * onClick={() => window.open(url)}\n * korean=\"보안 정책\"\n * english=\"Security Policy\"\n * icon=\"🔐\"\n * isMobile={false}\n * />\n * ```\n */\nexport const LinkButton: React.FC<LinkButtonProps> = ({\n onClick,\n korean,\n english,\n icon,\n isMobile,\n testId = \"link-button\",\n}) => {\n const labelKorean = icon ? `${icon} ${korean}` : korean;\n \n return (\n <BaseButtonOverlayHtml\n korean={labelKorean}\n english={english}\n onClick={onClick}\n variant=\"secondary\"\n size=\"sm\"\n isMobile={isMobile}\n testId={testId}\n />\n );\n};\n\nexport default { BackButton, LinkButton };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6CA,IAAa,cAAyC,EACpD,SACA,SAAS,QACT,UAAU,UACV,UACA,SAAS,oBACL;CACJ,OACE,oBAAC,uBAAD;EACU;EACC;EACA;EACT,SAAQ;EACR,MAAK;EACK;EACF;
|
|
1
|
+
{"version":3,"file":"BackButton.js","names":[],"sources":["../../../../src/components/shared/ui/BackButton.tsx"],"sourcesContent":["/**\n * BackButton - Shared back/return button for screens\n * \n * Provides a consistent bilingual button for returning to menu or previous screen.\n * Uses BaseButtonOverlayHtml for consistency and maintainability.\n * \n * @module components/shared/ui\n * @category UI Components\n * @korean 뒤로가기버튼\n */\n\nimport React from \"react\";\nimport { BaseButtonOverlayHtml } from \"../../shared/base/BaseButtonOverlayHtml\";\n\nexport interface BackButtonProps {\n /** Callback when button is clicked */\n readonly onClick: () => void;\n /** Korean text for the button */\n readonly korean?: string;\n /** English text for the button */\n readonly english?: string;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Test ID for the button */\n readonly testId?: string;\n}\n\n/**\n * BackButton Component\n * \n * Reusable bilingual back/return button using BaseButtonOverlayHtml.\n * Provides consistent Korean theming and responsive sizing.\n * \n * Refactored to use BaseButtonOverlayHtml for better consistency.\n * \n * @example\n * ```tsx\n * <BackButton\n * onClick={() => navigate('/menu')}\n * korean=\"돌아가기\"\n * english=\"Return\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BackButton: React.FC<BackButtonProps> = ({\n onClick,\n korean = \"돌아가기\",\n english = \"Return\",\n isMobile,\n testId = \"back-button\",\n}) => {\n return (\n <BaseButtonOverlayHtml\n korean={korean}\n english={english}\n onClick={onClick}\n variant=\"primary\"\n size=\"md\"\n isMobile={isMobile}\n testId={testId}\n />\n );\n};\n\nexport interface LinkButtonProps {\n /** Callback when button is clicked */\n readonly onClick: () => void;\n /** Korean text for the button */\n readonly korean: string;\n /** English text for the button */\n readonly english: string;\n /** Icon/emoji to display */\n readonly icon?: string;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Test ID for the button */\n readonly testId?: string;\n}\n\n/**\n * LinkButton Component\n * \n * Secondary action button using BaseButtonOverlayHtml with secondary variant.\n * Used for links and secondary actions like ISMS policy links.\n * \n * Refactored to use BaseButtonOverlayHtml for better consistency.\n * \n * @example\n * ```tsx\n * <LinkButton\n * onClick={() => window.open(url)}\n * korean=\"보안 정책\"\n * english=\"Security Policy\"\n * icon=\"🔐\"\n * isMobile={false}\n * />\n * ```\n */\nexport const LinkButton: React.FC<LinkButtonProps> = ({\n onClick,\n korean,\n english,\n icon,\n isMobile,\n testId = \"link-button\",\n}) => {\n const labelKorean = icon ? `${icon} ${korean}` : korean;\n \n return (\n <BaseButtonOverlayHtml\n korean={labelKorean}\n english={english}\n onClick={onClick}\n variant=\"secondary\"\n size=\"sm\"\n isMobile={isMobile}\n testId={testId}\n />\n );\n};\n\nexport default { BackButton, LinkButton };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6CA,IAAa,cAAyC,EACpD,SACA,SAAS,QACT,UAAU,UACV,UACA,SAAS,oBACL;CACJ,OACE,oBAAC,uBAAD;EACU;EACC;EACA;EACT,SAAQ;EACR,MAAK;EACK;EACF;CACT,CAAA;AAEL;;;;;;;;;;;;;;;;;;;;AAoCA,IAAa,cAAyC,EACpD,SACA,QACA,SACA,MACA,UACA,SAAS,oBACL;CAGJ,OACE,oBAAC,uBAAD;EACE,QAJgB,OAAO,GAAG,KAAK,GAAG,WAAW;EAKpC;EACA;EACT,SAAQ;EACR,MAAK;EACK;EACF;CACT,CAAA;AAEL"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseHUDContainer.js","names":[],"sources":["../../../../src/components/shared/ui/BaseHUDContainer.tsx"],"sourcesContent":["/**\n * BaseHUDContainer - Reusable HUD container with common patterns\n *\n * Provides consistent container styling and positioning for all HUD components.\n * Eliminates code duplication across Training and Combat HUDs.\n *\n * Features:\n * - Responsive positioning based on HUD position (left, right, top, bottom)\n * - Korean cyberpunk theming with gradients and borders\n * - Pointer events handling\n * - Backdrop blur effects\n *\n * ## Z-Index Stacking Order (Combat HUD Layers)\n *\n * The combat screen renders multiple overlapping HUD panels. The stacking\n * order is managed by the Z_INDEX constants from LayoutTypes.ts:\n *\n * | Layer | Z-Index | Description |\n * |---------------------|---------|------------------------------------|\n * | BACKGROUND | 0 | 3D scene background |\n * | ARENA | 10 | Combat arena mesh |\n * | PLAYERS | 20 | Character models |\n * | EFFECTS | 30 | Particles and VFX |\n * | HUD_BACKGROUND | 40 | HUD panel backgrounds |\n * | HUD (default) | 50 | Left/Right/Top/Bottom HUD panels |\n * | TECHNIQUE_BAR | 55 | Technique bar overlay |\n * | HUD_OVERLAY | 60 | PlayerStateOverlay and sub-HUDs |\n * | MOBILE_CONTROLS | 100 | Touch controls on mobile |\n * | MODAL | 200 | Modal dialogs |\n * | TOOLTIP | 300 | Tooltips and hints |\n * | PAUSE_MENU | 1000 | Pause menu overlay |\n * | LOADING | 2000 | Loading screens |\n * | DEBUG | 9000 | Performance debug overlay |\n *\n * All BaseHUDContainer instances default to Z_INDEX.HUD (50). Parent\n * screens can override via the `zIndex` prop when a different layer\n * is needed (e.g., overlays at HUD_OVERLAY = 60).\n *\n * ## Viewport Breakpoints (Expected HUD Sizes)\n *\n * | Viewport | Width | Left/Right HUD | Top HUD | Bottom HUD |\n * |---------------------|----------|----------------|---------|------------|\n * | Small Phone (≤375) | ≤375px | ~120-150px | ~50px | ~90px |\n * | Mobile (<768) | <768px | ~180-200px | ~60px | ~110px |\n * | Tablet (768-1199) | 768-1199 | ~220-260px | ~65px | ~120px |\n * | Desktop (≥1200) | ≥1200px | ~260-300px | ~70px | ~130px |\n * | 4K (≥1920) | ≥1920px | ~300-400px | ~80px | ~140px |\n *\n * Width/height values are passed by the parent screen and scaled via\n * positionScale multipliers. This table documents the expected ranges\n * produced by CombatScreen3D and TrainingScreen3D layout calculations.\n *\n * @module components/shared/ui\n * @korean 기본HUD컨테이너 - 공통 패턴을 가진 재사용 가능한 HUD 컨테이너\n */\n\nimport React from \"react\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\n\n/**\n * HUD position type\n */\nexport type HUDPosition = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n/**\n * Props for BaseHUDContainer component\n */\nexport interface BaseHUDContainerProps {\n /** Position of the HUD (left, right, top, bottom) */\n readonly position: HUDPosition;\n /** Width in pixels (used for left/right positions; top/bottom use 100% width) */\n readonly width: number;\n /** Height in pixels */\n readonly height: number;\n /** Top offset in pixels (for left/right HUDs) */\n readonly topOffset?: number;\n /** Internal padding in pixels */\n readonly padding?: number;\n /** Gap between sections in pixels */\n readonly gap?: number;\n /** Z-index for layering */\n readonly zIndex?: number;\n /** Additional CSS styles */\n readonly style?: React.CSSProperties;\n /** Child elements */\n readonly children: React.ReactNode;\n /** Test ID for testing */\n readonly dataTestId?: string;\n}\n\n/**\n * BaseHUDContainer Component\n *\n * Reusable container for HUD elements with consistent styling and positioning.\n * Handles left, right, top, and bottom positions with appropriate borders,\n * gradients, and responsive behavior.\n *\n * @example\n * ```tsx\n * <BaseHUDContainer\n * position=\"left\"\n * width={268}\n * height={940}\n * topOffset={70}\n * padding={12}\n * gap={14}\n * dataTestId=\"combat-left-hud\"\n * >\n * <PlayerHUD player={player} />\n * <SpeedIndicator speed={speed} />\n * </BaseHUDContainer>\n * ```\n */\nexport const BaseHUDContainer: React.FC<BaseHUDContainerProps> = ({\n position,\n width,\n height,\n topOffset = 0,\n padding = 10,\n gap = 12,\n zIndex = Z_INDEX.HUD,\n style = {},\n children,\n dataTestId,\n}) => {\n const containerStyle = React.useMemo<React.CSSProperties>(() => {\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"stretch\",\n pointerEvents: \"none\",\n padding: `${padding}px`,\n boxSizing: \"border-box\",\n gap: `${gap}px`,\n zIndex,\n backdropFilter: \"blur(8px)\",\n };\n\n switch (position) {\n case \"left\":\n return {\n ...baseStyle,\n left: 0,\n top: `${topOffset}px`,\n width: `${width}px`,\n height: `${height}px`,\n justifyContent: \"flex-start\",\n borderRight: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(90deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.4)} 100%)`,\n };\n\n case \"right\":\n return {\n ...baseStyle,\n right: 0,\n top: `${topOffset}px`,\n width: `${width}px`,\n height: `${height}px`,\n justifyContent: \"flex-start\",\n borderLeft: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(270deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.4)} 100%)`,\n };\n\n case \"top\":\n return {\n ...baseStyle,\n top: 0,\n left: 0,\n width: \"100%\",\n height: `${height}px`,\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n borderBottom: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(180deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.7)} 100%)`,\n };\n\n case \"bottom\":\n return {\n ...baseStyle,\n bottom: 0,\n left: 0,\n width: \"100%\",\n height: `${height}px`,\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n borderTop: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(0deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.7)} 100%)`,\n };\n }\n }, [position, width, height, topOffset, padding, gap, zIndex]);\n\n return (\n <div style={{ ...containerStyle, ...style }} data-testid={dataTestId}>\n {children}\n </div>\n );\n};\n\nexport default BaseHUDContainer;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmHA,IAAa,oBAAqD,EAChE,UACA,OACA,QACA,YAAY,GACZ,UAAU,IACV,MAAM,IACN,SAAS,QAAQ,KACjB,QAAQ,
|
|
1
|
+
{"version":3,"file":"BaseHUDContainer.js","names":[],"sources":["../../../../src/components/shared/ui/BaseHUDContainer.tsx"],"sourcesContent":["/**\n * BaseHUDContainer - Reusable HUD container with common patterns\n *\n * Provides consistent container styling and positioning for all HUD components.\n * Eliminates code duplication across Training and Combat HUDs.\n *\n * Features:\n * - Responsive positioning based on HUD position (left, right, top, bottom)\n * - Korean cyberpunk theming with gradients and borders\n * - Pointer events handling\n * - Backdrop blur effects\n *\n * ## Z-Index Stacking Order (Combat HUD Layers)\n *\n * The combat screen renders multiple overlapping HUD panels. The stacking\n * order is managed by the Z_INDEX constants from LayoutTypes.ts:\n *\n * | Layer | Z-Index | Description |\n * |---------------------|---------|------------------------------------|\n * | BACKGROUND | 0 | 3D scene background |\n * | ARENA | 10 | Combat arena mesh |\n * | PLAYERS | 20 | Character models |\n * | EFFECTS | 30 | Particles and VFX |\n * | HUD_BACKGROUND | 40 | HUD panel backgrounds |\n * | HUD (default) | 50 | Left/Right/Top/Bottom HUD panels |\n * | TECHNIQUE_BAR | 55 | Technique bar overlay |\n * | HUD_OVERLAY | 60 | PlayerStateOverlay and sub-HUDs |\n * | MOBILE_CONTROLS | 100 | Touch controls on mobile |\n * | MODAL | 200 | Modal dialogs |\n * | TOOLTIP | 300 | Tooltips and hints |\n * | PAUSE_MENU | 1000 | Pause menu overlay |\n * | LOADING | 2000 | Loading screens |\n * | DEBUG | 9000 | Performance debug overlay |\n *\n * All BaseHUDContainer instances default to Z_INDEX.HUD (50). Parent\n * screens can override via the `zIndex` prop when a different layer\n * is needed (e.g., overlays at HUD_OVERLAY = 60).\n *\n * ## Viewport Breakpoints (Expected HUD Sizes)\n *\n * | Viewport | Width | Left/Right HUD | Top HUD | Bottom HUD |\n * |---------------------|----------|----------------|---------|------------|\n * | Small Phone (≤375) | ≤375px | ~120-150px | ~50px | ~90px |\n * | Mobile (<768) | <768px | ~180-200px | ~60px | ~110px |\n * | Tablet (768-1199) | 768-1199 | ~220-260px | ~65px | ~120px |\n * | Desktop (≥1200) | ≥1200px | ~260-300px | ~70px | ~130px |\n * | 4K (≥1920) | ≥1920px | ~300-400px | ~80px | ~140px |\n *\n * Width/height values are passed by the parent screen and scaled via\n * positionScale multipliers. This table documents the expected ranges\n * produced by CombatScreen3D and TrainingScreen3D layout calculations.\n *\n * @module components/shared/ui\n * @korean 기본HUD컨테이너 - 공통 패턴을 가진 재사용 가능한 HUD 컨테이너\n */\n\nimport React from \"react\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexToRgbaString } from \"../../../utils/colorUtils\";\n\n/**\n * HUD position type\n */\nexport type HUDPosition = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n/**\n * Props for BaseHUDContainer component\n */\nexport interface BaseHUDContainerProps {\n /** Position of the HUD (left, right, top, bottom) */\n readonly position: HUDPosition;\n /** Width in pixels (used for left/right positions; top/bottom use 100% width) */\n readonly width: number;\n /** Height in pixels */\n readonly height: number;\n /** Top offset in pixels (for left/right HUDs) */\n readonly topOffset?: number;\n /** Internal padding in pixels */\n readonly padding?: number;\n /** Gap between sections in pixels */\n readonly gap?: number;\n /** Z-index for layering */\n readonly zIndex?: number;\n /** Additional CSS styles */\n readonly style?: React.CSSProperties;\n /** Child elements */\n readonly children: React.ReactNode;\n /** Test ID for testing */\n readonly dataTestId?: string;\n}\n\n/**\n * BaseHUDContainer Component\n *\n * Reusable container for HUD elements with consistent styling and positioning.\n * Handles left, right, top, and bottom positions with appropriate borders,\n * gradients, and responsive behavior.\n *\n * @example\n * ```tsx\n * <BaseHUDContainer\n * position=\"left\"\n * width={268}\n * height={940}\n * topOffset={70}\n * padding={12}\n * gap={14}\n * dataTestId=\"combat-left-hud\"\n * >\n * <PlayerHUD player={player} />\n * <SpeedIndicator speed={speed} />\n * </BaseHUDContainer>\n * ```\n */\nexport const BaseHUDContainer: React.FC<BaseHUDContainerProps> = ({\n position,\n width,\n height,\n topOffset = 0,\n padding = 10,\n gap = 12,\n zIndex = Z_INDEX.HUD,\n style = {},\n children,\n dataTestId,\n}) => {\n const containerStyle = React.useMemo<React.CSSProperties>(() => {\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"stretch\",\n pointerEvents: \"none\",\n padding: `${padding}px`,\n boxSizing: \"border-box\",\n gap: `${gap}px`,\n zIndex,\n backdropFilter: \"blur(8px)\",\n };\n\n switch (position) {\n case \"left\":\n return {\n ...baseStyle,\n left: 0,\n top: `${topOffset}px`,\n width: `${width}px`,\n height: `${height}px`,\n justifyContent: \"flex-start\",\n borderRight: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(90deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.4)} 100%)`,\n };\n\n case \"right\":\n return {\n ...baseStyle,\n right: 0,\n top: `${topOffset}px`,\n width: `${width}px`,\n height: `${height}px`,\n justifyContent: \"flex-start\",\n borderLeft: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(270deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.4)} 100%)`,\n };\n\n case \"top\":\n return {\n ...baseStyle,\n top: 0,\n left: 0,\n width: \"100%\",\n height: `${height}px`,\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n borderBottom: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(180deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.7)} 100%)`,\n };\n\n case \"bottom\":\n return {\n ...baseStyle,\n bottom: 0,\n left: 0,\n width: \"100%\",\n height: `${height}px`,\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n borderTop: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}`,\n background: `linear-gradient(0deg, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9)} 0%, ${hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.7)} 100%)`,\n };\n }\n }, [position, width, height, topOffset, padding, gap, zIndex]);\n\n return (\n <div style={{ ...containerStyle, ...style }} data-testid={dataTestId}>\n {children}\n </div>\n );\n};\n\nexport default BaseHUDContainer;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmHA,IAAa,oBAAqD,EAChE,UACA,OACA,QACA,YAAY,GACZ,UAAU,IACV,MAAM,IACN,SAAS,QAAQ,KACjB,QAAQ,CAAC,GACT,UACA,iBACI;CAsEJ,OACE,oBAAC,OAAD;EAAK,OAAO;GAAE,GAtEO,MAAM,cAAmC;IAC9D,MAAM,YAAiC;KACrC,UAAU;KACV,SAAS;KACT,eAAe;KACf,YAAY;KACZ,eAAe;KACf,SAAS,GAAG,QAAQ;KACpB,WAAW;KACX,KAAK,GAAG,IAAI;KACZ;KACA,gBAAgB;IAClB;IAEA,QAAQ,UAAR;KACE,KAAK,QACH,OAAO;MACL,GAAG;MACH,MAAM;MACN,KAAK,GAAG,UAAU;MAClB,OAAO,GAAG,MAAM;MAChB,QAAQ,GAAG,OAAO;MAClB,gBAAgB;MAChB,aAAa,aAAa,gBAAgB,cAAc,cAAc,EAAG;MACzE,YAAY,0BAA0B,gBAAgB,cAAc,oBAAoB,GAAI,EAAE,OAAO,gBAAgB,cAAc,oBAAoB,EAAG,EAAE;KAC9J;KAEF,KAAK,SACH,OAAO;MACL,GAAG;MACH,OAAO;MACP,KAAK,GAAG,UAAU;MAClB,OAAO,GAAG,MAAM;MAChB,QAAQ,GAAG,OAAO;MAClB,gBAAgB;MAChB,YAAY,aAAa,gBAAgB,cAAc,cAAc,EAAG;MACxE,YAAY,2BAA2B,gBAAgB,cAAc,oBAAoB,GAAI,EAAE,OAAO,gBAAgB,cAAc,oBAAoB,EAAG,EAAE;KAC/J;KAEF,KAAK,OACH,OAAO;MACL,GAAG;MACH,KAAK;MACL,MAAM;MACN,OAAO;MACP,QAAQ,GAAG,OAAO;MAClB,eAAe;MACf,gBAAgB;MAChB,YAAY;MACZ,cAAc,aAAa,gBAAgB,cAAc,cAAc,EAAG;MAC1E,YAAY,2BAA2B,gBAAgB,cAAc,oBAAoB,EAAG,EAAE,OAAO,gBAAgB,cAAc,oBAAoB,EAAG,EAAE;KAC9J;KAEF,KAAK,UACH,OAAO;MACL,GAAG;MACH,QAAQ;MACR,MAAM;MACN,OAAO;MACP,QAAQ,GAAG,OAAO;MAClB,eAAe;MACf,gBAAgB;MAChB,YAAY;MACZ,WAAW,aAAa,gBAAgB,cAAc,cAAc,EAAG;MACvE,YAAY,yBAAyB,gBAAgB,cAAc,oBAAoB,EAAG,EAAE,OAAO,gBAAgB,cAAc,oBAAoB,EAAG,EAAE;KAC5J;IACJ;GACF,GAAG;IAAC;IAAU;IAAO;IAAQ;IAAW;IAAS;IAAK;GAAM,CAGzC;GAAgB,GAAG;EAAM;EAAG,eAAa;EACvD;CACE,CAAA;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CombatTimer.js","names":[],"sources":["../../../../src/components/shared/ui/CombatTimer.tsx"],"sourcesContent":["/**\n * CombatTimer Component - Displays combat round timer\n *\n * Korean: 전투 타이머 (Combat Timer)\n *\n * Shows remaining time in MM:SS format with:\n * - Color changes based on time remaining\n * - Pulsing animation when time is critical\n * - Korean cyberpunk aesthetic\n * - Responsive sizing\n *\n * @module components/shared/ui/CombatTimer\n * @category Shared UI\n */\n\nimport React, { useMemo, useEffect } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { hexColorToCSS } from \"../../../utils/colorUtils\";\nimport { TimerWarningLevel } from \"../../../hooks/useCombatTimer\";\nimport \"../three/ui/HUDAnimations.css\";\n\nconst PULSE_ANIMATION_ID = \"combat-timer-pulse-animation\";\n\nconst injectPulseAnimation = () => {\n if (document.getElementById(PULSE_ANIMATION_ID)) return;\n\n const style = document.createElement(\"style\");\n style.id = PULSE_ANIMATION_ID;\n style.textContent = `\n @keyframes pulse {\n 0% {\n transform: translateX(-50%) scale(1);\n opacity: 1;\n }\n 50% {\n transform: translateX(-50%) scale(1.05);\n opacity: 0.9;\n }\n 100% {\n transform: translateX(-50%) scale(1);\n opacity: 1;\n }\n }\n `;\n document.head.appendChild(style);\n};\n\n/**\n * Props for the CombatTimer component\n */\nexport interface CombatTimerProps {\n /** Formatted time string (MM:SS) */\n readonly formattedTime: string;\n /** Current warning level */\n readonly warningLevel: TimerWarningLevel;\n /** Whether time is up */\n readonly isTimeUp: boolean;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n /** Optional custom position styling */\n readonly style?: React.CSSProperties;\n}\n\n/**\n * Get timer color based on warning level\n */\nfunction getTimerColor(\n warningLevel: TimerWarningLevel,\n isTimeUp: boolean,\n): number {\n if (isTimeUp) {\n return KOREAN_COLORS.NEGATIVE_RED; // Red for time up\n }\n switch (warningLevel) {\n case \"urgent\":\n return KOREAN_COLORS.NEGATIVE_RED; // Red (≤5s)\n case \"warning\":\n return KOREAN_COLORS.SECONDARY_YELLOW; // Yellow (≤10s)\n case \"none\":\n default:\n return KOREAN_COLORS.PRIMARY_CYAN; // Cyan (>10s)\n }\n}\n\n/**\n * CombatTimer Component\n *\n * Displays round timer with Korean cyberpunk aesthetic and responsive design.\n *\n * Features:\n * - MM:SS format display\n * - Color changes: cyan (>10s), yellow (≤10s), red (≤5s)\n * - Pulsing animation when time ≤5s\n * - \"Time's Up!\" message when timer reaches 0\n * - Responsive sizing for mobile/tablet/desktop\n * - Korean-English bilingual support\n *\n * Korean: 전투 타이머 컴포넌트\n *\n * @example\n * ```tsx\n * <CombatTimer\n * formattedTime=\"03:45\"\n * warningLevel=\"none\"\n * isTimeUp={false}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatTimer: React.FC<CombatTimerProps> = ({\n formattedTime,\n warningLevel,\n isTimeUp,\n isMobile,\n style = {},\n}) => {\n useEffect(() => {\n injectPulseAnimation();\n }, []);\n\n const timerColor = getTimerColor(warningLevel, isTimeUp);\n const timerColorCSS = useMemo(() => hexColorToCSS(timerColor), [timerColor]);\n\n const bgColor = useMemo(\n () => hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK),\n [],\n );\n\n const borderColor = useMemo(() => {\n if (isTimeUp) return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n if (warningLevel === \"urgent\")\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n if (warningLevel === \"warning\")\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW);\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }, [warningLevel, isTimeUp]);\n\n const shouldFlash = warningLevel === \"urgent\" || isTimeUp;\n\n const fontSize = isMobile ? \"32px\" : \"48px\";\n const labelFontSize = isMobile ? \"12px\" : \"14px\";\n\n const displayText = isTimeUp ? \"시간 종료 | Time's Up!\" : formattedTime;\n\n return (\n <div\n data-testid=\"combat-timer\"\n role=\"timer\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n aria-label={`Time remaining: ${formattedTime}`}\n className=\"hud-animated\"\n style={{\n position: \"absolute\",\n top: isMobile ? \"8px\" : \"12px\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n fontSize,\n fontFamily: \"monospace\",\n fontWeight: \"bold\",\n color: timerColorCSS,\n textShadow: `0 0 ${isMobile ? \"8px\" : \"12px\"} ${timerColorCSS}, 0 0 ${\n isMobile ? \"16px\" : \"24px\"\n } ${timerColorCSS}`,\n padding: isMobile ? \"8px 16px\" : \"12px 24px\",\n backgroundColor: `${bgColor}dd`,\n border: `2px solid ${borderColor}`,\n borderRadius: isMobile ? \"6px\" : \"8px\",\n animation: shouldFlash ? \"timerFlash 1s ease-in-out infinite\" : \"none\",\n zIndex: 100,\n pointerEvents: \"none\",\n userSelect: \"none\",\n minWidth: isMobile ? \"120px\" : \"160px\",\n textAlign: \"center\",\n boxShadow: `0 0 ${isMobile ? \"12px\" : \"20px\"} ${borderColor}40`,\n transition: \"all 0.3s ease-in-out\",\n ...style,\n }}\n >\n {/* Label */}\n {!isTimeUp && (\n <div\n style={{\n fontSize: labelFontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n color: timerColorCSS,\n marginBottom: \"2px\",\n opacity: 0.8,\n }}\n >\n 시간 | TIME\n </div>\n )}\n\n {/* Timer display */}\n <div>{displayText}</div>\n </div>\n );\n};\n\nexport default CombatTimer;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,qBAAqB;AAE3B,IAAM,6BAA6B;CACjC,IAAI,SAAS,eAAe,
|
|
1
|
+
{"version":3,"file":"CombatTimer.js","names":[],"sources":["../../../../src/components/shared/ui/CombatTimer.tsx"],"sourcesContent":["/**\n * CombatTimer Component - Displays combat round timer\n *\n * Korean: 전투 타이머 (Combat Timer)\n *\n * Shows remaining time in MM:SS format with:\n * - Color changes based on time remaining\n * - Pulsing animation when time is critical\n * - Korean cyberpunk aesthetic\n * - Responsive sizing\n *\n * @module components/shared/ui/CombatTimer\n * @category Shared UI\n */\n\nimport React, { useMemo, useEffect } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { hexColorToCSS } from \"../../../utils/colorUtils\";\nimport { TimerWarningLevel } from \"../../../hooks/useCombatTimer\";\nimport \"../three/ui/HUDAnimations.css\";\n\nconst PULSE_ANIMATION_ID = \"combat-timer-pulse-animation\";\n\nconst injectPulseAnimation = () => {\n if (document.getElementById(PULSE_ANIMATION_ID)) return;\n\n const style = document.createElement(\"style\");\n style.id = PULSE_ANIMATION_ID;\n style.textContent = `\n @keyframes pulse {\n 0% {\n transform: translateX(-50%) scale(1);\n opacity: 1;\n }\n 50% {\n transform: translateX(-50%) scale(1.05);\n opacity: 0.9;\n }\n 100% {\n transform: translateX(-50%) scale(1);\n opacity: 1;\n }\n }\n `;\n document.head.appendChild(style);\n};\n\n/**\n * Props for the CombatTimer component\n */\nexport interface CombatTimerProps {\n /** Formatted time string (MM:SS) */\n readonly formattedTime: string;\n /** Current warning level */\n readonly warningLevel: TimerWarningLevel;\n /** Whether time is up */\n readonly isTimeUp: boolean;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n /** Optional custom position styling */\n readonly style?: React.CSSProperties;\n}\n\n/**\n * Get timer color based on warning level\n */\nfunction getTimerColor(\n warningLevel: TimerWarningLevel,\n isTimeUp: boolean,\n): number {\n if (isTimeUp) {\n return KOREAN_COLORS.NEGATIVE_RED; // Red for time up\n }\n switch (warningLevel) {\n case \"urgent\":\n return KOREAN_COLORS.NEGATIVE_RED; // Red (≤5s)\n case \"warning\":\n return KOREAN_COLORS.SECONDARY_YELLOW; // Yellow (≤10s)\n case \"none\":\n default:\n return KOREAN_COLORS.PRIMARY_CYAN; // Cyan (>10s)\n }\n}\n\n/**\n * CombatTimer Component\n *\n * Displays round timer with Korean cyberpunk aesthetic and responsive design.\n *\n * Features:\n * - MM:SS format display\n * - Color changes: cyan (>10s), yellow (≤10s), red (≤5s)\n * - Pulsing animation when time ≤5s\n * - \"Time's Up!\" message when timer reaches 0\n * - Responsive sizing for mobile/tablet/desktop\n * - Korean-English bilingual support\n *\n * Korean: 전투 타이머 컴포넌트\n *\n * @example\n * ```tsx\n * <CombatTimer\n * formattedTime=\"03:45\"\n * warningLevel=\"none\"\n * isTimeUp={false}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatTimer: React.FC<CombatTimerProps> = ({\n formattedTime,\n warningLevel,\n isTimeUp,\n isMobile,\n style = {},\n}) => {\n useEffect(() => {\n injectPulseAnimation();\n }, []);\n\n const timerColor = getTimerColor(warningLevel, isTimeUp);\n const timerColorCSS = useMemo(() => hexColorToCSS(timerColor), [timerColor]);\n\n const bgColor = useMemo(\n () => hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK),\n [],\n );\n\n const borderColor = useMemo(() => {\n if (isTimeUp) return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n if (warningLevel === \"urgent\")\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n if (warningLevel === \"warning\")\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW);\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }, [warningLevel, isTimeUp]);\n\n const shouldFlash = warningLevel === \"urgent\" || isTimeUp;\n\n const fontSize = isMobile ? \"32px\" : \"48px\";\n const labelFontSize = isMobile ? \"12px\" : \"14px\";\n\n const displayText = isTimeUp ? \"시간 종료 | Time's Up!\" : formattedTime;\n\n return (\n <div\n data-testid=\"combat-timer\"\n role=\"timer\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n aria-label={`Time remaining: ${formattedTime}`}\n className=\"hud-animated\"\n style={{\n position: \"absolute\",\n top: isMobile ? \"8px\" : \"12px\",\n left: \"50%\",\n transform: \"translateX(-50%)\",\n fontSize,\n fontFamily: \"monospace\",\n fontWeight: \"bold\",\n color: timerColorCSS,\n textShadow: `0 0 ${isMobile ? \"8px\" : \"12px\"} ${timerColorCSS}, 0 0 ${\n isMobile ? \"16px\" : \"24px\"\n } ${timerColorCSS}`,\n padding: isMobile ? \"8px 16px\" : \"12px 24px\",\n backgroundColor: `${bgColor}dd`,\n border: `2px solid ${borderColor}`,\n borderRadius: isMobile ? \"6px\" : \"8px\",\n animation: shouldFlash ? \"timerFlash 1s ease-in-out infinite\" : \"none\",\n zIndex: 100,\n pointerEvents: \"none\",\n userSelect: \"none\",\n minWidth: isMobile ? \"120px\" : \"160px\",\n textAlign: \"center\",\n boxShadow: `0 0 ${isMobile ? \"12px\" : \"20px\"} ${borderColor}40`,\n transition: \"all 0.3s ease-in-out\",\n ...style,\n }}\n >\n {/* Label */}\n {!isTimeUp && (\n <div\n style={{\n fontSize: labelFontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n color: timerColorCSS,\n marginBottom: \"2px\",\n opacity: 0.8,\n }}\n >\n 시간 | TIME\n </div>\n )}\n\n {/* Timer display */}\n <div>{displayText}</div>\n </div>\n );\n};\n\nexport default CombatTimer;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,qBAAqB;AAE3B,IAAM,6BAA6B;CACjC,IAAI,SAAS,eAAe,kBAAkB,GAAG;CAEjD,MAAM,QAAQ,SAAS,cAAc,OAAO;CAC5C,MAAM,KAAK;CACX,MAAM,cAAc;;;;;;;;;;;;;;;;CAgBpB,SAAS,KAAK,YAAY,KAAK;AACjC;;;;AAqBA,SAAS,cACP,cACA,UACQ;CACR,IAAI,UACF,OAAO,cAAc;CAEvB,QAAQ,cAAR;EACE,KAAK,UACH,OAAO,cAAc;EACvB,KAAK,WACH,OAAO,cAAc;EAEvB,SACE,OAAO,cAAc;CACzB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,IAAa,eAA2C,EACtD,eACA,cACA,UACA,UACA,QAAQ,CAAC,QACL;CACJ,gBAAgB;EACd,qBAAqB;CACvB,GAAG,CAAC,CAAC;CAEL,MAAM,aAAa,cAAc,cAAc,QAAQ;CACvD,MAAM,gBAAgB,cAAc,cAAc,UAAU,GAAG,CAAC,UAAU,CAAC;CAE3E,MAAM,UAAU,cACR,cAAc,cAAc,kBAAkB,GACpD,CAAC,CACH;CAEA,MAAM,cAAc,cAAc;EAChC,IAAI,UAAU,OAAO,cAAc,cAAc,YAAY;EAC7D,IAAI,iBAAiB,UACnB,OAAO,cAAc,cAAc,YAAY;EACjD,IAAI,iBAAiB,WACnB,OAAO,cAAc,cAAc,gBAAgB;EACrD,OAAO,cAAc,cAAc,YAAY;CACjD,GAAG,CAAC,cAAc,QAAQ,CAAC;CAE3B,MAAM,cAAc,iBAAiB,YAAY;CAEjD,MAAM,WAAW,WAAW,SAAS;CACrC,MAAM,gBAAgB,WAAW,SAAS;CAE1C,MAAM,cAAc,WAAW,uBAAuB;CAEtD,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,MAAK;EACL,aAAU;EACV,eAAY;EACZ,cAAY,mBAAmB;EAC/B,WAAU;EACV,OAAO;GACL,UAAU;GACV,KAAK,WAAW,QAAQ;GACxB,MAAM;GACN,WAAW;GACX;GACA,YAAY;GACZ,YAAY;GACZ,OAAO;GACP,YAAY,OAAO,WAAW,QAAQ,OAAO,GAAG,cAAc,QAC5D,WAAW,SAAS,OACrB,GAAG;GACJ,SAAS,WAAW,aAAa;GACjC,iBAAiB,GAAG,QAAQ;GAC5B,QAAQ,aAAa;GACrB,cAAc,WAAW,QAAQ;GACjC,WAAW,cAAc,uCAAuC;GAChE,QAAQ;GACR,eAAe;GACf,YAAY;GACZ,UAAU,WAAW,UAAU;GAC/B,WAAW;GACX,WAAW,OAAO,WAAW,SAAS,OAAO,GAAG,YAAY;GAC5D,YAAY;GACZ,GAAG;EACL;YAhCF,CAmCG,CAAC,YACA,oBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,YAAY,YAAY;IACxB,OAAO;IACP,cAAc;IACd,SAAS;GACX;aACD;EAEI,CAAA,GAIP,oBAAC,OAAD,EAAA,UAAM,YAAiB,CAAA,CACpB;;AAET"}
|