blacktrigram 0.7.39 → 0.7.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.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.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.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.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.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.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.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 +3 -90
- package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
- 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/DefensiveAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/RecoveryAnimations.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.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/core/AnimationHitTiming.js.map +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
- package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- 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/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.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.map +1 -1
- package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/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.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.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/GonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/SonTechniques.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/types/AccessibilityTypes.js.map +1 -1
- package/lib/types/PhysicsTypes.js.map +1 -1
- package/lib/types/common.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/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/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/physics.js.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.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 +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrainingModeSelectorOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/TrainingModeSelectorOverlayHtml.tsx"],"sourcesContent":["/**\n * TrainingModeSelectorOverlayHtml - Html overlay for training mode selection\n *\n * Allows switching between different training modes with consistent Korean theming.\n * Uses KOREAN_COLORS constants and bilingual formatting.\n *\n * @module components/screens/training\n * @category Training UI\n * @korean 훈련모드선택오버레이\n */\n\nimport React from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { SPACING } from \"../../../../types/constants/ui\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport {\n formatBilingualText,\n getEnhancedKoreanOverlayStyles,\n getResponsiveSpacing,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport {\n getNeonTextShadow,\n getSmoothTransition,\n} from \"../../../../utils/visualEffects\";\nimport \"../training.css\";\n\n/**\n * Training mode types\n */\nexport type TrainingMode =\n | \"basics\" // Basic Training - Simple striking practice\n | \"advanced\" // Advanced Training - Vital point precision\n | \"free\" // Free Practice - Open combat vs AI\n | \"stance_training\" // Stance Training - Practice 8 trigrams\n | \"vital_point\" // Vital Point Training - Precision targeting\n | \"combo_practice\" // Combo Practice - Multi-hit techniques\n | \"footwork\"; // Footwork Training - Movement drills (보법 훈련)\n\n/**\n * Props for TrainingModeSelectorOverlayHtml component\n */\nexport interface TrainingModeSelectorOverlayHtmlProps {\n /** Currently selected training mode */\n readonly currentMode: TrainingMode;\n /** Callback when mode changes */\n readonly onModeChange: (mode: TrainingMode) => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * Mode information\n */\nconst MODE_INFO: Record<\n TrainingMode,\n { korean: string; english: string; description: string }\n> = {\n basics: {\n korean: \"기본 훈련\",\n english: \"Basic Training\",\n description: \"기초 타격 연습 | Basic striking practice\",\n },\n advanced: {\n korean: \"고급 훈련\",\n english: \"Advanced Training\",\n description: \"급소 정밀 타격 | Vital point precision\",\n },\n free: {\n korean: \"자유 훈련\",\n english: \"Free Practice\",\n description: \"AI 대련 | Combat vs AI opponent\",\n },\n stance_training: {\n korean: \"팔괘 수련\",\n english: \"Stance Training\",\n description: \"팔괘 전환 연습 | Eight trigrams mastery\",\n },\n vital_point: {\n korean: \"급소 훈련\",\n english: \"Vital Point Training\",\n description: \"정밀 타격 연습 | Precision targeting drill\",\n },\n combo_practice: {\n korean: \"연속 기술\",\n english: \"Combo Practice\",\n description: \"연속 타격 훈련 | Multi-hit techniques\",\n },\n footwork: {\n korean: \"보법 훈련\",\n english: \"Footwork Training\",\n description: \"보법 수련 | Movement drills\",\n },\n};\n\n/**\n * TrainingModeSelectorOverlayHtml Component\n *\n * Html overlay for selecting training mode with Korean theming.\n * Compact horizontal grid layout optimized for desktop and mobile.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when currentMode hasn't changed\n * - Callback expected to be stable (parent should use useCallback)\n *\n * @example\n * ```tsx\n * <TrainingModeSelectorOverlayHtml\n * currentMode=\"vital_point\"\n * onModeChange={(mode) => console.log(mode)}\n * isMobile={false}\n * />\n * ```\n *\n * @korean 훈련모드선택오버레이컴포넌트\n */\nexport const TrainingModeSelectorOverlayHtml =\n React.memo<TrainingModeSelectorOverlayHtmlProps>(\n ({ currentMode, onModeChange, isMobile }) => {\n // Use 100% width to fill container instead of fixed width\n const padding = getResponsiveSpacing(\"sm\", isMobile);\n const gap = getResponsiveSpacing(\"xs\", isMobile);\n\n // Enhanced panel styles with neon glow - now uses 100% width\n const panelStyle: React.CSSProperties = {\n ...getEnhancedKoreanOverlayStyles({\n opacity: 0.9,\n glowIntensity: \"medium\",\n includeGradient: false,\n includeBackdropBlur: true,\n depthLayers: 3,\n }),\n width: \"100%\",\n padding: `${padding}px`,\n boxSizing: \"border-box\",\n };\n\n const titleFontSize = isMobile ? 13 : 15;\n const descFontSize = isMobile ? 9 : 10;\n\n return (\n <div style={panelStyle} data-testid=\"training-mode-selector-html\">\n {/* Header with bilingual title */}\n <div style={{ marginBottom: `${SPACING.SM}px`, textAlign: \"center\" }}>\n <div\n style={{\n fontSize: `${titleFontSize}px`,\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(\n KOREAN_COLORS.PRIMARY_CYAN,\n \"medium\",\n ),\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n {formatBilingualText(\"훈련 모드\", \"Training Mode\", \"pipe\")}\n </div>\n <div\n style={{\n fontSize: `${descFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n fontStyle: \"italic\",\n marginTop: \"2px\",\n minHeight: \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n {MODE_INFO[currentMode].description}\n </div>\n </div>\n\n {/* Mode Buttons - Grid layout (2 columns to fit narrow side HUD) */}\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(2, 1fr)\",\n gap: `${gap}px`,\n }}\n >\n {(Object.keys(MODE_INFO) as TrainingMode[]).map((mode) => {\n const isSelected = mode === currentMode;\n const info = MODE_INFO[mode];\n\n return (\n <button\n key={mode}\n onClick={() => onModeChange(mode)}\n className={`mode-button ${isSelected ? \"selected\" : \"\"}`}\n style={{\n padding: isMobile ? \"6px 4px\" : \"8px 6px\",\n textAlign: \"center\",\n fontSize: isMobile ? \"10px\" : \"11px\",\n minHeight: isMobile ? \"45px\" : \"50px\",\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n data-testid={`mode-${mode}`}\n >\n <div\n style={{\n fontWeight: \"bold\",\n color: hexToRgbaString(\n isSelected\n ? KOREAN_COLORS.PRIMARY_CYAN\n : KOREAN_COLORS.TEXT_PRIMARY,\n ),\n fontSize: isMobile ? \"11px\" : \"12px\",\n marginBottom: \"2px\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n width: \"100%\",\n minWidth: 0,\n }}\n >\n {info.korean}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"8px\" : \"9px\",\n color: hexToRgbaString(\n isSelected\n ? KOREAN_COLORS.ACCENT_GOLD\n : KOREAN_COLORS.TEXT_TERTIARY,\n ),\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n width: \"100%\",\n minWidth: 0,\n }}\n >\n {info.english}\n </div>\n </button>\n );\n })}\n </div>\n </div>\n );\n },\n (prevProps, nextProps) => {\n // Only re-render if current mode, mobile state, or mode change callback changes.\n // Including onModeChange here prevents stale callback closures when the parent\n // provides a new function that captures updated state.\n return (\n prevProps.currentMode === nextProps.currentMode &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.onModeChange === nextProps.onModeChange\n );\n },\n );\n\nTrainingModeSelectorOverlayHtml.displayName = \"TrainingModeSelectorOverlayHtml\";\n\nexport default TrainingModeSelectorOverlayHtml;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TrainingModeSelectorOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/TrainingModeSelectorOverlayHtml.tsx"],"sourcesContent":["/**\n * TrainingModeSelectorOverlayHtml - Html overlay for training mode selection\n *\n * Allows switching between different training modes with consistent Korean theming.\n * Uses KOREAN_COLORS constants and bilingual formatting.\n *\n * @module components/screens/training\n * @category Training UI\n * @korean 훈련모드선택오버레이\n */\n\nimport React from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { SPACING } from \"../../../../types/constants/ui\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport {\n formatBilingualText,\n getEnhancedKoreanOverlayStyles,\n getResponsiveSpacing,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport {\n getNeonTextShadow,\n getSmoothTransition,\n} from \"../../../../utils/visualEffects\";\nimport \"../training.css\";\n\n/**\n * Training mode types\n */\nexport type TrainingMode =\n | \"basics\" // Basic Training - Simple striking practice\n | \"advanced\" // Advanced Training - Vital point precision\n | \"free\" // Free Practice - Open combat vs AI\n | \"stance_training\" // Stance Training - Practice 8 trigrams\n | \"vital_point\" // Vital Point Training - Precision targeting\n | \"combo_practice\" // Combo Practice - Multi-hit techniques\n | \"footwork\"; // Footwork Training - Movement drills (보법 훈련)\n\n/**\n * Props for TrainingModeSelectorOverlayHtml component\n */\nexport interface TrainingModeSelectorOverlayHtmlProps {\n /** Currently selected training mode */\n readonly currentMode: TrainingMode;\n /** Callback when mode changes */\n readonly onModeChange: (mode: TrainingMode) => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * Mode information\n */\nconst MODE_INFO: Record<\n TrainingMode,\n { korean: string; english: string; description: string }\n> = {\n basics: {\n korean: \"기본 훈련\",\n english: \"Basic Training\",\n description: \"기초 타격 연습 | Basic striking practice\",\n },\n advanced: {\n korean: \"고급 훈련\",\n english: \"Advanced Training\",\n description: \"급소 정밀 타격 | Vital point precision\",\n },\n free: {\n korean: \"자유 훈련\",\n english: \"Free Practice\",\n description: \"AI 대련 | Combat vs AI opponent\",\n },\n stance_training: {\n korean: \"팔괘 수련\",\n english: \"Stance Training\",\n description: \"팔괘 전환 연습 | Eight trigrams mastery\",\n },\n vital_point: {\n korean: \"급소 훈련\",\n english: \"Vital Point Training\",\n description: \"정밀 타격 연습 | Precision targeting drill\",\n },\n combo_practice: {\n korean: \"연속 기술\",\n english: \"Combo Practice\",\n description: \"연속 타격 훈련 | Multi-hit techniques\",\n },\n footwork: {\n korean: \"보법 훈련\",\n english: \"Footwork Training\",\n description: \"보법 수련 | Movement drills\",\n },\n};\n\n/**\n * TrainingModeSelectorOverlayHtml Component\n *\n * Html overlay for selecting training mode with Korean theming.\n * Compact horizontal grid layout optimized for desktop and mobile.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when currentMode hasn't changed\n * - Callback expected to be stable (parent should use useCallback)\n *\n * @example\n * ```tsx\n * <TrainingModeSelectorOverlayHtml\n * currentMode=\"vital_point\"\n * onModeChange={(mode) => console.log(mode)}\n * isMobile={false}\n * />\n * ```\n *\n * @korean 훈련모드선택오버레이컴포넌트\n */\nexport const TrainingModeSelectorOverlayHtml =\n React.memo<TrainingModeSelectorOverlayHtmlProps>(\n ({ currentMode, onModeChange, isMobile }) => {\n // Use 100% width to fill container instead of fixed width\n const padding = getResponsiveSpacing(\"sm\", isMobile);\n const gap = getResponsiveSpacing(\"xs\", isMobile);\n\n // Enhanced panel styles with neon glow - now uses 100% width\n const panelStyle: React.CSSProperties = {\n ...getEnhancedKoreanOverlayStyles({\n opacity: 0.9,\n glowIntensity: \"medium\",\n includeGradient: false,\n includeBackdropBlur: true,\n depthLayers: 3,\n }),\n width: \"100%\",\n padding: `${padding}px`,\n boxSizing: \"border-box\",\n };\n\n const titleFontSize = isMobile ? 13 : 15;\n const descFontSize = isMobile ? 9 : 10;\n\n return (\n <div style={panelStyle} data-testid=\"training-mode-selector-html\">\n {/* Header with bilingual title */}\n <div style={{ marginBottom: `${SPACING.SM}px`, textAlign: \"center\" }}>\n <div\n style={{\n fontSize: `${titleFontSize}px`,\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(\n KOREAN_COLORS.PRIMARY_CYAN,\n \"medium\",\n ),\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n {formatBilingualText(\"훈련 모드\", \"Training Mode\", \"pipe\")}\n </div>\n <div\n style={{\n fontSize: `${descFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n fontStyle: \"italic\",\n marginTop: \"2px\",\n minHeight: \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n {MODE_INFO[currentMode].description}\n </div>\n </div>\n\n {/* Mode Buttons - Grid layout (2 columns to fit narrow side HUD) */}\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(2, 1fr)\",\n gap: `${gap}px`,\n }}\n >\n {(Object.keys(MODE_INFO) as TrainingMode[]).map((mode) => {\n const isSelected = mode === currentMode;\n const info = MODE_INFO[mode];\n\n return (\n <button\n key={mode}\n onClick={() => onModeChange(mode)}\n className={`mode-button ${isSelected ? \"selected\" : \"\"}`}\n style={{\n padding: isMobile ? \"6px 4px\" : \"8px 6px\",\n textAlign: \"center\",\n fontSize: isMobile ? \"10px\" : \"11px\",\n minHeight: isMobile ? \"45px\" : \"50px\",\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n data-testid={`mode-${mode}`}\n >\n <div\n style={{\n fontWeight: \"bold\",\n color: hexToRgbaString(\n isSelected\n ? KOREAN_COLORS.PRIMARY_CYAN\n : KOREAN_COLORS.TEXT_PRIMARY,\n ),\n fontSize: isMobile ? \"11px\" : \"12px\",\n marginBottom: \"2px\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n width: \"100%\",\n minWidth: 0,\n }}\n >\n {info.korean}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"8px\" : \"9px\",\n color: hexToRgbaString(\n isSelected\n ? KOREAN_COLORS.ACCENT_GOLD\n : KOREAN_COLORS.TEXT_TERTIARY,\n ),\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n width: \"100%\",\n minWidth: 0,\n }}\n >\n {info.english}\n </div>\n </button>\n );\n })}\n </div>\n </div>\n );\n },\n (prevProps, nextProps) => {\n // Only re-render if current mode, mobile state, or mode change callback changes.\n // Including onModeChange here prevents stale callback closures when the parent\n // provides a new function that captures updated state.\n return (\n prevProps.currentMode === nextProps.currentMode &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.onModeChange === nextProps.onModeChange\n );\n },\n );\n\nTrainingModeSelectorOverlayHtml.displayName = \"TrainingModeSelectorOverlayHtml\";\n\nexport default TrainingModeSelectorOverlayHtml;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrainingStatsOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/TrainingStatsOverlayHtml.tsx"],"sourcesContent":["/**\n * TrainingStatsOverlayHtml - Html overlay for training statistics\n *\n * Displays score, combo, hits, misses, and accuracy with consistent Korean theming.\n * Uses Korean cyberpunk color palette and bilingual text formatting.\n *\n * @module components/screens/training\n * @category Training UI\n * @korean 훈련통계오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { SPACING } from \"../../../../types/constants/ui\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport {\n formatBilingualText,\n getEnhancedKoreanOverlayStyles,\n getResponsiveSpacing,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport { getMobileKoreanFontSize } from \"../../../../utils/mobileUIUtils\";\nimport { getSafeAreaPadding } from \"../../../../utils/safeAreaUtils\";\nimport {\n getNeonTextShadow,\n getSmoothTransition,\n} from \"../../../../utils/visualEffects\";\n\n/**\n * Training statistics interface\n */\nexport interface TrainingStats {\n readonly score: number;\n readonly combo: number;\n readonly hits: number;\n readonly misses: number;\n readonly accuracy: number;\n readonly sessionDuration?: number;\n readonly bestCombo?: number;\n readonly perfectStrikes?: number;\n}\n\n/**\n * Props for TrainingStatsOverlayHtml component\n */\nexport interface TrainingStatsOverlayHtmlProps {\n /** Current training statistics */\n readonly stats: TrainingStats;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Viewport width for Super HD font scaling */\n readonly width?: number;\n /** Distance to training dummy in meters (for distance-based hit feedback) */\n readonly distanceToDummy?: number;\n /** Effective reach for current technique in meters */\n readonly effectiveReach?: number;\n}\n\n/**\n * TrainingStatsOverlayHtml Component\n *\n * Html overlay displaying training performance metrics with Korean theming.\n * All colors use KOREAN_COLORS constants for consistency.\n *\n * Optimized with React.memo for 60fps performance:\n * - Memoized with custom comparison function\n * - Only re-renders when stats actually change\n * - Reduces unnecessary DOM updates\n *\n * @example\n * ```tsx\n * <TrainingStatsOverlayHtml\n * stats={{ score: 1500, combo: 8, hits: 45, misses: 5, accuracy: 90 }}\n * isMobile={false}\n * />\n * ```\n *\n * @korean 훈련통계오버레이컴포넌트\n */\nexport const TrainingStatsOverlayHtml =\n React.memo<TrainingStatsOverlayHtmlProps>(\n ({\n stats,\n isMobile,\n width = 375,\n distanceToDummy,\n effectiveReach = 0.7, // Default punch reach\n }) => {\n // Use full width of container (passed from parent HUD)\n const panelWidth = width > 10 ? width : isMobile ? 180 : 200;\n const padding = getResponsiveSpacing(\"sm\", isMobile);\n const gap = getResponsiveSpacing(\"xs\", isMobile);\n\n // Safe area support for notched devices\n const safeAreaStyles = useMemo(\n () => (isMobile ? getSafeAreaPadding([\"top\"], padding) : {}),\n [isMobile, padding],\n );\n\n // Format accuracy with memoization\n const formattedAccuracy = useMemo(\n () => stats.accuracy.toFixed(1),\n [stats.accuracy],\n );\n\n // Format session duration\n const formattedDuration = useMemo(() => {\n if (!stats.sessionDuration) return \"00:00\";\n const minutes = Math.floor(stats.sessionDuration / 60);\n const seconds = stats.sessionDuration % 60;\n return `${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`;\n }, [stats.sessionDuration]);\n\n // Format distance to dummy and check if in range\n const distanceInfo = useMemo(() => {\n if (distanceToDummy === undefined) return null;\n const isInRange = distanceToDummy <= effectiveReach;\n return {\n formatted: distanceToDummy.toFixed(2),\n isInRange,\n rangeStatus: isInRange\n ? \"사정거리 내 | In Range\"\n : \"사정거리 밖 | Out of Range\",\n };\n }, [distanceToDummy, effectiveReach]);\n\n // Calculate perfect strike rate\n const perfectRate = useMemo(() => {\n const totalAttempts = stats.hits + stats.misses;\n if (totalAttempts === 0 || !stats.perfectStrikes) return \"0\";\n return ((stats.perfectStrikes / totalAttempts) * 100).toFixed(1);\n }, [stats.hits, stats.misses, stats.perfectStrikes]);\n\n // Enhanced panel styles with neon glow and safe area support\n const panelStyle: React.CSSProperties = {\n ...getEnhancedKoreanOverlayStyles({\n opacity: 0.92,\n glowIntensity: \"medium\",\n includeGradient: false,\n includeBackdropBlur: true,\n depthLayers: 3,\n }),\n ...safeAreaStyles,\n width: `${panelWidth}px`,\n padding: `${padding}px`,\n };\n\n // Header title styles with improved mobile font size (16px+ for Korean)\n const titleFontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", width ?? 375) // 16px minimum\n : 18;\n\n return (\n <div style={panelStyle} data-testid=\"training-stats-html\">\n {/* Header with bilingual title */}\n <div style={{ marginBottom: `${padding}px` }}>\n <div\n style={{\n fontSize: `${titleFontSize}px`,\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(\n KOREAN_COLORS.ACCENT_GOLD,\n \"medium\",\n ),\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n {formatBilingualText(\"훈련 통계\", \"Training Statistics\", \"pipe\")}\n </div>\n </div>\n\n {/* Stats Grid with consistent Korean theming */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${gap}px`,\n }}\n >\n {/* Score - 점수 */}\n <StatRow\n korean=\"점수\"\n english=\"Score\"\n value={stats.score.toLocaleString()}\n color={KOREAN_COLORS.ACCENT_GOLD}\n isMobile={isMobile}\n width={width}\n />\n\n {/* Combo - 콤보 */}\n <StatRow\n korean=\"콤보\"\n english=\"Combo\"\n value={`${stats.combo}x`}\n color={\n stats.combo > 5\n ? KOREAN_COLORS.ACCENT_RED\n : KOREAN_COLORS.PRIMARY_CYAN\n }\n isMobile={isMobile}\n width={width}\n />\n\n {/* Hits - 성공 */}\n <StatRow\n korean=\"성공\"\n english=\"Hits\"\n value={stats.hits.toString()}\n color={KOREAN_COLORS.ACCENT_GREEN}\n isMobile={isMobile}\n width={width}\n />\n\n {/* Misses - 실패 */}\n <StatRow\n korean=\"실패\"\n english=\"Misses\"\n value={stats.misses.toString()}\n color={KOREAN_COLORS.TEXT_TERTIARY}\n isMobile={isMobile}\n width={width}\n />\n\n {/* Accuracy - 정확도 */}\n <StatRow\n korean=\"정확도\"\n english=\"Accuracy\"\n value={`${formattedAccuracy}%`}\n color={\n stats.accuracy >= 80\n ? KOREAN_COLORS.ACCENT_GREEN\n : stats.accuracy >= 50\n ? KOREAN_COLORS.ACCENT_GOLD\n : KOREAN_COLORS.ACCENT_RED\n }\n isMobile={isMobile}\n width={width}\n />\n\n {/* Session Duration - 시간 */}\n {stats.sessionDuration !== undefined && (\n <StatRow\n korean=\"시간\"\n english=\"Duration\"\n value={formattedDuration}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n isMobile={isMobile}\n width={width}\n />\n )}\n\n {/* Best Combo - 최고 콤보 */}\n {stats.bestCombo !== undefined && stats.bestCombo > 0 && (\n <StatRow\n korean=\"최고 콤보\"\n english=\"Best Combo\"\n value={`${stats.bestCombo}x`}\n color={KOREAN_COLORS.ACCENT_GOLD}\n isMobile={isMobile}\n width={width}\n />\n )}\n\n {/* Perfect Rate - 완벽률 */}\n {stats.hits + stats.misses > 0 && (\n <StatRow\n korean=\"완벽률\"\n english=\"Perfect Rate\"\n value={`${perfectRate}%`}\n color={\n parseFloat(perfectRate) >= 30\n ? KOREAN_COLORS.ACCENT_GOLD\n : parseFloat(perfectRate) >= 10\n ? KOREAN_COLORS.PRIMARY_CYAN\n : KOREAN_COLORS.TEXT_TERTIARY\n }\n isMobile={isMobile}\n width={width}\n />\n )}\n\n {/* Distance to Dummy - 거리 */}\n {distanceInfo && (\n <>\n <StatRow\n korean=\"거리\"\n english=\"Distance\"\n value={`${distanceInfo.formatted}m`}\n color={\n distanceInfo.isInRange\n ? KOREAN_COLORS.ACCENT_GREEN\n : KOREAN_COLORS.ACCENT_RED\n }\n isMobile={isMobile}\n width={width}\n />\n <StatRow\n korean=\"상태\"\n english=\"Status\"\n value={\n distanceInfo.rangeStatus.split(\" | \")[isMobile ? 0 : 1]\n }\n color={\n distanceInfo.isInRange\n ? KOREAN_COLORS.ACCENT_GREEN\n : KOREAN_COLORS.ACCENT_RED\n }\n isMobile={isMobile}\n width={width}\n />\n </>\n )}\n </div>\n </div>\n );\n },\n (prevProps, nextProps) => {\n // Custom comparison for optimal re-render prevention\n // Only re-render if stats values actually changed\n return (\n prevProps.stats.score === nextProps.stats.score &&\n prevProps.stats.combo === nextProps.stats.combo &&\n prevProps.stats.hits === nextProps.stats.hits &&\n prevProps.stats.misses === nextProps.stats.misses &&\n prevProps.stats.accuracy === nextProps.stats.accuracy &&\n prevProps.stats.sessionDuration === nextProps.stats.sessionDuration &&\n prevProps.stats.bestCombo === nextProps.stats.bestCombo &&\n prevProps.stats.perfectStrikes === nextProps.stats.perfectStrikes &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.width === nextProps.width &&\n prevProps.distanceToDummy === nextProps.distanceToDummy &&\n prevProps.effectiveReach === nextProps.effectiveReach\n );\n },\n );\n\nTrainingStatsOverlayHtml.displayName = \"TrainingStatsOverlayHtml\";\n\n/**\n * Single stat row component with Korean theming\n *\n * Uses KOREAN_COLORS constants for all text colors\n * Enhanced with smooth transitions and neon glow on value\n *\n * Optimized with React.memo for performance\n *\n * @korean 통계행컴포넌트\n */\nconst StatRow = React.memo<{\n korean: string;\n english: string;\n value: string;\n color: number; // Numeric hex color from KOREAN_COLORS (e.g., 0x00ffff)\n isMobile: boolean;\n width: number; // Width for Super HD font scaling\n}>(({ korean, english, value, color, isMobile, width }) => {\n // Improved font sizes for mobile readability (min 16px for Korean body text)\n const labelFontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", width) // 16px minimum\n : 14;\n const sublabelFontSize = isMobile ? 12 : 11; // Increased from 8-9px\n const valueFontSize = isMobile\n ? getMobileKoreanFontSize(\"MEDIUM\", width) // 18px minimum\n : 20;\n\n return (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n paddingBottom: `${SPACING.SM}px`,\n borderBottom: `1px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.1)}`,\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n <div>\n <div\n style={{\n fontSize: `${labelFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(KOREAN_COLORS.PRIMARY_CYAN, \"subtle\"),\n }}\n >\n {korean}\n </div>\n <div\n style={{\n fontSize: `${sublabelFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {english}\n </div>\n </div>\n <div\n style={{\n fontSize: `${valueFontSize}px`,\n fontWeight: \"bold\",\n color: hexToRgbaString(color),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(color, \"medium\"),\n transition: getSmoothTransition(\"transform, color\", \"normal\"),\n transform: \"scale(1)\",\n }}\n >\n {value}\n </div>\n </div>\n );\n});\n\nStatRow.displayName = \"StatRow\";\n\nexport default TrainingStatsOverlayHtml;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TrainingStatsOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/TrainingStatsOverlayHtml.tsx"],"sourcesContent":["/**\n * TrainingStatsOverlayHtml - Html overlay for training statistics\n *\n * Displays score, combo, hits, misses, and accuracy with consistent Korean theming.\n * Uses Korean cyberpunk color palette and bilingual text formatting.\n *\n * @module components/screens/training\n * @category Training UI\n * @korean 훈련통계오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { SPACING } from \"../../../../types/constants/ui\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport {\n formatBilingualText,\n getEnhancedKoreanOverlayStyles,\n getResponsiveSpacing,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport { getMobileKoreanFontSize } from \"../../../../utils/mobileUIUtils\";\nimport { getSafeAreaPadding } from \"../../../../utils/safeAreaUtils\";\nimport {\n getNeonTextShadow,\n getSmoothTransition,\n} from \"../../../../utils/visualEffects\";\n\n/**\n * Training statistics interface\n */\nexport interface TrainingStats {\n readonly score: number;\n readonly combo: number;\n readonly hits: number;\n readonly misses: number;\n readonly accuracy: number;\n readonly sessionDuration?: number;\n readonly bestCombo?: number;\n readonly perfectStrikes?: number;\n}\n\n/**\n * Props for TrainingStatsOverlayHtml component\n */\nexport interface TrainingStatsOverlayHtmlProps {\n /** Current training statistics */\n readonly stats: TrainingStats;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n /** Viewport width for Super HD font scaling */\n readonly width?: number;\n /** Distance to training dummy in meters (for distance-based hit feedback) */\n readonly distanceToDummy?: number;\n /** Effective reach for current technique in meters */\n readonly effectiveReach?: number;\n}\n\n/**\n * TrainingStatsOverlayHtml Component\n *\n * Html overlay displaying training performance metrics with Korean theming.\n * All colors use KOREAN_COLORS constants for consistency.\n *\n * Optimized with React.memo for 60fps performance:\n * - Memoized with custom comparison function\n * - Only re-renders when stats actually change\n * - Reduces unnecessary DOM updates\n *\n * @example\n * ```tsx\n * <TrainingStatsOverlayHtml\n * stats={{ score: 1500, combo: 8, hits: 45, misses: 5, accuracy: 90 }}\n * isMobile={false}\n * />\n * ```\n *\n * @korean 훈련통계오버레이컴포넌트\n */\nexport const TrainingStatsOverlayHtml =\n React.memo<TrainingStatsOverlayHtmlProps>(\n ({\n stats,\n isMobile,\n width = 375,\n distanceToDummy,\n effectiveReach = 0.7, // Default punch reach\n }) => {\n // Use full width of container (passed from parent HUD)\n const panelWidth = width > 10 ? width : isMobile ? 180 : 200;\n const padding = getResponsiveSpacing(\"sm\", isMobile);\n const gap = getResponsiveSpacing(\"xs\", isMobile);\n\n // Safe area support for notched devices\n const safeAreaStyles = useMemo(\n () => (isMobile ? getSafeAreaPadding([\"top\"], padding) : {}),\n [isMobile, padding],\n );\n\n // Format accuracy with memoization\n const formattedAccuracy = useMemo(\n () => stats.accuracy.toFixed(1),\n [stats.accuracy],\n );\n\n // Format session duration\n const formattedDuration = useMemo(() => {\n if (!stats.sessionDuration) return \"00:00\";\n const minutes = Math.floor(stats.sessionDuration / 60);\n const seconds = stats.sessionDuration % 60;\n return `${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`;\n }, [stats.sessionDuration]);\n\n // Format distance to dummy and check if in range\n const distanceInfo = useMemo(() => {\n if (distanceToDummy === undefined) return null;\n const isInRange = distanceToDummy <= effectiveReach;\n return {\n formatted: distanceToDummy.toFixed(2),\n isInRange,\n rangeStatus: isInRange\n ? \"사정거리 내 | In Range\"\n : \"사정거리 밖 | Out of Range\",\n };\n }, [distanceToDummy, effectiveReach]);\n\n // Calculate perfect strike rate\n const perfectRate = useMemo(() => {\n const totalAttempts = stats.hits + stats.misses;\n if (totalAttempts === 0 || !stats.perfectStrikes) return \"0\";\n return ((stats.perfectStrikes / totalAttempts) * 100).toFixed(1);\n }, [stats.hits, stats.misses, stats.perfectStrikes]);\n\n // Enhanced panel styles with neon glow and safe area support\n const panelStyle: React.CSSProperties = {\n ...getEnhancedKoreanOverlayStyles({\n opacity: 0.92,\n glowIntensity: \"medium\",\n includeGradient: false,\n includeBackdropBlur: true,\n depthLayers: 3,\n }),\n ...safeAreaStyles,\n width: `${panelWidth}px`,\n padding: `${padding}px`,\n };\n\n // Header title styles with improved mobile font size (16px+ for Korean)\n const titleFontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", width ?? 375) // 16px minimum\n : 18;\n\n return (\n <div style={panelStyle} data-testid=\"training-stats-html\">\n {/* Header with bilingual title */}\n <div style={{ marginBottom: `${padding}px` }}>\n <div\n style={{\n fontSize: `${titleFontSize}px`,\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(\n KOREAN_COLORS.ACCENT_GOLD,\n \"medium\",\n ),\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n {formatBilingualText(\"훈련 통계\", \"Training Statistics\", \"pipe\")}\n </div>\n </div>\n\n {/* Stats Grid with consistent Korean theming */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${gap}px`,\n }}\n >\n {/* Score - 점수 */}\n <StatRow\n korean=\"점수\"\n english=\"Score\"\n value={stats.score.toLocaleString()}\n color={KOREAN_COLORS.ACCENT_GOLD}\n isMobile={isMobile}\n width={width}\n />\n\n {/* Combo - 콤보 */}\n <StatRow\n korean=\"콤보\"\n english=\"Combo\"\n value={`${stats.combo}x`}\n color={\n stats.combo > 5\n ? KOREAN_COLORS.ACCENT_RED\n : KOREAN_COLORS.PRIMARY_CYAN\n }\n isMobile={isMobile}\n width={width}\n />\n\n {/* Hits - 성공 */}\n <StatRow\n korean=\"성공\"\n english=\"Hits\"\n value={stats.hits.toString()}\n color={KOREAN_COLORS.ACCENT_GREEN}\n isMobile={isMobile}\n width={width}\n />\n\n {/* Misses - 실패 */}\n <StatRow\n korean=\"실패\"\n english=\"Misses\"\n value={stats.misses.toString()}\n color={KOREAN_COLORS.TEXT_TERTIARY}\n isMobile={isMobile}\n width={width}\n />\n\n {/* Accuracy - 정확도 */}\n <StatRow\n korean=\"정확도\"\n english=\"Accuracy\"\n value={`${formattedAccuracy}%`}\n color={\n stats.accuracy >= 80\n ? KOREAN_COLORS.ACCENT_GREEN\n : stats.accuracy >= 50\n ? KOREAN_COLORS.ACCENT_GOLD\n : KOREAN_COLORS.ACCENT_RED\n }\n isMobile={isMobile}\n width={width}\n />\n\n {/* Session Duration - 시간 */}\n {stats.sessionDuration !== undefined && (\n <StatRow\n korean=\"시간\"\n english=\"Duration\"\n value={formattedDuration}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n isMobile={isMobile}\n width={width}\n />\n )}\n\n {/* Best Combo - 최고 콤보 */}\n {stats.bestCombo !== undefined && stats.bestCombo > 0 && (\n <StatRow\n korean=\"최고 콤보\"\n english=\"Best Combo\"\n value={`${stats.bestCombo}x`}\n color={KOREAN_COLORS.ACCENT_GOLD}\n isMobile={isMobile}\n width={width}\n />\n )}\n\n {/* Perfect Rate - 완벽률 */}\n {stats.hits + stats.misses > 0 && (\n <StatRow\n korean=\"완벽률\"\n english=\"Perfect Rate\"\n value={`${perfectRate}%`}\n color={\n parseFloat(perfectRate) >= 30\n ? KOREAN_COLORS.ACCENT_GOLD\n : parseFloat(perfectRate) >= 10\n ? KOREAN_COLORS.PRIMARY_CYAN\n : KOREAN_COLORS.TEXT_TERTIARY\n }\n isMobile={isMobile}\n width={width}\n />\n )}\n\n {/* Distance to Dummy - 거리 */}\n {distanceInfo && (\n <>\n <StatRow\n korean=\"거리\"\n english=\"Distance\"\n value={`${distanceInfo.formatted}m`}\n color={\n distanceInfo.isInRange\n ? KOREAN_COLORS.ACCENT_GREEN\n : KOREAN_COLORS.ACCENT_RED\n }\n isMobile={isMobile}\n width={width}\n />\n <StatRow\n korean=\"상태\"\n english=\"Status\"\n value={\n distanceInfo.rangeStatus.split(\" | \")[isMobile ? 0 : 1]\n }\n color={\n distanceInfo.isInRange\n ? KOREAN_COLORS.ACCENT_GREEN\n : KOREAN_COLORS.ACCENT_RED\n }\n isMobile={isMobile}\n width={width}\n />\n </>\n )}\n </div>\n </div>\n );\n },\n (prevProps, nextProps) => {\n // Custom comparison for optimal re-render prevention\n // Only re-render if stats values actually changed\n return (\n prevProps.stats.score === nextProps.stats.score &&\n prevProps.stats.combo === nextProps.stats.combo &&\n prevProps.stats.hits === nextProps.stats.hits &&\n prevProps.stats.misses === nextProps.stats.misses &&\n prevProps.stats.accuracy === nextProps.stats.accuracy &&\n prevProps.stats.sessionDuration === nextProps.stats.sessionDuration &&\n prevProps.stats.bestCombo === nextProps.stats.bestCombo &&\n prevProps.stats.perfectStrikes === nextProps.stats.perfectStrikes &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.width === nextProps.width &&\n prevProps.distanceToDummy === nextProps.distanceToDummy &&\n prevProps.effectiveReach === nextProps.effectiveReach\n );\n },\n );\n\nTrainingStatsOverlayHtml.displayName = \"TrainingStatsOverlayHtml\";\n\n/**\n * Single stat row component with Korean theming\n *\n * Uses KOREAN_COLORS constants for all text colors\n * Enhanced with smooth transitions and neon glow on value\n *\n * Optimized with React.memo for performance\n *\n * @korean 통계행컴포넌트\n */\nconst StatRow = React.memo<{\n korean: string;\n english: string;\n value: string;\n color: number; // Numeric hex color from KOREAN_COLORS (e.g., 0x00ffff)\n isMobile: boolean;\n width: number; // Width for Super HD font scaling\n}>(({ korean, english, value, color, isMobile, width }) => {\n // Improved font sizes for mobile readability (min 16px for Korean body text)\n const labelFontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", width) // 16px minimum\n : 14;\n const sublabelFontSize = isMobile ? 12 : 11; // Increased from 8-9px\n const valueFontSize = isMobile\n ? getMobileKoreanFontSize(\"MEDIUM\", width) // 18px minimum\n : 20;\n\n return (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n paddingBottom: `${SPACING.SM}px`,\n borderBottom: `1px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.1)}`,\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n >\n <div>\n <div\n style={{\n fontSize: `${labelFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(KOREAN_COLORS.PRIMARY_CYAN, \"subtle\"),\n }}\n >\n {korean}\n </div>\n <div\n style={{\n fontSize: `${sublabelFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {english}\n </div>\n </div>\n <div\n style={{\n fontSize: `${valueFontSize}px`,\n fontWeight: \"bold\",\n color: hexToRgbaString(color),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: getNeonTextShadow(color, \"medium\"),\n transition: getSmoothTransition(\"transform, color\", \"normal\"),\n transform: \"scale(1)\",\n }}\n >\n {value}\n </div>\n </div>\n );\n});\n\nStatRow.displayName = \"StatRow\";\n\nexport default TrainingStatsOverlayHtml;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitalPointMarker3D.js","names":[],"sources":["../../../../../src/components/screens/training/components/VitalPointMarker3D.tsx"],"sourcesContent":["/**\n * VitalPointMarker3D - Individual vital point marker with hover labels\n *\n * Provides interactive 3D markers for vital points with Korean-English bilingual labels\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport * as THREE from \"three\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport {\n FONT_FAMILY,\n KOREAN_COLORS,\n UI_DIMENSIONS,\n} from \"../../../../types/constants\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../../utils/htmlOverlayHelpers\";\n\n/**\n * Props for VitalPointMarker3D component\n */\nexport interface VitalPointMarker3DProps {\n /** The vital point data to visualize */\n readonly vitalPoint: VitalPoint;\n /** Whether this vital point is currently selected */\n readonly isSelected: boolean;\n /** Whether training mode is active */\n readonly isTraining: boolean;\n /** Whether on mobile device (larger hit targets) */\n readonly isMobile?: boolean;\n /** Callback when vital point is clicked/hit */\n readonly onHit?: (vitalPointId: string) => void;\n /** Base size multiplier (for difficulty modes) */\n readonly sizeMultiplier?: number;\n /** Pulse frequency in Hz (default: 6Hz for selected, 4Hz for training mode) */\n readonly pulseFrequency?: number;\n /** Pulse amplitude (default: 0.25 for selected, 0.15 for training mode) */\n readonly pulseAmplitude?: number;\n /** Maximum emissive intensity for selected/hovered state (default: 3.5) */\n readonly maxEmissiveIntensity?: number;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.POSITIVE_GREEN;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.WARNING_YELLOW;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.NEGATIVE_RED; // Most severe - red for lethal vital points\n default:\n return KOREAN_COLORS.TEXT_SECONDARY;\n }\n};\n\n/**\n * Base scale offset for pulsing animation\n * This value (1.15) ensures the marker pulses around a slightly enlarged baseline,\n * making the pulsing effect more visible without excessive size variation.\n * Used in useFrame hook for scale calculation (see line ~180).\n */\nconst PULSE_BASE_SCALE = 1.15;\n\n/**\n * VitalPointMarker3D Component\n * Individual 3D marker with hover tooltip\n */\nexport const VitalPointMarker3D: React.FC<VitalPointMarker3DProps> = ({\n vitalPoint,\n isSelected,\n isTraining,\n isMobile = false,\n onHit,\n sizeMultiplier = 1.0,\n pulseFrequency,\n pulseAmplitude,\n maxEmissiveIntensity = 3.5,\n}) => {\n // Default pulse settings: higher frequency/amplitude for selected, lower for training mode\n const defaultPulseFrequency = isTraining && !isSelected ? 4 : 6;\n const defaultPulseAmplitude = isTraining && !isSelected ? 0.15 : 0.25;\n \n const activePulseFrequency = pulseFrequency ?? defaultPulseFrequency;\n const activePulseAmplitude = pulseAmplitude ?? defaultPulseAmplitude;\n const meshRef = useRef<THREE.Mesh>(null);\n const [hovered, setHovered] = useState(false);\n\n // Calculate marker size (larger on mobile, adjustable by difficulty)\n const baseSize = isMobile ? 0.15 : 0.1;\n const markerSize = baseSize * sizeMultiplier;\n\n // Reusable vector for scale animation\n const targetScale = useMemo(() => new THREE.Vector3(1, 1, 1), []);\n\n // Animate selected and hovered markers\n // Note: Pulse frequency and amplitude are configurable via props to allow adjustment\n // for extended training sessions where high frequency may cause visual fatigue.\n useFrame((state) => {\n if (!meshRef.current) return;\n\n if (isSelected || hovered) {\n // Pulsing animation with configurable frequency and amplitude\n const pulse = Math.sin(state.clock.elapsedTime * activePulseFrequency) * activePulseAmplitude + PULSE_BASE_SCALE;\n meshRef.current.scale.setScalar(pulse);\n } else {\n // Smooth return to normal scale\n targetScale.set(1, 1, 1);\n meshRef.current.scale.lerp(targetScale, 0.1);\n }\n });\n\n const color = useMemo(\n () => getSeverityColor(vitalPoint.severity),\n [vitalPoint.severity]\n );\n\n // Memoize marker material to avoid recreating on every render\n const markerMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissive: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissiveIntensity: isSelected || hovered ? maxEmissiveIntensity : 2.0,\n // Note: High emissive intensity (default 3.5 for selected) is optimized for\n // a small number of simultaneously highlighted markers. When many markers are\n // active (e.g., displaying all 70 vital points), reduce maxEmissiveIntensity\n // via props or implement LOD to cap total high-intensity markers at ~10-15.\n // See AnatomyOverlay3D.tsx for consistent guidance on emissive thresholds.\n metalness: 0.9, // Increased metalness for more reflective appearance (was 0.8)\n roughness: 0.1, // Reduced roughness for stronger reflections (was 0.2)\n clearcoat: 1.0,\n clearcoatRoughness: 0.05, // Reduced for sharper clearcoat (was 0.1)\n transparent: true,\n opacity: isTraining ? 0.9 : 0.6,\n // Enhanced PBR properties\n transmission: 0.1, // Slight transmission for glass-like effect\n thickness: 0.2,\n ior: 2.4, // High IOR for gem-like appearance\n sheen: 0.3, // Increased sheen\n sheenRoughness: 0.1,\n }),\n [isSelected, hovered, color, maxEmissiveIntensity, isTraining]\n );\n\n // Dispose marker material on unmount or when a new material is created\n useEffect(() => {\n return () => {\n markerMaterial.dispose();\n };\n }, [markerMaterial]);\n\n // Track screen width for responsive distance factor updates on resize\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // Calculate optimal distance factor for tooltip overlay\n const tooltipDistanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"text\", isMobile);\n }, [screenWidth, isMobile]);\n\n // Apply Html overlay styles for tooltip\n const tooltipOverlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(\n \"tooltip\",\n false,\n tooltipDistanceFactor,\n true,\n false\n );\n }, [tooltipDistanceFactor]);\n\n const handleClick = useCallback(() => {\n if (isTraining && onHit) {\n onHit(vitalPoint.id);\n }\n }, [isTraining, onHit, vitalPoint.id]);\n\n return (\n <group>\n {/* Hit target sphere\n Note: Three.js 3D objects lack standard DOM accessibility (aria-label, role, etc.).\n For accessible alternatives, see keyboard shortcuts documented in the UI and\n consider future enhancements for screen reader support via Html overlays. */}\n <mesh\n ref={meshRef}\n onClick={handleClick}\n onPointerOver={() => setHovered(true)}\n onPointerOut={() => setHovered(false)}\n name={`vital-point-marker-${vitalPoint.id}`}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <primitive object={markerMaterial} attach=\"material\" />\n </mesh>\n\n {/* Ring indicator for selected marker */}\n {isSelected && (\n <mesh rotation={[Math.PI / 2, 0, 0]}>\n <ringGeometry args={[markerSize * 1.2, markerSize * 1.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Hover tooltip with Korean-English labels */}\n {hovered && (\n <Html\n position={[0, markerSize + 0.2, 0]}\n center={tooltipOverlayStyle.center}\n distanceFactor={tooltipOverlayStyle.distanceFactor}\n occlude={tooltipOverlayStyle.occlude}\n style={{ pointerEvents: tooltipOverlayStyle.pointerEvents }}\n >\n <div\n style={{\n background: \"rgba(0, 0, 0, 0.9)\",\n border: `2px solid ${isSelected ? \"#ffd700\" : \"#00ffff\"}`,\n borderRadius: \"8px\",\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n boxShadow: \"0 0 15px rgba(0, 255, 255, 0.5)\",\n transform: tooltipOverlayStyle.transform,\n zIndex: tooltipOverlayStyle.zIndex,\n }}\n data-testid={`vital-point-tooltip-${vitalPoint.id}`}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n color: \"#ffd700\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: \"#00ffff\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.english}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: \"#999999\",\n fontStyle: \"italic\",\n }}\n >\n {vitalPoint.names.romanized}\n </div>\n\n {/* Severity indicator */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: `#${new THREE.Color(color).getHexString()}`,\n marginTop: \"6px\",\n borderTop: \"1px solid rgba(255, 255, 255, 0.2)\",\n paddingTop: \"4px\",\n }}\n >\n 심각도 | {vitalPoint.severity}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\nexport default VitalPointMarker3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VitalPointMarker3D.js","names":[],"sources":["../../../../../src/components/screens/training/components/VitalPointMarker3D.tsx"],"sourcesContent":["/**\n * VitalPointMarker3D - Individual vital point marker with hover labels\n *\n * Provides interactive 3D markers for vital points with Korean-English bilingual labels\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport * as THREE from \"three\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport {\n FONT_FAMILY,\n KOREAN_COLORS,\n UI_DIMENSIONS,\n} from \"../../../../types/constants\";\nimport {\n applyHtmlOverlayStyles,\n calculateDistanceFactor,\n} from \"../../../../utils/htmlOverlayHelpers\";\n\n/**\n * Props for VitalPointMarker3D component\n */\nexport interface VitalPointMarker3DProps {\n /** The vital point data to visualize */\n readonly vitalPoint: VitalPoint;\n /** Whether this vital point is currently selected */\n readonly isSelected: boolean;\n /** Whether training mode is active */\n readonly isTraining: boolean;\n /** Whether on mobile device (larger hit targets) */\n readonly isMobile?: boolean;\n /** Callback when vital point is clicked/hit */\n readonly onHit?: (vitalPointId: string) => void;\n /** Base size multiplier (for difficulty modes) */\n readonly sizeMultiplier?: number;\n /** Pulse frequency in Hz (default: 6Hz for selected, 4Hz for training mode) */\n readonly pulseFrequency?: number;\n /** Pulse amplitude (default: 0.25 for selected, 0.15 for training mode) */\n readonly pulseAmplitude?: number;\n /** Maximum emissive intensity for selected/hovered state (default: 3.5) */\n readonly maxEmissiveIntensity?: number;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.POSITIVE_GREEN;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.WARNING_YELLOW;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.NEGATIVE_RED; // Most severe - red for lethal vital points\n default:\n return KOREAN_COLORS.TEXT_SECONDARY;\n }\n};\n\n/**\n * Base scale offset for pulsing animation\n * This value (1.15) ensures the marker pulses around a slightly enlarged baseline,\n * making the pulsing effect more visible without excessive size variation.\n * Used in useFrame hook for scale calculation (see line ~180).\n */\nconst PULSE_BASE_SCALE = 1.15;\n\n/**\n * VitalPointMarker3D Component\n * Individual 3D marker with hover tooltip\n */\nexport const VitalPointMarker3D: React.FC<VitalPointMarker3DProps> = ({\n vitalPoint,\n isSelected,\n isTraining,\n isMobile = false,\n onHit,\n sizeMultiplier = 1.0,\n pulseFrequency,\n pulseAmplitude,\n maxEmissiveIntensity = 3.5,\n}) => {\n // Default pulse settings: higher frequency/amplitude for selected, lower for training mode\n const defaultPulseFrequency = isTraining && !isSelected ? 4 : 6;\n const defaultPulseAmplitude = isTraining && !isSelected ? 0.15 : 0.25;\n \n const activePulseFrequency = pulseFrequency ?? defaultPulseFrequency;\n const activePulseAmplitude = pulseAmplitude ?? defaultPulseAmplitude;\n const meshRef = useRef<THREE.Mesh>(null);\n const [hovered, setHovered] = useState(false);\n\n // Calculate marker size (larger on mobile, adjustable by difficulty)\n const baseSize = isMobile ? 0.15 : 0.1;\n const markerSize = baseSize * sizeMultiplier;\n\n // Reusable vector for scale animation\n const targetScale = useMemo(() => new THREE.Vector3(1, 1, 1), []);\n\n // Animate selected and hovered markers\n // Note: Pulse frequency and amplitude are configurable via props to allow adjustment\n // for extended training sessions where high frequency may cause visual fatigue.\n useFrame((state) => {\n if (!meshRef.current) return;\n\n if (isSelected || hovered) {\n // Pulsing animation with configurable frequency and amplitude\n const pulse = Math.sin(state.clock.elapsedTime * activePulseFrequency) * activePulseAmplitude + PULSE_BASE_SCALE;\n meshRef.current.scale.setScalar(pulse);\n } else {\n // Smooth return to normal scale\n targetScale.set(1, 1, 1);\n meshRef.current.scale.lerp(targetScale, 0.1);\n }\n });\n\n const color = useMemo(\n () => getSeverityColor(vitalPoint.severity),\n [vitalPoint.severity]\n );\n\n // Memoize marker material to avoid recreating on every render\n const markerMaterial = useMemo(\n () =>\n new THREE.MeshPhysicalMaterial({\n color: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissive: isSelected ? KOREAN_COLORS.ACCENT_GOLD : color,\n emissiveIntensity: isSelected || hovered ? maxEmissiveIntensity : 2.0,\n // Note: High emissive intensity (default 3.5 for selected) is optimized for\n // a small number of simultaneously highlighted markers. When many markers are\n // active (e.g., displaying all 70 vital points), reduce maxEmissiveIntensity\n // via props or implement LOD to cap total high-intensity markers at ~10-15.\n // See AnatomyOverlay3D.tsx for consistent guidance on emissive thresholds.\n metalness: 0.9, // Increased metalness for more reflective appearance (was 0.8)\n roughness: 0.1, // Reduced roughness for stronger reflections (was 0.2)\n clearcoat: 1.0,\n clearcoatRoughness: 0.05, // Reduced for sharper clearcoat (was 0.1)\n transparent: true,\n opacity: isTraining ? 0.9 : 0.6,\n // Enhanced PBR properties\n transmission: 0.1, // Slight transmission for glass-like effect\n thickness: 0.2,\n ior: 2.4, // High IOR for gem-like appearance\n sheen: 0.3, // Increased sheen\n sheenRoughness: 0.1,\n }),\n [isSelected, hovered, color, maxEmissiveIntensity, isTraining]\n );\n\n // Dispose marker material on unmount or when a new material is created\n useEffect(() => {\n return () => {\n markerMaterial.dispose();\n };\n }, [markerMaterial]);\n\n // Track screen width for responsive distance factor updates on resize\n const [screenWidth, setScreenWidth] = useState(() =>\n typeof window !== \"undefined\"\n ? window.innerWidth\n : UI_DIMENSIONS.DEFAULT_SCREEN_WIDTH\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n const handleResize = () => {\n setScreenWidth(window.innerWidth);\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // Calculate optimal distance factor for tooltip overlay\n const tooltipDistanceFactor = useMemo(() => {\n return calculateDistanceFactor(screenWidth, \"text\", isMobile);\n }, [screenWidth, isMobile]);\n\n // Apply Html overlay styles for tooltip\n const tooltipOverlayStyle = useMemo(() => {\n return applyHtmlOverlayStyles(\n \"tooltip\",\n false,\n tooltipDistanceFactor,\n true,\n false\n );\n }, [tooltipDistanceFactor]);\n\n const handleClick = useCallback(() => {\n if (isTraining && onHit) {\n onHit(vitalPoint.id);\n }\n }, [isTraining, onHit, vitalPoint.id]);\n\n return (\n <group>\n {/* Hit target sphere\n Note: Three.js 3D objects lack standard DOM accessibility (aria-label, role, etc.).\n For accessible alternatives, see keyboard shortcuts documented in the UI and\n consider future enhancements for screen reader support via Html overlays. */}\n <mesh\n ref={meshRef}\n onClick={handleClick}\n onPointerOver={() => setHovered(true)}\n onPointerOut={() => setHovered(false)}\n name={`vital-point-marker-${vitalPoint.id}`}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <primitive object={markerMaterial} attach=\"material\" />\n </mesh>\n\n {/* Ring indicator for selected marker */}\n {isSelected && (\n <mesh rotation={[Math.PI / 2, 0, 0]}>\n <ringGeometry args={[markerSize * 1.2, markerSize * 1.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Hover tooltip with Korean-English labels */}\n {hovered && (\n <Html\n position={[0, markerSize + 0.2, 0]}\n center={tooltipOverlayStyle.center}\n distanceFactor={tooltipOverlayStyle.distanceFactor}\n occlude={tooltipOverlayStyle.occlude}\n style={{ pointerEvents: tooltipOverlayStyle.pointerEvents }}\n >\n <div\n style={{\n background: \"rgba(0, 0, 0, 0.9)\",\n border: `2px solid ${isSelected ? \"#ffd700\" : \"#00ffff\"}`,\n borderRadius: \"8px\",\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n boxShadow: \"0 0 15px rgba(0, 255, 255, 0.5)\",\n transform: tooltipOverlayStyle.transform,\n zIndex: tooltipOverlayStyle.zIndex,\n }}\n data-testid={`vital-point-tooltip-${vitalPoint.id}`}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n color: \"#ffd700\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: \"#00ffff\",\n marginBottom: \"4px\",\n }}\n >\n {vitalPoint.names.english}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: \"#999999\",\n fontStyle: \"italic\",\n }}\n >\n {vitalPoint.names.romanized}\n </div>\n\n {/* Severity indicator */}\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: `#${new THREE.Color(color).getHexString()}`,\n marginTop: \"6px\",\n borderTop: \"1px solid rgba(255, 255, 255, 0.2)\",\n paddingTop: \"4px\",\n }}\n >\n 심각도 | {vitalPoint.severity}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\nexport default VitalPointMarker3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitalPointTrainingOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/VitalPointTrainingOverlayHtml.tsx"],"sourcesContent":["/**\n * VitalPointTrainingOverlayHtml - Html overlay for vital point selection\n *\n * Provides vital point selection interface with consistent Korean martial arts theming.\n *\n * @module components/screens/training/components/VitalPointTrainingOverlayHtml\n * @category Training UI\n * @korean 급소훈련오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { SPACING } from \"../../../../types/constants/ui\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport {\n formatBilingualText,\n getEnhancedKoreanOverlayStyles,\n getResponsiveSpacing,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport {\n getNeonGlowEffect,\n getNeonTextShadow,\n getSmoothTransition,\n} from \"../../../../utils/visualEffects\";\nimport \"../training.css\";\n\n/**\n * Props for VitalPointTrainingOverlayHtml component\n */\nexport interface VitalPointTrainingOverlayHtmlProps {\n /** Currently selected vital point ID */\n readonly selectedVitalPoint: string | null;\n /** Callback when vital point is selected */\n readonly onVitalPointSelect: (vitalPointId: string) => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * Get Korean color from severity\n * Maps vital point severity to KOREAN_COLORS constants\n *\n * @korean 심각도별색상\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n const colorMap: Record<VitalPointSeverity, number> = {\n [VitalPointSeverity.MINOR]: KOREAN_COLORS.POSITIVE_GREEN,\n [VitalPointSeverity.MODERATE]: KOREAN_COLORS.WARNING_ORANGE,\n [VitalPointSeverity.MAJOR]: KOREAN_COLORS.ACCENT_GOLD,\n [VitalPointSeverity.CRITICAL]: KOREAN_COLORS.ACCENT_RED,\n [VitalPointSeverity.LETHAL]: KOREAN_COLORS.PRIMARY_RED,\n };\n return colorMap[severity] ?? KOREAN_COLORS.TEXT_TERTIARY;\n};\n\n/**\n * Get difficulty stars\n */\nconst getDifficultyStars = (difficulty: number): string => {\n const stars = Math.ceil(difficulty * 5);\n return \"★\".repeat(Math.min(stars, 5)) + \"☆\".repeat(5 - Math.min(stars, 5));\n};\n\n/**\n * VitalPointTrainingOverlayHtml Component\n * Html overlay for vital point selection and information\n */\nexport const VitalPointTrainingOverlayHtml =\n React.memo<VitalPointTrainingOverlayHtmlProps>(\n ({ selectedVitalPoint, onVitalPointSelect, isMobile }) => {\n // Use first 6 vital points for training panel (compact)\n const availableVitalPoints = useMemo(\n () => KOREAN_VITAL_POINTS.slice(0, isMobile ? 4 : 6),\n [isMobile],\n );\n\n const selectedPoint = useMemo(\n () => availableVitalPoints.find((p) => p.id === selectedVitalPoint),\n [availableVitalPoints, selectedVitalPoint],\n );\n\n // Memoize glow effect for selected vital points to avoid repeated calculations\n const selectedVitalPointGlow = React.useMemo(\n () => getNeonGlowEffect(KOREAN_COLORS.ACCENT_GOLD, \"strong\", true),\n [],\n );\n\n const panelWidth = isMobile ? 200 : 240;\n const padding = getResponsiveSpacing(\"sm\", isMobile);\n\n // Compact panel styles\n const panelStyle: React.CSSProperties = {\n ...getEnhancedKoreanOverlayStyles({\n opacity: 0.88,\n glowIntensity: \"medium\",\n includeGradient: false,\n includeBackdropBlur: true,\n depthLayers: 3,\n }),\n width: `${panelWidth}px`,\n padding: `${padding}px`,\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.7)}`,\n };\n\n return (\n <div style={panelStyle} data-testid=\"vital-point-training-html\">\n {/* Compact header */}\n <div style={{ marginBottom: `${SPACING.XS}px` }}>\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"13px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA),\n textShadow: getNeonTextShadow(\n KOREAN_COLORS.SECONDARY_MAGENTA,\n \"subtle\",\n ),\n }}\n >\n {formatBilingualText(\"급소 선택\", \"Vital Point\", \"pipe\")}\n </div>\n </div>\n\n {/* Vital Points List */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${SPACING.SM}px`,\n }}\n >\n {availableVitalPoints.map((point) => {\n const isSelected = point.id === selectedVitalPoint;\n const severityColor = getSeverityColor(point.severity);\n const severityColorRgba = hexToRgbaString(severityColor);\n\n // Enhanced glow effect for selected vital points (memoized to avoid recalculation)\n const glowEffect = isSelected\n ? selectedVitalPointGlow\n : undefined;\n\n return (\n <button\n key={point.id}\n onClick={() => onVitalPointSelect(point.id)}\n className={`vital-point-button ${isSelected ? \"selected\" : \"\"}`}\n style={{\n borderColor: isSelected\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)\n : severityColorRgba,\n fontFamily: FONT_FAMILY.KOREAN,\n boxShadow: glowEffect,\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n data-testid={`vital-point-${point.id}`}\n >\n <div\n style={{\n fontSize: isMobile ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n color: severityColorRgba,\n }}\n >\n {point.names.korean}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n }}\n >\n {point.names.english}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"8px\" : \"9px\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n marginTop: `${SPACING.XS}px`,\n }}\n >\n {getDifficultyStars(point.targetingDifficulty ?? 0.5)}\n </div>\n </button>\n );\n })}\n </div>\n\n {/* Selected Point Details */}\n {selectedPoint && (\n <div\n style={{\n marginTop: `${SPACING.MD}px`,\n paddingTop: `${SPACING.MD}px`,\n borderTop: `1px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY, 0.2)}`,\n }}\n >\n <div\n style={{\n fontSize: isMobile ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n marginBottom: `${SPACING.SM}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {formatBilingualText(\"선택된 급소\", \"Selected Point\", \"pipe\")}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n lineHeight: \"1.4\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n <div style={{ marginBottom: `${SPACING.XS}px` }}>\n <span\n style={{\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n }}\n >\n {formatBilingualText(\"위치\", \"Location\", \"parentheses\")}:\n </span>{\" \"}\n {selectedPoint.category}\n </div>\n <div style={{ marginBottom: `${SPACING.XS}px` }}>\n <span\n style={{\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n }}\n >\n {formatBilingualText(\"심각도\", \"Severity\", \"parentheses\")}:\n </span>{\" \"}\n {selectedPoint.severity}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n marginTop: `${SPACING.SM}px`,\n }}\n >\n {formatBilingualText(\n selectedPoint.description.korean,\n selectedPoint.description.english,\n \"pipe\",\n )}\n </div>\n </div>\n </div>\n )}\n </div>\n );\n },\n (prevProps, nextProps) => {\n // Re-render when vital point selection, mobile state, or callback changes\n // Including callback prop prevents stale closures when parent provides\n // a new function that captures updated state.\n return (\n prevProps.selectedVitalPoint === nextProps.selectedVitalPoint &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.onVitalPointSelect === nextProps.onVitalPointSelect\n );\n },\n );\n\nVitalPointTrainingOverlayHtml.displayName = \"VitalPointTrainingOverlayHtml\";\n\nexport default VitalPointTrainingOverlayHtml;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VitalPointTrainingOverlayHtml.js","names":[],"sources":["../../../../../src/components/screens/training/components/VitalPointTrainingOverlayHtml.tsx"],"sourcesContent":["/**\n * VitalPointTrainingOverlayHtml - Html overlay for vital point selection\n *\n * Provides vital point selection interface with consistent Korean martial arts theming.\n *\n * @module components/screens/training/components/VitalPointTrainingOverlayHtml\n * @category Training UI\n * @korean 급소훈련오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { SPACING } from \"../../../../types/constants/ui\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport {\n formatBilingualText,\n getEnhancedKoreanOverlayStyles,\n getResponsiveSpacing,\n} from \"../../../../utils/koreanThemeHelpers\";\nimport {\n getNeonGlowEffect,\n getNeonTextShadow,\n getSmoothTransition,\n} from \"../../../../utils/visualEffects\";\nimport \"../training.css\";\n\n/**\n * Props for VitalPointTrainingOverlayHtml component\n */\nexport interface VitalPointTrainingOverlayHtmlProps {\n /** Currently selected vital point ID */\n readonly selectedVitalPoint: string | null;\n /** Callback when vital point is selected */\n readonly onVitalPointSelect: (vitalPointId: string) => void;\n /** Whether on mobile device */\n readonly isMobile: boolean;\n}\n\n/**\n * Get Korean color from severity\n * Maps vital point severity to KOREAN_COLORS constants\n *\n * @korean 심각도별색상\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n const colorMap: Record<VitalPointSeverity, number> = {\n [VitalPointSeverity.MINOR]: KOREAN_COLORS.POSITIVE_GREEN,\n [VitalPointSeverity.MODERATE]: KOREAN_COLORS.WARNING_ORANGE,\n [VitalPointSeverity.MAJOR]: KOREAN_COLORS.ACCENT_GOLD,\n [VitalPointSeverity.CRITICAL]: KOREAN_COLORS.ACCENT_RED,\n [VitalPointSeverity.LETHAL]: KOREAN_COLORS.PRIMARY_RED,\n };\n return colorMap[severity] ?? KOREAN_COLORS.TEXT_TERTIARY;\n};\n\n/**\n * Get difficulty stars\n */\nconst getDifficultyStars = (difficulty: number): string => {\n const stars = Math.ceil(difficulty * 5);\n return \"★\".repeat(Math.min(stars, 5)) + \"☆\".repeat(5 - Math.min(stars, 5));\n};\n\n/**\n * VitalPointTrainingOverlayHtml Component\n * Html overlay for vital point selection and information\n */\nexport const VitalPointTrainingOverlayHtml =\n React.memo<VitalPointTrainingOverlayHtmlProps>(\n ({ selectedVitalPoint, onVitalPointSelect, isMobile }) => {\n // Use first 6 vital points for training panel (compact)\n const availableVitalPoints = useMemo(\n () => KOREAN_VITAL_POINTS.slice(0, isMobile ? 4 : 6),\n [isMobile],\n );\n\n const selectedPoint = useMemo(\n () => availableVitalPoints.find((p) => p.id === selectedVitalPoint),\n [availableVitalPoints, selectedVitalPoint],\n );\n\n // Memoize glow effect for selected vital points to avoid repeated calculations\n const selectedVitalPointGlow = React.useMemo(\n () => getNeonGlowEffect(KOREAN_COLORS.ACCENT_GOLD, \"strong\", true),\n [],\n );\n\n const panelWidth = isMobile ? 200 : 240;\n const padding = getResponsiveSpacing(\"sm\", isMobile);\n\n // Compact panel styles\n const panelStyle: React.CSSProperties = {\n ...getEnhancedKoreanOverlayStyles({\n opacity: 0.88,\n glowIntensity: \"medium\",\n includeGradient: false,\n includeBackdropBlur: true,\n depthLayers: 3,\n }),\n width: `${panelWidth}px`,\n padding: `${padding}px`,\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.7)}`,\n };\n\n return (\n <div style={panelStyle} data-testid=\"vital-point-training-html\">\n {/* Compact header */}\n <div style={{ marginBottom: `${SPACING.XS}px` }}>\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"13px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA),\n textShadow: getNeonTextShadow(\n KOREAN_COLORS.SECONDARY_MAGENTA,\n \"subtle\",\n ),\n }}\n >\n {formatBilingualText(\"급소 선택\", \"Vital Point\", \"pipe\")}\n </div>\n </div>\n\n {/* Vital Points List */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${SPACING.SM}px`,\n }}\n >\n {availableVitalPoints.map((point) => {\n const isSelected = point.id === selectedVitalPoint;\n const severityColor = getSeverityColor(point.severity);\n const severityColorRgba = hexToRgbaString(severityColor);\n\n // Enhanced glow effect for selected vital points (memoized to avoid recalculation)\n const glowEffect = isSelected\n ? selectedVitalPointGlow\n : undefined;\n\n return (\n <button\n key={point.id}\n onClick={() => onVitalPointSelect(point.id)}\n className={`vital-point-button ${isSelected ? \"selected\" : \"\"}`}\n style={{\n borderColor: isSelected\n ? hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD)\n : severityColorRgba,\n fontFamily: FONT_FAMILY.KOREAN,\n boxShadow: glowEffect,\n transition: getSmoothTransition(\"all\", \"normal\"),\n }}\n data-testid={`vital-point-${point.id}`}\n >\n <div\n style={{\n fontSize: isMobile ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n color: severityColorRgba,\n }}\n >\n {point.names.korean}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n }}\n >\n {point.names.english}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"8px\" : \"9px\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n marginTop: `${SPACING.XS}px`,\n }}\n >\n {getDifficultyStars(point.targetingDifficulty ?? 0.5)}\n </div>\n </button>\n );\n })}\n </div>\n\n {/* Selected Point Details */}\n {selectedPoint && (\n <div\n style={{\n marginTop: `${SPACING.MD}px`,\n paddingTop: `${SPACING.MD}px`,\n borderTop: `1px solid ${hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY, 0.2)}`,\n }}\n >\n <div\n style={{\n fontSize: isMobile ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n marginBottom: `${SPACING.SM}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {formatBilingualText(\"선택된 급소\", \"Selected Point\", \"pipe\")}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n lineHeight: \"1.4\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n <div style={{ marginBottom: `${SPACING.XS}px` }}>\n <span\n style={{\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n }}\n >\n {formatBilingualText(\"위치\", \"Location\", \"parentheses\")}:\n </span>{\" \"}\n {selectedPoint.category}\n </div>\n <div style={{ marginBottom: `${SPACING.XS}px` }}>\n <span\n style={{\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n }}\n >\n {formatBilingualText(\"심각도\", \"Severity\", \"parentheses\")}:\n </span>{\" \"}\n {selectedPoint.severity}\n </div>\n <div\n style={{\n fontSize: isMobile ? \"9px\" : \"10px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_TERTIARY),\n marginTop: `${SPACING.SM}px`,\n }}\n >\n {formatBilingualText(\n selectedPoint.description.korean,\n selectedPoint.description.english,\n \"pipe\",\n )}\n </div>\n </div>\n </div>\n )}\n </div>\n );\n },\n (prevProps, nextProps) => {\n // Re-render when vital point selection, mobile state, or callback changes\n // Including callback prop prevents stale closures when parent provides\n // a new function that captures updated state.\n return (\n prevProps.selectedVitalPoint === nextProps.selectedVitalPoint &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.onVitalPointSelect === nextProps.onVitalPointSelect\n );\n },\n );\n\nVitalPointTrainingOverlayHtml.displayName = \"VitalPointTrainingOverlayHtml\";\n\nexport default VitalPointTrainingOverlayHtml;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrainingBottomHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingBottomHUD.tsx"],"sourcesContent":["/**\n * TrainingBottomHUD - Bottom bar for training screen\n *\n * Contains:\n * - Technique Bar (centered)\n * - Volume Control (bottom-right, compact)\n * - Feedback Message (centered overlay)\n * - Archetype Selector (mobile only - bottom-left)\n *\n * Gaming Layout Best Practice:\n * - Width: 100% of screen\n * - Height: Resolution-based ~11% of screen height (40-120px range)\n * - On mobile, consolidates controls from TopHUD\n *\n * @korean 훈련화면 하단 바 - 기술 바, 음량, 피드백, 모바일 원형선택\n */\n\nimport React from \"react\";\nimport { PlayerState } from \"../../../../../systems\";\nimport { Technique } from \"../../../../../types\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport { HUD_SIDE_CONTROL_RESERVES } from \"../../../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../../../types/LayoutTypes\";\nimport { SPACING, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport {\n getHUDHeight,\n getResponsivePadding,\n shouldShowMobileControls,\n} from \"../../../../../utils/responsiveLayout\";\nimport { TechniqueBar } from \"../../../../shared/three/ui/TechniqueBar\";\nimport { VolumeControl } from \"../../../../shared/ui/VolumeControl\";\nimport { ArchetypeSelectionButtons } from \"../TrainingButtonsOverlayHtml\";\nimport TrainingFeedbackOverlayHtml from \"../TrainingFeedbackOverlayHtml\";\n\n\n\nexport interface TrainingBottomHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Available techniques for the technique bar */\n readonly techniques: readonly Technique[];\n /** Player state for technique availability checks */\n readonly player: PlayerState;\n /** Currently selected technique index */\n readonly selectedIndex: number;\n /** Active technique cooldowns */\n readonly cooldowns: Map<string, number>;\n /** Handler for technique selection */\n readonly onTechniqueSelect: (index: number) => void;\n /** Whether to show feedback message */\n readonly showFeedback: boolean;\n /** Feedback message to display */\n readonly feedbackMessage: string;\n /** Currently selected archetype (for mobile) */\n readonly selectedArchetype?: PlayerArchetype;\n /** Handler for archetype selection (for mobile) */\n readonly onArchetypeSelect?: (archetype: PlayerArchetype) => void;\n /** Handler for playing sound effects (for mobile) */\n readonly onPlaySFX?: (sound: string) => void;\n}\n\n/**\n * TrainingBottomHUD Component\n *\n * Compact bottom bar with centered technique bar, volume control,\n * and archetype selector on mobile. Uses resolution-based sizing.\n */\nexport const TrainingBottomHUD: React.FC<TrainingBottomHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n showFeedback,\n feedbackMessage,\n selectedArchetype,\n onArchetypeSelect,\n onPlaySFX,\n}) => {\n // isMobile only used for showing mobile controls\n const showMobileControls = shouldShowMobileControls(width, isMobile);\n const showArchetypeSelector =\n showMobileControls &&\n onArchetypeSelect !== undefined &&\n selectedArchetype !== undefined;\n\n const layout = React.useMemo(() => {\n // Resolution-based HUD height (11% of screen height, 40-120px range)\n const hudHeight = getHUDHeight(height, 0.11) * positionScale;\n \n // Resolution-based padding\n const padding = getResponsivePadding(width) * positionScale;\n\n // Compute the actual pixel width available to TechniqueBar so it can make\n // accurate scale/scroll decisions. The technique section has flex:1 but\n // loses pixels to: HUD padding (both sides), the absolute volume control\n // reserve, and (on mobile) the absolute archetype selector reserve.\n const volumeReserve = showMobileControls\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.VOLUME_CONTROL;\n // showArchetypeSelector is only true with mobile controls, so the mobile\n // technique-bar reserve is the only reachable archetype selector reserve.\n const archetypeReserve = showArchetypeSelector\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : 0;\n\n const techniqueBarContainerWidth = Math.max(\n 0,\n width -\n padding * 2 -\n volumeReserve -\n archetypeReserve,\n );\n\n return {\n hudHeight,\n padding,\n volumeReserve,\n archetypeReserve,\n techniqueBarContainerWidth,\n };\n }, [width, height, positionScale, showArchetypeSelector, showMobileControls]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n bottom: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"center\",\n alignItems: \"center\",\n pointerEvents: \"none\",\n padding: `${layout.padding}px`,\n boxSizing: \"border-box\",\n borderTop: BORDERS.default,\n background: GRADIENTS.verticalReverse(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-bottom-hud\"\n >\n {/* Feedback Message (centered in screen, above technique bar) */}\n {showFeedback && (\n <div\n style={{\n position: \"fixed\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n zIndex: Z_INDEX.MODAL,\n pointerEvents: \"none\",\n }}\n >\n <TrainingFeedbackOverlayHtml\n message={feedbackMessage}\n isMobile={showMobileControls}\n />\n </div>\n )}\n\n {/* Technique Bar - centered, embedded mode for proper containment.\n Reserve right space for the absolute Volume Control and (on mobile)\n left space for the Archetype Selector so technique cards never sit\n under the side controls. */}\n <div\n style={{\n pointerEvents: \"all\",\n flex: 1,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n overflow: \"visible\",\n height: \"100%\",\n marginRight: layout.volumeReserve,\n marginLeft: showArchetypeSelector ? layout.archetypeReserve : 0,\n }}\n data-testid=\"training-bottom-hud-technique-section\"\n >\n <TechniqueBar\n techniques={techniques as Technique[]}\n player={player}\n selectedIndex={selectedIndex}\n cooldowns={cooldowns}\n onTechniqueSelect={onTechniqueSelect}\n onTechniqueHover={(_tech) => {}}\n isMobile={showMobileControls}\n screenWidth={width}\n screenHeight={height}\n embedded={true}\n containerWidth={layout.techniqueBarContainerWidth}\n />\n </div>\n\n {/* Volume Control - bottom right corner */}\n <div\n style={{\n position: \"absolute\",\n right: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n }}\n data-testid=\"training-bottom-hud-volume-section\"\n >\n <VolumeControl position=\"custom\" compact={true} />\n </div>\n\n {/* Mobile Archetype Selector - bottom left corner */}\n {showArchetypeSelector && (\n <div\n style={{\n position: \"absolute\",\n left: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n display: \"flex\",\n alignItems: \"center\",\n gap: SPACING.xxs,\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: GRADIENTS.vertical(0.9),\n border: BORDERS.accent,\n borderRadius: SPACING.xxs,\n }}\n data-testid=\"training-bottom-hud-archetype-section\"\n >\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX ?? (() => {})}\n isMobile={showMobileControls}\n />\n </div>\n )}\n </div>\n );\n};\n\nexport default TrainingBottomHUD;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,IAAa,qBAAuD,EAClE,OACA,QACA,WAAW,OACX,eACA,YACA,QACA,eACA,WACA,mBACA,cACA,iBACA,mBACA,mBACA,gBACI;CAEJ,MAAM,qBAAqB,yBAAyB,OAAO,SAAS;CACpE,MAAM,wBACJ,sBACA,sBAAsB,KAAA,KACtB,sBAAsB,KAAA;CAExB,MAAM,SAAS,MAAM,cAAc;EAEjC,MAAM,YAAY,aAAa,QAAQ,IAAK,GAAG;EAG/C,MAAM,UAAU,qBAAqB,MAAM,GAAG;EAM9C,MAAM,gBAAgB,qBAClB,0BAA0B,uBAC1B,0BAA0B;EAG9B,MAAM,mBAAmB,wBACrB,0BAA0B,uBAC1B;AAUJ,SAAO;GACL;GACA;GACA;GACA;GACA,4BAbiC,KAAK,IACtC,GACA,QACE,UAAU,IACV,gBACA,iBAQF;GACD;IACA;EAAC;EAAO;EAAQ;EAAe;EAAuB;EAAmB,CAAC;AAE7E,QACE,qBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,QAAQ;GACR,MAAM;GACN,OAAO;GACP,QAAQ,GAAG,OAAO,UAAU;GAC5B,SAAS;GACT,eAAe;GACf,gBAAgB;GAChB,YAAY;GACZ,eAAe;GACf,SAAS,GAAG,OAAO,QAAQ;GAC3B,WAAW;GACX,WAAW,QAAQ;GACnB,YAAY,UAAU,gBAAgB,GAAI;GAC1C,gBAAgB,UAAU;GAC3B;EACD,eAAY;YAlBd;GAqBG,gBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,KAAK;KACL,MAAM;KACN,WAAW;KACX,QAAQ,QAAQ;KAChB,eAAe;KAChB;cAED,oBAAC,6BAAD;KACE,SAAS;KACT,UAAU;KACV,CAAA;IACE,CAAA;GAOR,oBAAC,OAAD;IACE,OAAO;KACL,eAAe;KACf,MAAM;KACN,SAAS;KACT,eAAe;KACf,gBAAgB;KAChB,YAAY;KACZ,UAAU;KACV,QAAQ;KACR,aAAa,OAAO;KACpB,YAAY,wBAAwB,OAAO,mBAAmB;KAC/D;IACD,eAAY;cAEZ,oBAAC,cAAD;KACc;KACJ;KACO;KACJ;KACQ;KACnB,mBAAmB,UAAU;KAC7B,UAAU;KACV,aAAa;KACb,cAAc;KACd,UAAU;KACV,gBAAgB,OAAO;KACvB,CAAA;IACE,CAAA;GAGN,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,OAAO,GAAG,OAAO,UAAU,IAAI;KAC/B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;KAChB;IACD,eAAY;cAEZ,oBAAC,eAAD;KAAe,UAAS;KAAS,SAAS;KAAQ,CAAA;IAC9C,CAAA;GAGL,yBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,MAAM,GAAG,OAAO,UAAU,IAAI;KAC9B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;KACf,SAAS;KACT,YAAY;KACZ,KAAK,QAAQ;KACb,SAAS,GAAG,QAAQ,IAAI,GAAG,QAAQ;KACnC,YAAY,UAAU,SAAS,GAAI;KACnC,QAAQ,QAAQ;KAChB,cAAc,QAAQ;KACvB;IACD,eAAY;cAEZ,oBAAC,2BAAD;KACqB;KACA;KACnB,WAAW,oBAAoB;KAC/B,UAAU;KACV,CAAA;IACE,CAAA;GAEJ"}
|
|
1
|
+
{"version":3,"file":"TrainingBottomHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingBottomHUD.tsx"],"sourcesContent":["/**\n * TrainingBottomHUD - Bottom bar for training screen\n *\n * Contains:\n * - Technique Bar (centered)\n * - Volume Control (bottom-right, compact)\n * - Feedback Message (centered overlay)\n * - Archetype Selector (mobile only - bottom-left)\n *\n * Gaming Layout Best Practice:\n * - Width: 100% of screen\n * - Height: Resolution-based ~11% of screen height (40-120px range)\n * - On mobile, consolidates controls from TopHUD\n *\n * @korean 훈련화면 하단 바 - 기술 바, 음량, 피드백, 모바일 원형선택\n */\n\nimport React from \"react\";\nimport { PlayerState } from \"../../../../../systems\";\nimport { Technique } from \"../../../../../types\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport { HUD_SIDE_CONTROL_RESERVES } from \"../../../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../../../types/LayoutTypes\";\nimport { SPACING, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport {\n getHUDHeight,\n getResponsivePadding,\n shouldShowMobileControls,\n} from \"../../../../../utils/responsiveLayout\";\nimport { TechniqueBar } from \"../../../../shared/three/ui/TechniqueBar\";\nimport { VolumeControl } from \"../../../../shared/ui/VolumeControl\";\nimport { ArchetypeSelectionButtons } from \"../TrainingButtonsOverlayHtml\";\nimport TrainingFeedbackOverlayHtml from \"../TrainingFeedbackOverlayHtml\";\n\n\n\nexport interface TrainingBottomHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Available techniques for the technique bar */\n readonly techniques: readonly Technique[];\n /** Player state for technique availability checks */\n readonly player: PlayerState;\n /** Currently selected technique index */\n readonly selectedIndex: number;\n /** Active technique cooldowns */\n readonly cooldowns: Map<string, number>;\n /** Handler for technique selection */\n readonly onTechniqueSelect: (index: number) => void;\n /** Whether to show feedback message */\n readonly showFeedback: boolean;\n /** Feedback message to display */\n readonly feedbackMessage: string;\n /** Currently selected archetype (for mobile) */\n readonly selectedArchetype?: PlayerArchetype;\n /** Handler for archetype selection (for mobile) */\n readonly onArchetypeSelect?: (archetype: PlayerArchetype) => void;\n /** Handler for playing sound effects (for mobile) */\n readonly onPlaySFX?: (sound: string) => void;\n}\n\n/**\n * TrainingBottomHUD Component\n *\n * Compact bottom bar with centered technique bar, volume control,\n * and archetype selector on mobile. Uses resolution-based sizing.\n */\nexport const TrainingBottomHUD: React.FC<TrainingBottomHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n showFeedback,\n feedbackMessage,\n selectedArchetype,\n onArchetypeSelect,\n onPlaySFX,\n}) => {\n // isMobile only used for showing mobile controls\n const showMobileControls = shouldShowMobileControls(width, isMobile);\n const showArchetypeSelector =\n showMobileControls &&\n onArchetypeSelect !== undefined &&\n selectedArchetype !== undefined;\n\n const layout = React.useMemo(() => {\n // Resolution-based HUD height (11% of screen height, 40-120px range)\n const hudHeight = getHUDHeight(height, 0.11) * positionScale;\n \n // Resolution-based padding\n const padding = getResponsivePadding(width) * positionScale;\n\n // Compute the actual pixel width available to TechniqueBar so it can make\n // accurate scale/scroll decisions. The technique section has flex:1 but\n // loses pixels to: HUD padding (both sides), the absolute volume control\n // reserve, and (on mobile) the absolute archetype selector reserve.\n const volumeReserve = showMobileControls\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.VOLUME_CONTROL;\n // showArchetypeSelector is only true with mobile controls, so the mobile\n // technique-bar reserve is the only reachable archetype selector reserve.\n const archetypeReserve = showArchetypeSelector\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : 0;\n\n const techniqueBarContainerWidth = Math.max(\n 0,\n width -\n padding * 2 -\n volumeReserve -\n archetypeReserve,\n );\n\n return {\n hudHeight,\n padding,\n volumeReserve,\n archetypeReserve,\n techniqueBarContainerWidth,\n };\n }, [width, height, positionScale, showArchetypeSelector, showMobileControls]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n bottom: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"center\",\n alignItems: \"center\",\n pointerEvents: \"none\",\n padding: `${layout.padding}px`,\n boxSizing: \"border-box\",\n borderTop: BORDERS.default,\n background: GRADIENTS.verticalReverse(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-bottom-hud\"\n >\n {/* Feedback Message (centered in screen, above technique bar) */}\n {showFeedback && (\n <div\n style={{\n position: \"fixed\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n zIndex: Z_INDEX.MODAL,\n pointerEvents: \"none\",\n }}\n >\n <TrainingFeedbackOverlayHtml\n message={feedbackMessage}\n isMobile={showMobileControls}\n />\n </div>\n )}\n\n {/* Technique Bar - centered, embedded mode for proper containment.\n Reserve right space for the absolute Volume Control and (on mobile)\n left space for the Archetype Selector so technique cards never sit\n under the side controls. */}\n <div\n style={{\n pointerEvents: \"all\",\n flex: 1,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n overflow: \"visible\",\n height: \"100%\",\n marginRight: layout.volumeReserve,\n marginLeft: showArchetypeSelector ? layout.archetypeReserve : 0,\n }}\n data-testid=\"training-bottom-hud-technique-section\"\n >\n <TechniqueBar\n techniques={techniques as Technique[]}\n player={player}\n selectedIndex={selectedIndex}\n cooldowns={cooldowns}\n onTechniqueSelect={onTechniqueSelect}\n onTechniqueHover={(_tech) => {}}\n isMobile={showMobileControls}\n screenWidth={width}\n screenHeight={height}\n embedded={true}\n containerWidth={layout.techniqueBarContainerWidth}\n />\n </div>\n\n {/* Volume Control - bottom right corner */}\n <div\n style={{\n position: \"absolute\",\n right: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n }}\n data-testid=\"training-bottom-hud-volume-section\"\n >\n <VolumeControl position=\"custom\" compact={true} />\n </div>\n\n {/* Mobile Archetype Selector - bottom left corner */}\n {showArchetypeSelector && (\n <div\n style={{\n position: \"absolute\",\n left: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n display: \"flex\",\n alignItems: \"center\",\n gap: SPACING.xxs,\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: GRADIENTS.vertical(0.9),\n border: BORDERS.accent,\n borderRadius: SPACING.xxs,\n }}\n data-testid=\"training-bottom-hud-archetype-section\"\n >\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX ?? (() => {})}\n isMobile={showMobileControls}\n />\n </div>\n )}\n </div>\n );\n};\n\nexport default TrainingBottomHUD;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,IAAa,qBAAuD,EAClE,OACA,QACA,WAAW,OACX,eACA,YACA,QACA,eACA,WACA,mBACA,cACA,iBACA,mBACA,mBACA,gBACI;CAEJ,MAAM,qBAAqB,yBAAyB,OAAO,SAAS;CACpE,MAAM,wBACJ,sBACA,sBAAsB,KAAA,KACtB,sBAAsB,KAAA;CAExB,MAAM,SAAS,MAAM,cAAc;EAEjC,MAAM,YAAY,aAAa,QAAQ,IAAK,GAAG;EAG/C,MAAM,UAAU,qBAAqB,MAAM,GAAG;EAM9C,MAAM,gBAAgB,qBAClB,0BAA0B,uBAC1B,0BAA0B;EAG9B,MAAM,mBAAmB,wBACrB,0BAA0B,uBAC1B;EAUJ,OAAO;GACL;GACA;GACA;GACA;GACA,4BAbiC,KAAK,IACtC,GACA,QACE,UAAU,IACV,gBACA,iBAQF;GACD;IACA;EAAC;EAAO;EAAQ;EAAe;EAAuB;EAAmB,CAAC;CAE7E,OACE,qBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,QAAQ;GACR,MAAM;GACN,OAAO;GACP,QAAQ,GAAG,OAAO,UAAU;GAC5B,SAAS;GACT,eAAe;GACf,gBAAgB;GAChB,YAAY;GACZ,eAAe;GACf,SAAS,GAAG,OAAO,QAAQ;GAC3B,WAAW;GACX,WAAW,QAAQ;GACnB,YAAY,UAAU,gBAAgB,GAAI;GAC1C,gBAAgB,UAAU;GAC3B;EACD,eAAY;YAlBd;GAqBG,gBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,KAAK;KACL,MAAM;KACN,WAAW;KACX,QAAQ,QAAQ;KAChB,eAAe;KAChB;cAED,oBAAC,6BAAD;KACE,SAAS;KACT,UAAU;KACV,CAAA;IACE,CAAA;GAOR,oBAAC,OAAD;IACE,OAAO;KACL,eAAe;KACf,MAAM;KACN,SAAS;KACT,eAAe;KACf,gBAAgB;KAChB,YAAY;KACZ,UAAU;KACV,QAAQ;KACR,aAAa,OAAO;KACpB,YAAY,wBAAwB,OAAO,mBAAmB;KAC/D;IACD,eAAY;cAEZ,oBAAC,cAAD;KACc;KACJ;KACO;KACJ;KACQ;KACnB,mBAAmB,UAAU;KAC7B,UAAU;KACV,aAAa;KACb,cAAc;KACd,UAAU;KACV,gBAAgB,OAAO;KACvB,CAAA;IACE,CAAA;GAGN,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,OAAO,GAAG,OAAO,UAAU,IAAI;KAC/B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;KAChB;IACD,eAAY;cAEZ,oBAAC,eAAD;KAAe,UAAS;KAAS,SAAS;KAAQ,CAAA;IAC9C,CAAA;GAGL,yBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,MAAM,GAAG,OAAO,UAAU,IAAI;KAC9B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;KACf,SAAS;KACT,YAAY;KACZ,KAAK,QAAQ;KACb,SAAS,GAAG,QAAQ,IAAI,GAAG,QAAQ;KACnC,YAAY,UAAU,SAAS,GAAI;KACnC,QAAQ,QAAQ;KAChB,cAAc,QAAQ;KACvB;IACD,eAAY;cAEZ,oBAAC,2BAAD;KACqB;KACA;KACnB,WAAW,oBAAoB;KAC/B,UAAU;KACV,CAAA;IACE,CAAA;GAEJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrainingLeftHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingLeftHUD.tsx"],"sourcesContent":["/**\n * TrainingLeftHUD - Left side HUD for training screen\n *\n * Contains:\n * - Anatomy Display controls\n * - Guard Indicator\n *\n * Gaming Layout Best Practice:\n * - Width: Resolution-based 14-18% of screen\n * - Height: 100% minus top/bottom HUD heights\n * - When combined with the right HUD, leaves roughly 64–72% center width for the arena depending on resolution\n *\n * Responsible for sizing and positioning all left-side UI elements.\n * Now uses shared HUD utilities with resolution-based sizing.\n *\n * @korean 훈련화면 왼쪽 HUD - 해부학 표시 및 가드 표시기\n */\n\nimport React from \"react\";\nimport { useHUDLayout } from \"../../../../../hooks/useHUDLayout\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../../types/common\";\nimport { BaseHUDContainer } from \"../../../../shared/ui/BaseHUDContainer\";\nimport { GuardIndicator } from \"../../../../shared/three/indicators/GuardIndicator\";\nimport AnatomyControlsOverlayHtml from \"../AnatomyControlsOverlayHtml\";\nimport type { AnatomyLayer } from \"../AnatomyOverlay3D\";\n\nexport interface TrainingLeftHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Currently visible anatomy layers */\n readonly visibleAnatomyLayers: readonly AnatomyLayer[];\n /** Handler for toggling anatomy layers */\n readonly onAnatomyLayerToggle: (layer: AnatomyLayer) => void;\n /** Current stance index (0-7) */\n readonly currentStanceIndex: number;\n /** Whether player is in guard stance */\n readonly isInGuard: boolean;\n}\n\n/**\n * TrainingLeftHUD Component\n *\n * Left side of the training screen containing anatomy controls and guard indicator.\n * Uses resolution-based sizing for smooth scaling across all screen sizes.\n * Uses shared HUD utilities for consistent layout and styling.\n */\nexport const TrainingLeftHUD: React.FC<TrainingLeftHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n visibleAnatomyLayers,\n onAnatomyLayerToggle,\n currentStanceIndex,\n isInGuard,\n}) => {\n // Use shared HUD layout hook\n const layout = useHUDLayout(\n width,\n height,\n positionScale,\n 'left',\n 'training'\n );\n\n const currentStance: TrigramStance =\n TRIGRAM_STANCES_ORDER[currentStanceIndex];\n const anatomyControlsWidth =\n layout.hudWidth > layout.padding * 2\n ? layout.hudWidth - layout.padding * 2\n : undefined;\n\n return (\n <BaseHUDContainer\n position=\"left\"\n width={layout.hudWidth}\n height={layout.availableHeight}\n topOffset={layout.topOffset}\n padding={layout.padding}\n gap={layout.gap}\n dataTestId=\"training-left-hud\"\n >\n {/* Anatomy Controls */}\n <div\n style={{\n pointerEvents: \"all\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${layout.gap}px`,\n maxWidth: \"100%\",\n }}\n data-testid=\"training-left-hud-anatomy-section\"\n >\n <AnatomyControlsOverlayHtml\n visibleLayers={visibleAnatomyLayers as AnatomyLayer[]}\n onLayerToggle={onAnatomyLayerToggle}\n isMobile={isMobile}\n width={anatomyControlsWidth}\n />\n </div>\n\n {/* Guard Indicator */}\n <div\n style={{ pointerEvents: \"none\", maxWidth: \"100%\" }}\n data-testid=\"training-left-hud-guard-section\"\n >\n <GuardIndicator\n currentStance={currentStance}\n isInGuard={isInGuard}\n position=\"left\"\n isMobile={isMobile}\n />\n </div>\n </BaseHUDContainer>\n );\n};\n\nexport default TrainingLeftHUD;\n"],"mappings":";;;;;;;;;;;;;;;AAqDA,IAAa,mBAAmD,EAC9D,OACA,QACA,WAAW,OACX,eACA,sBACA,sBACA,oBACA,gBACI;CAEJ,MAAM,SAAS,aACb,OACA,QACA,eACA,QACA,WACD;CAED,MAAM,gBACJ,sBAAsB;CACxB,MAAM,uBACJ,OAAO,WAAW,OAAO,UAAU,IAC/B,OAAO,WAAW,OAAO,UAAU,IACnC,KAAA;
|
|
1
|
+
{"version":3,"file":"TrainingLeftHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingLeftHUD.tsx"],"sourcesContent":["/**\n * TrainingLeftHUD - Left side HUD for training screen\n *\n * Contains:\n * - Anatomy Display controls\n * - Guard Indicator\n *\n * Gaming Layout Best Practice:\n * - Width: Resolution-based 14-18% of screen\n * - Height: 100% minus top/bottom HUD heights\n * - When combined with the right HUD, leaves roughly 64–72% center width for the arena depending on resolution\n *\n * Responsible for sizing and positioning all left-side UI elements.\n * Now uses shared HUD utilities with resolution-based sizing.\n *\n * @korean 훈련화면 왼쪽 HUD - 해부학 표시 및 가드 표시기\n */\n\nimport React from \"react\";\nimport { useHUDLayout } from \"../../../../../hooks/useHUDLayout\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../../types/common\";\nimport { BaseHUDContainer } from \"../../../../shared/ui/BaseHUDContainer\";\nimport { GuardIndicator } from \"../../../../shared/three/indicators/GuardIndicator\";\nimport AnatomyControlsOverlayHtml from \"../AnatomyControlsOverlayHtml\";\nimport type { AnatomyLayer } from \"../AnatomyOverlay3D\";\n\nexport interface TrainingLeftHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Currently visible anatomy layers */\n readonly visibleAnatomyLayers: readonly AnatomyLayer[];\n /** Handler for toggling anatomy layers */\n readonly onAnatomyLayerToggle: (layer: AnatomyLayer) => void;\n /** Current stance index (0-7) */\n readonly currentStanceIndex: number;\n /** Whether player is in guard stance */\n readonly isInGuard: boolean;\n}\n\n/**\n * TrainingLeftHUD Component\n *\n * Left side of the training screen containing anatomy controls and guard indicator.\n * Uses resolution-based sizing for smooth scaling across all screen sizes.\n * Uses shared HUD utilities for consistent layout and styling.\n */\nexport const TrainingLeftHUD: React.FC<TrainingLeftHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n visibleAnatomyLayers,\n onAnatomyLayerToggle,\n currentStanceIndex,\n isInGuard,\n}) => {\n // Use shared HUD layout hook\n const layout = useHUDLayout(\n width,\n height,\n positionScale,\n 'left',\n 'training'\n );\n\n const currentStance: TrigramStance =\n TRIGRAM_STANCES_ORDER[currentStanceIndex];\n const anatomyControlsWidth =\n layout.hudWidth > layout.padding * 2\n ? layout.hudWidth - layout.padding * 2\n : undefined;\n\n return (\n <BaseHUDContainer\n position=\"left\"\n width={layout.hudWidth}\n height={layout.availableHeight}\n topOffset={layout.topOffset}\n padding={layout.padding}\n gap={layout.gap}\n dataTestId=\"training-left-hud\"\n >\n {/* Anatomy Controls */}\n <div\n style={{\n pointerEvents: \"all\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: `${layout.gap}px`,\n maxWidth: \"100%\",\n }}\n data-testid=\"training-left-hud-anatomy-section\"\n >\n <AnatomyControlsOverlayHtml\n visibleLayers={visibleAnatomyLayers as AnatomyLayer[]}\n onLayerToggle={onAnatomyLayerToggle}\n isMobile={isMobile}\n width={anatomyControlsWidth}\n />\n </div>\n\n {/* Guard Indicator */}\n <div\n style={{ pointerEvents: \"none\", maxWidth: \"100%\" }}\n data-testid=\"training-left-hud-guard-section\"\n >\n <GuardIndicator\n currentStance={currentStance}\n isInGuard={isInGuard}\n position=\"left\"\n isMobile={isMobile}\n />\n </div>\n </BaseHUDContainer>\n );\n};\n\nexport default TrainingLeftHUD;\n"],"mappings":";;;;;;;;;;;;;;;AAqDA,IAAa,mBAAmD,EAC9D,OACA,QACA,WAAW,OACX,eACA,sBACA,sBACA,oBACA,gBACI;CAEJ,MAAM,SAAS,aACb,OACA,QACA,eACA,QACA,WACD;CAED,MAAM,gBACJ,sBAAsB;CACxB,MAAM,uBACJ,OAAO,WAAW,OAAO,UAAU,IAC/B,OAAO,WAAW,OAAO,UAAU,IACnC,KAAA;CAEN,OACE,qBAAC,kBAAD;EACE,UAAS;EACT,OAAO,OAAO;EACd,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,KAAK,OAAO;EACZ,YAAW;YAPb,CAUE,oBAAC,OAAD;GACE,OAAO;IACL,eAAe;IACf,SAAS;IACT,eAAe;IACf,KAAK,GAAG,OAAO,IAAI;IACnB,UAAU;IACX;GACD,eAAY;aAEZ,oBAAC,4BAAD;IACE,eAAe;IACf,eAAe;IACL;IACV,OAAO;IACP,CAAA;GACE,CAAA,EAGN,oBAAC,OAAD;GACE,OAAO;IAAE,eAAe;IAAQ,UAAU;IAAQ;GAClD,eAAY;aAEZ,oBAAC,gBAAD;IACiB;IACJ;IACX,UAAS;IACC;IACV,CAAA;GACE,CAAA,CACW"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrainingRightHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingRightHUD.tsx"],"sourcesContent":["/**\n * TrainingRightHUD - Right side panel for training screen\n *\n * Layout Order (gaming best practice - most referenced at top):\n * 1. Training Statistics (top) - Always visible, most checked\n * 2. Training Mode Selector (middle) - Changed occasionally\n * 3. Vital Point / Footwork Panel (bottom) - Contextual\n *\n * Gaming Layout Best Practice:\n * - Width: Resolution-based 14-18% of screen\n * - Height: Between top/bottom bars\n * - Scrollable vital point section for long lists\n *\n * Now uses shared HUD utilities with resolution-based sizing.\n *\n * @korean 훈련화면 오른쪽 패널 - 통계(상), 모드(중), 급소(하)\n */\n\nimport React from \"react\";\nimport { useHUDLayout } from \"../../../../../hooks/useHUDLayout\";\nimport { getResponsiveSize } from \"../../../../../utils/responsiveLayout\";\nimport { BaseHUDContainer } from \"../../../../shared/ui/BaseHUDContainer\";\nimport type {\n FootworkDrill,\n TrainingMode,\n TrainingStats,\n} from \"../../hooks/useTrainingState\";\nimport FootworkDrillsOverlayHtml from \"../FootworkDrillsOverlayHtml\";\nimport TrainingModeSelectorOverlayHtml from \"../TrainingModeSelectorOverlayHtml\";\nimport TrainingStatsOverlayHtml from \"../TrainingStatsOverlayHtml\";\nimport VitalPointTrainingOverlayHtml from \"../VitalPointTrainingOverlayHtml\";\n\nexport interface TrainingRightHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Current training mode */\n readonly trainingMode: TrainingMode;\n /** Handler for training mode change */\n readonly onModeChange: (mode: TrainingMode) => void;\n /** Training statistics */\n readonly stats: TrainingStats & {\n readonly sessionDuration?: number;\n readonly bestCombo?: number;\n readonly perfectStrikes?: number;\n };\n /** Distance to training dummy */\n readonly distanceToDummy: number;\n /** Effective reach for current technique */\n readonly effectiveReach: number;\n /** Selected vital point ID */\n readonly selectedVitalPoint: string | null;\n /** Handler for vital point selection */\n readonly onVitalPointSelect: (point: string | null) => void;\n /** Current footwork drill type */\n readonly footworkDrillType: FootworkDrill;\n /** Current footwork drill step */\n readonly footworkDrillStep: number;\n /** Whether footwork drill is active */\n readonly footworkDrillActive: boolean;\n /** Handler to start footwork drill */\n readonly onStartFootworkDrill: (drill: FootworkDrill) => void;\n /** Handler to stop footwork drill */\n readonly onStopFootworkDrill: () => void;\n /** Handler to advance footwork step */\n readonly onAdvanceFootworkStep: () => void;\n}\n\n/**\n * TrainingRightHUD Component\n *\n * Right panel with Stats (top), Mode selector (middle), Vital points (bottom).\n * Uses shared HUD utilities for consistent layout and styling.\n */\nexport const TrainingRightHUD: React.FC<TrainingRightHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n trainingMode,\n onModeChange,\n stats,\n distanceToDummy,\n effectiveReach,\n selectedVitalPoint,\n onVitalPointSelect,\n footworkDrillType,\n footworkDrillStep,\n footworkDrillActive,\n onStartFootworkDrill,\n onStopFootworkDrill,\n onAdvanceFootworkStep,\n}) => {\n // Use shared HUD layout hook with TrainingRightHUD-specific spacing\n // TrainingRightHUD uses tighter spacing than TrainingLeftHUD for denser content\n // Resolution-based gap: mobile 6px, tablet 7px, desktop 8px\n const gapOverride = getResponsiveSize(width, {\n mobile: 6,\n tablet: 7,\n desktop: 8,\n }) * positionScale;\n \n const layout = useHUDLayout(\n width,\n height,\n positionScale,\n 'right',\n 'training',\n undefined, // Use default padding from hook\n gapOverride // Override gap for tighter spacing\n );\n\n return (\n <BaseHUDContainer\n position=\"right\"\n width={layout.hudWidth}\n height={layout.availableHeight}\n topOffset={layout.topOffset}\n padding={layout.padding}\n gap={layout.gap}\n style={{ overflow: \"hidden\" }}\n dataTestId=\"training-right-hud\"\n >\n {/* TOP: Training Stats - most referenced, always visible */}\n <div\n style={{\n pointerEvents: \"all\",\n width: \"100%\",\n flexShrink: 0,\n }}\n data-testid=\"training-right-hud-stats-section\"\n >\n <TrainingStatsOverlayHtml\n stats={stats}\n isMobile={isMobile}\n width={layout.hudWidth - layout.padding * 2}\n distanceToDummy={distanceToDummy}\n effectiveReach={effectiveReach}\n />\n </div>\n\n {/* MIDDLE: Mode Selector - compact */}\n <div\n style={{\n pointerEvents: \"all\",\n width: \"100%\",\n flexShrink: 0,\n }}\n data-testid=\"training-right-hud-mode-section\"\n >\n <TrainingModeSelectorOverlayHtml\n currentMode={trainingMode}\n onModeChange={onModeChange}\n isMobile={isMobile}\n />\n </div>\n\n {/* BOTTOM: Vital Point / Footwork - scrollable for long lists */}\n <div\n style={{\n pointerEvents: \"all\",\n width: \"100%\",\n flex: 1,\n overflow: \"auto\",\n minHeight: 0,\n }}\n data-testid=\"training-right-hud-bottom-section\"\n >\n {trainingMode === \"footwork\" || trainingMode === \"combo_practice\" ? (\n <FootworkDrillsOverlayHtml\n currentDrill={footworkDrillType}\n onDrillChange={onStartFootworkDrill}\n currentStep={footworkDrillStep}\n onStepComplete={onAdvanceFootworkStep}\n isActive={footworkDrillActive}\n onToggleActive={() => {\n if (footworkDrillActive) {\n onStopFootworkDrill();\n } else {\n onStartFootworkDrill(footworkDrillType);\n }\n }}\n isMobile={isMobile}\n />\n ) : (\n <VitalPointTrainingOverlayHtml\n selectedVitalPoint={selectedVitalPoint}\n onVitalPointSelect={onVitalPointSelect}\n isMobile={isMobile}\n />\n )}\n </div>\n </BaseHUDContainer>\n );\n};\n\nexport default TrainingRightHUD;\n"],"mappings":";;;;;;;;;;;;;;;;AA+EA,IAAa,oBAAqD,EAChE,OACA,QACA,WAAW,OACX,eACA,cACA,cACA,OACA,iBACA,gBACA,oBACA,oBACA,mBACA,mBACA,qBACA,sBACA,qBACA,4BACI;CAUJ,MAAM,SAAS,aACb,OACA,QACA,eACA,SACA,YACA,KAAA,GAZkB,kBAAkB,OAAO;EAC3C,QAAQ;EACR,QAAQ;EACR,SAAS;EACV,CAAC,GAAG,cAUJ;
|
|
1
|
+
{"version":3,"file":"TrainingRightHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingRightHUD.tsx"],"sourcesContent":["/**\n * TrainingRightHUD - Right side panel for training screen\n *\n * Layout Order (gaming best practice - most referenced at top):\n * 1. Training Statistics (top) - Always visible, most checked\n * 2. Training Mode Selector (middle) - Changed occasionally\n * 3. Vital Point / Footwork Panel (bottom) - Contextual\n *\n * Gaming Layout Best Practice:\n * - Width: Resolution-based 14-18% of screen\n * - Height: Between top/bottom bars\n * - Scrollable vital point section for long lists\n *\n * Now uses shared HUD utilities with resolution-based sizing.\n *\n * @korean 훈련화면 오른쪽 패널 - 통계(상), 모드(중), 급소(하)\n */\n\nimport React from \"react\";\nimport { useHUDLayout } from \"../../../../../hooks/useHUDLayout\";\nimport { getResponsiveSize } from \"../../../../../utils/responsiveLayout\";\nimport { BaseHUDContainer } from \"../../../../shared/ui/BaseHUDContainer\";\nimport type {\n FootworkDrill,\n TrainingMode,\n TrainingStats,\n} from \"../../hooks/useTrainingState\";\nimport FootworkDrillsOverlayHtml from \"../FootworkDrillsOverlayHtml\";\nimport TrainingModeSelectorOverlayHtml from \"../TrainingModeSelectorOverlayHtml\";\nimport TrainingStatsOverlayHtml from \"../TrainingStatsOverlayHtml\";\nimport VitalPointTrainingOverlayHtml from \"../VitalPointTrainingOverlayHtml\";\n\nexport interface TrainingRightHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Current training mode */\n readonly trainingMode: TrainingMode;\n /** Handler for training mode change */\n readonly onModeChange: (mode: TrainingMode) => void;\n /** Training statistics */\n readonly stats: TrainingStats & {\n readonly sessionDuration?: number;\n readonly bestCombo?: number;\n readonly perfectStrikes?: number;\n };\n /** Distance to training dummy */\n readonly distanceToDummy: number;\n /** Effective reach for current technique */\n readonly effectiveReach: number;\n /** Selected vital point ID */\n readonly selectedVitalPoint: string | null;\n /** Handler for vital point selection */\n readonly onVitalPointSelect: (point: string | null) => void;\n /** Current footwork drill type */\n readonly footworkDrillType: FootworkDrill;\n /** Current footwork drill step */\n readonly footworkDrillStep: number;\n /** Whether footwork drill is active */\n readonly footworkDrillActive: boolean;\n /** Handler to start footwork drill */\n readonly onStartFootworkDrill: (drill: FootworkDrill) => void;\n /** Handler to stop footwork drill */\n readonly onStopFootworkDrill: () => void;\n /** Handler to advance footwork step */\n readonly onAdvanceFootworkStep: () => void;\n}\n\n/**\n * TrainingRightHUD Component\n *\n * Right panel with Stats (top), Mode selector (middle), Vital points (bottom).\n * Uses shared HUD utilities for consistent layout and styling.\n */\nexport const TrainingRightHUD: React.FC<TrainingRightHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n trainingMode,\n onModeChange,\n stats,\n distanceToDummy,\n effectiveReach,\n selectedVitalPoint,\n onVitalPointSelect,\n footworkDrillType,\n footworkDrillStep,\n footworkDrillActive,\n onStartFootworkDrill,\n onStopFootworkDrill,\n onAdvanceFootworkStep,\n}) => {\n // Use shared HUD layout hook with TrainingRightHUD-specific spacing\n // TrainingRightHUD uses tighter spacing than TrainingLeftHUD for denser content\n // Resolution-based gap: mobile 6px, tablet 7px, desktop 8px\n const gapOverride = getResponsiveSize(width, {\n mobile: 6,\n tablet: 7,\n desktop: 8,\n }) * positionScale;\n \n const layout = useHUDLayout(\n width,\n height,\n positionScale,\n 'right',\n 'training',\n undefined, // Use default padding from hook\n gapOverride // Override gap for tighter spacing\n );\n\n return (\n <BaseHUDContainer\n position=\"right\"\n width={layout.hudWidth}\n height={layout.availableHeight}\n topOffset={layout.topOffset}\n padding={layout.padding}\n gap={layout.gap}\n style={{ overflow: \"hidden\" }}\n dataTestId=\"training-right-hud\"\n >\n {/* TOP: Training Stats - most referenced, always visible */}\n <div\n style={{\n pointerEvents: \"all\",\n width: \"100%\",\n flexShrink: 0,\n }}\n data-testid=\"training-right-hud-stats-section\"\n >\n <TrainingStatsOverlayHtml\n stats={stats}\n isMobile={isMobile}\n width={layout.hudWidth - layout.padding * 2}\n distanceToDummy={distanceToDummy}\n effectiveReach={effectiveReach}\n />\n </div>\n\n {/* MIDDLE: Mode Selector - compact */}\n <div\n style={{\n pointerEvents: \"all\",\n width: \"100%\",\n flexShrink: 0,\n }}\n data-testid=\"training-right-hud-mode-section\"\n >\n <TrainingModeSelectorOverlayHtml\n currentMode={trainingMode}\n onModeChange={onModeChange}\n isMobile={isMobile}\n />\n </div>\n\n {/* BOTTOM: Vital Point / Footwork - scrollable for long lists */}\n <div\n style={{\n pointerEvents: \"all\",\n width: \"100%\",\n flex: 1,\n overflow: \"auto\",\n minHeight: 0,\n }}\n data-testid=\"training-right-hud-bottom-section\"\n >\n {trainingMode === \"footwork\" || trainingMode === \"combo_practice\" ? (\n <FootworkDrillsOverlayHtml\n currentDrill={footworkDrillType}\n onDrillChange={onStartFootworkDrill}\n currentStep={footworkDrillStep}\n onStepComplete={onAdvanceFootworkStep}\n isActive={footworkDrillActive}\n onToggleActive={() => {\n if (footworkDrillActive) {\n onStopFootworkDrill();\n } else {\n onStartFootworkDrill(footworkDrillType);\n }\n }}\n isMobile={isMobile}\n />\n ) : (\n <VitalPointTrainingOverlayHtml\n selectedVitalPoint={selectedVitalPoint}\n onVitalPointSelect={onVitalPointSelect}\n isMobile={isMobile}\n />\n )}\n </div>\n </BaseHUDContainer>\n );\n};\n\nexport default TrainingRightHUD;\n"],"mappings":";;;;;;;;;;;;;;;;AA+EA,IAAa,oBAAqD,EAChE,OACA,QACA,WAAW,OACX,eACA,cACA,cACA,OACA,iBACA,gBACA,oBACA,oBACA,mBACA,mBACA,qBACA,sBACA,qBACA,4BACI;CAUJ,MAAM,SAAS,aACb,OACA,QACA,eACA,SACA,YACA,KAAA,GAZkB,kBAAkB,OAAO;EAC3C,QAAQ;EACR,QAAQ;EACR,SAAS;EACV,CAAC,GAAG,cAUJ;CAED,OACE,qBAAC,kBAAD;EACE,UAAS;EACT,OAAO,OAAO;EACd,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,KAAK,OAAO;EACZ,OAAO,EAAE,UAAU,UAAU;EAC7B,YAAW;YARb;GAWE,oBAAC,OAAD;IACE,OAAO;KACL,eAAe;KACf,OAAO;KACP,YAAY;KACb;IACD,eAAY;cAEZ,oBAAC,0BAAD;KACS;KACG;KACV,OAAO,OAAO,WAAW,OAAO,UAAU;KACzB;KACD;KAChB,CAAA;IACE,CAAA;GAGN,oBAAC,OAAD;IACE,OAAO;KACL,eAAe;KACf,OAAO;KACP,YAAY;KACb;IACD,eAAY;cAEZ,oBAAC,iCAAD;KACE,aAAa;KACC;KACJ;KACV,CAAA;IACE,CAAA;GAGN,oBAAC,OAAD;IACE,OAAO;KACL,eAAe;KACf,OAAO;KACP,MAAM;KACN,UAAU;KACV,WAAW;KACZ;IACD,eAAY;cAEX,iBAAiB,cAAc,iBAAiB,mBAC/C,oBAAC,2BAAD;KACE,cAAc;KACd,eAAe;KACf,aAAa;KACb,gBAAgB;KAChB,UAAU;KACV,sBAAsB;MACpB,IAAI,qBACF,qBAAqB;WAErB,qBAAqB,kBAAkB;;KAGjC;KACV,CAAA,GAEF,oBAAC,+BAAD;KACsB;KACA;KACV;KACV,CAAA;IAEA,CAAA;GACW"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrainingTopHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingTopHUD.tsx"],"sourcesContent":["/**\n * TrainingTopHUD - Slim top bar for training screen\n *\n * Gaming Best Practice - Minimal Top Bar:\n * - Training Active/Stop indicator (left)\n * - Vital Point hint + Archetype Selector (center) - desktop only\n * - Return to Menu button (right) - Standard gaming pattern\n *\n * Mobile:\n * - Only shows Training status (left) and Return button (right)\n * - Other controls consolidated in BottomHUD\n *\n * Layout:\n * - Width: 100% of screen\n * - Height: Compact 50-70px (minimal obstruction)\n *\n * @korean 훈련화면 상단 바 - 훈련 상태, 급소 힌트, 원형 선택, 메뉴 복귀\n */\n\nimport React from \"react\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport { TRAINING_TOP_HUD_HEIGHT_PERCENT } from \"../../../../../types/constants/layout\";\nimport { SPACING, SPACING_NUMERIC, SPACING_ADJUSTMENTS, BORDER_RADIUS, TYPOGRAPHY, TYPOGRAPHY_NUMERIC, HIERARCHY, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport { getHUDHeight } from \"../../../../../utils/responsiveLayout\";\nimport {\n ArchetypeSelectionButtons,\n ReturnToMenuButton,\n} from \"../TrainingButtonsOverlayHtml\";\nimport TrainingControlsOverlayHtml from \"../TrainingControlsOverlayHtml\";\n\nexport interface TrainingTopHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile layout is active */\n readonly isMobile: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Whether training is currently active */\n readonly isTraining: boolean;\n /** Handler to start training */\n readonly onStartTraining: () => void;\n /** Handler to stop training */\n readonly onStopTraining: () => void;\n /** Currently selected archetype */\n readonly selectedArchetype: PlayerArchetype;\n /** Handler for archetype selection */\n readonly onArchetypeSelect: (archetype: PlayerArchetype) => void;\n /** Whether vital point overlay is visible */\n readonly overlayVisible: boolean;\n /** Handler for returning to menu */\n readonly onReturnToMenu: () => void;\n /** Handler for playing sound effects */\n readonly onPlaySFX: (sound: string) => void;\n}\n\n/**\n * TrainingTopHUD Component\n *\n * Slim top bar containing training controls, vital point hint, archetype selector,\n * and return to menu button. On mobile, only essential controls shown.\n */\nexport const TrainingTopHUD: React.FC<TrainingTopHUDProps> = ({\n width,\n height,\n isMobile,\n positionScale,\n isTraining,\n onStartTraining,\n onStopTraining,\n selectedArchetype,\n onArchetypeSelect,\n overlayVisible,\n onReturnToMenu,\n onPlaySFX,\n}) => {\n // Layout calculations for slim top bar\n const layout = React.useMemo(() => {\n // Use the exact same formula as useHUDLayout's `topOffset`\n // (getHUDHeight(height, 0.06) * positionScale) so side HUDs start\n // precisely where the top HUD ends on every viewport, including mobile.\n // The shared `getHUDHeight` helper already applies a 40px minimum and\n // 120px maximum, so no extra clamp is needed.\n const hudHeight =\n getHUDHeight(height, TRAINING_TOP_HUD_HEIGHT_PERCENT) * positionScale;\n\n const padding = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const gap = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const fontSize = isMobile ? TYPOGRAPHY_NUMERIC.bodySmall : TYPOGRAPHY_NUMERIC.bodySmall * positionScale;\n\n return {\n hudHeight,\n padding,\n gap,\n fontSize,\n hudWidth: width,\n };\n }, [width, height, isMobile, positionScale]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n padding: `${layout.padding}px`,\n pointerEvents: \"none\",\n boxSizing: \"border-box\",\n borderBottom: BORDERS.default,\n background: GRADIENTS.vertical(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-top-hud\"\n >\n {/* Left Section - Training Controls (compact) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n gap: `${layout.gap}px`,\n pointerEvents: \"all\",\n alignItems: \"center\",\n }}\n data-testid=\"training-top-hud-left-section\"\n >\n <TrainingControlsOverlayHtml\n isTraining={isTraining}\n onStartTraining={onStartTraining}\n onStopTraining={onStopTraining}\n isMobile={isMobile}\n variant=\"compact\"\n />\n </div>\n\n {/* Center Section - Vital Point Hint + Archetype Selector (desktop) */}\n {!isMobile && (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap * 2}px`,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n {/* Vital Point Hint */}\n {!overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING_ADJUSTMENTS.xsPlus}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: BORDER_RADIUS.sm,\n fontSize: `${layout.fontSize}px`,\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n color: HIERARCHY.accent.color,\n whiteSpace: \"nowrap\",\n }}\n data-testid=\"vital-point-hint\"\n >\n 💡 Press{\" \"}\n <span\n style={{\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n }}\n >\n V\n </span>{\" \"}\n for vital points\n </div>\n )}\n\n {/* Archetype Selector */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap}px`,\n padding: `${SPACING_ADJUSTMENTS.compact} ${SPACING.sm}`,\n background: HUD_STYLE.background,\n border: BORDERS.accent,\n borderRadius: BORDER_RADIUS.md,\n pointerEvents: \"all\",\n }}\n >\n <span\n style={{\n fontSize: `${layout.fontSize}px`,\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n whiteSpace: \"nowrap\",\n }}\n >\n 원형 | Archetype:\n </span>\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX}\n isMobile={isMobile}\n />\n </div>\n </div>\n )}\n\n {/* Mobile Center - Just vital point hint */}\n {isMobile && !overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: BORDER_RADIUS.sm,\n fontSize: TYPOGRAPHY.caption.fontSize,\n fontFamily: TYPOGRAPHY.caption.fontFamily,\n color: HIERARCHY.accent.color,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n <span style={{ color: HIERARCHY.gold.color }}>\n V\n </span>{\" \"}\n = 급소\n </div>\n )}\n\n {/* Right Section - Return to Menu */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n pointerEvents: \"all\",\n }}\n data-testid=\"training-top-hud-right-section\"\n >\n <ReturnToMenuButton\n onClick={onReturnToMenu}\n onMouseEnter={() => onPlaySFX(\"menu_hover\")}\n isMobile={isMobile}\n />\n </div>\n </div>\n );\n};\n\nexport default TrainingTopHUD;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TrainingTopHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingTopHUD.tsx"],"sourcesContent":["/**\n * TrainingTopHUD - Slim top bar for training screen\n *\n * Gaming Best Practice - Minimal Top Bar:\n * - Training Active/Stop indicator (left)\n * - Vital Point hint + Archetype Selector (center) - desktop only\n * - Return to Menu button (right) - Standard gaming pattern\n *\n * Mobile:\n * - Only shows Training status (left) and Return button (right)\n * - Other controls consolidated in BottomHUD\n *\n * Layout:\n * - Width: 100% of screen\n * - Height: Compact 50-70px (minimal obstruction)\n *\n * @korean 훈련화면 상단 바 - 훈련 상태, 급소 힌트, 원형 선택, 메뉴 복귀\n */\n\nimport React from \"react\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport { TRAINING_TOP_HUD_HEIGHT_PERCENT } from \"../../../../../types/constants/layout\";\nimport { SPACING, SPACING_NUMERIC, SPACING_ADJUSTMENTS, BORDER_RADIUS, TYPOGRAPHY, TYPOGRAPHY_NUMERIC, HIERARCHY, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport { getHUDHeight } from \"../../../../../utils/responsiveLayout\";\nimport {\n ArchetypeSelectionButtons,\n ReturnToMenuButton,\n} from \"../TrainingButtonsOverlayHtml\";\nimport TrainingControlsOverlayHtml from \"../TrainingControlsOverlayHtml\";\n\nexport interface TrainingTopHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile layout is active */\n readonly isMobile: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Whether training is currently active */\n readonly isTraining: boolean;\n /** Handler to start training */\n readonly onStartTraining: () => void;\n /** Handler to stop training */\n readonly onStopTraining: () => void;\n /** Currently selected archetype */\n readonly selectedArchetype: PlayerArchetype;\n /** Handler for archetype selection */\n readonly onArchetypeSelect: (archetype: PlayerArchetype) => void;\n /** Whether vital point overlay is visible */\n readonly overlayVisible: boolean;\n /** Handler for returning to menu */\n readonly onReturnToMenu: () => void;\n /** Handler for playing sound effects */\n readonly onPlaySFX: (sound: string) => void;\n}\n\n/**\n * TrainingTopHUD Component\n *\n * Slim top bar containing training controls, vital point hint, archetype selector,\n * and return to menu button. On mobile, only essential controls shown.\n */\nexport const TrainingTopHUD: React.FC<TrainingTopHUDProps> = ({\n width,\n height,\n isMobile,\n positionScale,\n isTraining,\n onStartTraining,\n onStopTraining,\n selectedArchetype,\n onArchetypeSelect,\n overlayVisible,\n onReturnToMenu,\n onPlaySFX,\n}) => {\n // Layout calculations for slim top bar\n const layout = React.useMemo(() => {\n // Use the exact same formula as useHUDLayout's `topOffset`\n // (getHUDHeight(height, 0.06) * positionScale) so side HUDs start\n // precisely where the top HUD ends on every viewport, including mobile.\n // The shared `getHUDHeight` helper already applies a 40px minimum and\n // 120px maximum, so no extra clamp is needed.\n const hudHeight =\n getHUDHeight(height, TRAINING_TOP_HUD_HEIGHT_PERCENT) * positionScale;\n\n const padding = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const gap = isMobile ? SPACING_NUMERIC.xs : SPACING_NUMERIC.sm * positionScale;\n const fontSize = isMobile ? TYPOGRAPHY_NUMERIC.bodySmall : TYPOGRAPHY_NUMERIC.bodySmall * positionScale;\n\n return {\n hudHeight,\n padding,\n gap,\n fontSize,\n hudWidth: width,\n };\n }, [width, height, isMobile, positionScale]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n padding: `${layout.padding}px`,\n pointerEvents: \"none\",\n boxSizing: \"border-box\",\n borderBottom: BORDERS.default,\n background: GRADIENTS.vertical(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-top-hud\"\n >\n {/* Left Section - Training Controls (compact) */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n gap: `${layout.gap}px`,\n pointerEvents: \"all\",\n alignItems: \"center\",\n }}\n data-testid=\"training-top-hud-left-section\"\n >\n <TrainingControlsOverlayHtml\n isTraining={isTraining}\n onStartTraining={onStartTraining}\n onStopTraining={onStopTraining}\n isMobile={isMobile}\n variant=\"compact\"\n />\n </div>\n\n {/* Center Section - Vital Point Hint + Archetype Selector (desktop) */}\n {!isMobile && (\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap * 2}px`,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n {/* Vital Point Hint */}\n {!overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING_ADJUSTMENTS.xsPlus}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: BORDER_RADIUS.sm,\n fontSize: `${layout.fontSize}px`,\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n color: HIERARCHY.accent.color,\n whiteSpace: \"nowrap\",\n }}\n data-testid=\"vital-point-hint\"\n >\n 💡 Press{\" \"}\n <span\n style={{\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n }}\n >\n V\n </span>{\" \"}\n for vital points\n </div>\n )}\n\n {/* Archetype Selector */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: `${layout.gap}px`,\n padding: `${SPACING_ADJUSTMENTS.compact} ${SPACING.sm}`,\n background: HUD_STYLE.background,\n border: BORDERS.accent,\n borderRadius: BORDER_RADIUS.md,\n pointerEvents: \"all\",\n }}\n >\n <span\n style={{\n fontSize: `${layout.fontSize}px`,\n color: HIERARCHY.gold.color,\n fontWeight: \"bold\",\n fontFamily: TYPOGRAPHY.bodySmall.fontFamily,\n whiteSpace: \"nowrap\",\n }}\n >\n 원형 | Archetype:\n </span>\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX}\n isMobile={isMobile}\n />\n </div>\n </div>\n )}\n\n {/* Mobile Center - Just vital point hint */}\n {isMobile && !overlayVisible && (\n <div\n style={{\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: HUD_STYLE.background,\n border: BORDERS.muted,\n borderRadius: BORDER_RADIUS.sm,\n fontSize: TYPOGRAPHY.caption.fontSize,\n fontFamily: TYPOGRAPHY.caption.fontFamily,\n color: HIERARCHY.accent.color,\n }}\n data-testid=\"training-top-hud-center-section\"\n >\n <span style={{ color: HIERARCHY.gold.color }}>\n V\n </span>{\" \"}\n = 급소\n </div>\n )}\n\n {/* Right Section - Return to Menu */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"row\",\n alignItems: \"center\",\n pointerEvents: \"all\",\n }}\n data-testid=\"training-top-hud-right-section\"\n >\n <ReturnToMenuButton\n onClick={onReturnToMenu}\n onMouseEnter={() => onPlaySFX(\"menu_hover\")}\n isMobile={isMobile}\n />\n </div>\n </div>\n );\n};\n\nexport default TrainingTopHUD;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAttackMovement.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useAttackMovement.ts"],"sourcesContent":["/**\n * useAttackMovement Hook - Track Attack Movement for 3D Characters\n *\n * Custom hook for managing attack movement physics during animations.\n * Tracks lunge and recovery phases with smooth easing curves.\n *\n * @korean 공격이동훅 - 공격 이동 추적\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { AnimationType } from \"@/systems/animation\";\nimport { AttackMovementPhysics } from \"@/systems/physics\";\nimport { TrigramStance } from \"@/types/common\";\n\n/**\n * Configuration for useAttackMovement hook\n */\nexport interface UseAttackMovementConfig {\n /** Whether character is currently attacking */\n readonly isAttacking: boolean;\n /** Animation type for current attack */\n readonly animationType?: AnimationType;\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Base position of character (when not attacking) */\n readonly basePosition: [number, number, number];\n /** Direction vector for attack (normalized) */\n readonly attackDirection?: THREE.Vector3;\n /** Animation duration in seconds (default: 0.4) */\n readonly animationDuration?: number;\n}\n\n/**\n * Return value from useAttackMovement hook\n */\nexport interface UseAttackMovementResult {\n /** Current 3D position including attack movement */\n readonly currentPosition: [number, number, number];\n /** Whether in forward lunge phase */\n readonly isLunging: boolean;\n /** Whether in recovery return phase */\n readonly isRecovering: boolean;\n /** Progress through attack movement (0-1) */\n readonly progress: number;\n}\n\n/**\n * useAttackMovement hook\n *\n * Tracks attack movement physics and returns current position.\n * Automatically handles lunge and recovery phases with smooth easing.\n *\n * @param config - Attack movement configuration\n * @returns Current position and movement state\n *\n * @example\n * ```typescript\n * const { currentPosition, isLunging } = useAttackMovement({\n * isAttacking: playerIsAttacking,\n * animationType: AnimationType.ROUNDHOUSE_KICK,\n * currentStance: TrigramStance.LI,\n * basePosition: [0, 0, 0],\n * attackDirection: new THREE.Vector3(1, 0, 0),\n * });\n *\n * // Use currentPosition for rendering\n * <CharacterModel position={currentPosition} />\n * ```\n *\n * @korean 공격이동사용\n */\nexport function useAttackMovement(\n config: UseAttackMovementConfig\n): UseAttackMovementResult {\n const {\n isAttacking,\n animationType,\n currentStance,\n basePosition,\n attackDirection,\n animationDuration = 0.4,\n } = config;\n\n // Attack movement physics engine\n const physicsRef = useRef(new AttackMovementPhysics());\n\n // Attack timing\n const attackStartTimeRef = useRef<number | null>(null);\n const attackMovementResultRef = useRef<ReturnType<\n typeof physicsRef.current.calculateAttackMovement\n > | null>(null);\n\n // Current position state\n const [currentPosition, setCurrentPosition] = useState<\n [number, number, number]\n >(basePosition);\n const [isLunging, setIsLunging] = useState(false);\n const [isRecovering, setIsRecovering] = useState(false);\n const [progress, setProgress] = useState(0);\n\n // Track previous attack state\n const wasAttackingRef = useRef(false);\n\n // Keep position synced with basePosition when not attacking\n useEffect(() => {\n if (!isAttacking) {\n setCurrentPosition(basePosition);\n }\n }, [isAttacking, basePosition]);\n\n // Initialize attack movement and manage animation loop\n useEffect(() => {\n if (isAttacking && !wasAttackingRef.current) {\n // Attack just started - initialize movement\n attackStartTimeRef.current = performance.now() / 1000;\n\n // Calculate attack movement if we have animation type\n if (animationType && attackDirection) {\n attackMovementResultRef.current =\n physicsRef.current.calculateAttackMovement({\n animationType,\n currentStance,\n direction: attackDirection.clone().normalize(),\n animationDuration,\n });\n }\n } else if (!isAttacking && wasAttackingRef.current) {\n // Attack ended - clean up\n attackStartTimeRef.current = null;\n attackMovementResultRef.current = null;\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(0);\n }\n\n wasAttackingRef.current = isAttacking;\n\n // Only start animation loop if attacking\n if (!isAttacking || !attackStartTimeRef.current || !attackMovementResultRef.current) {\n return;\n }\n\n const result = attackMovementResultRef.current;\n let animationFrameId: number;\n\n const updatePosition = () => {\n if (attackStartTimeRef.current === null) return;\n\n const currentTime = performance.now() / 1000;\n const elapsedTime = currentTime - attackStartTimeRef.current;\n\n // Determine phase\n const lunging = physicsRef.current.isInLungePhase(\n elapsedTime,\n result.lungeDuration\n );\n const recovering = physicsRef.current.isInRecoveryPhase(\n elapsedTime,\n result\n );\n\n setIsLunging(lunging);\n setIsRecovering(recovering);\n\n // Calculate progress\n const totalProgress = Math.min(1.0, elapsedTime / result.totalDuration);\n setProgress(totalProgress);\n\n if (lunging || recovering) {\n // Apply attack movement physics\n const basePos = new THREE.Vector3(...basePosition);\n const newPosition = physicsRef.current.applyAttackMovement(\n basePos,\n result,\n elapsedTime,\n recovering\n );\n\n setCurrentPosition([newPosition.x, newPosition.y, newPosition.z]);\n\n // Continue animation\n animationFrameId = requestAnimationFrame(updatePosition);\n } else {\n // Attack movement complete - return to base\n setCurrentPosition(basePosition);\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(1.0);\n }\n };\n\n // Start animation loop\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n }, [\n isAttacking,\n animationType,\n currentStance,\n attackDirection,\n animationDuration,\n basePosition,\n ]);\n\n return {\n currentPosition,\n isLunging,\n isRecovering,\n progress,\n };\n}\n\nexport default useAttackMovement;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAgB,kBACd,QACyB;CACzB,MAAM,EACJ,aACA,eACA,eACA,cACA,iBACA,oBAAoB,OAClB;CAGJ,MAAM,aAAa,OAAO,IAAI,uBAAuB,CAAC;CAGtD,MAAM,qBAAqB,OAAsB,KAAK;CACtD,MAAM,0BAA0B,OAEtB,KAAK;CAGf,MAAM,CAAC,iBAAiB,sBAAsB,SAE5C,aAAa;CACf,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CACjD,MAAM,CAAC,cAAc,mBAAmB,SAAS,MAAM;CACvD,MAAM,CAAC,UAAU,eAAe,SAAS,EAAE;CAG3C,MAAM,kBAAkB,OAAO,MAAM;
|
|
1
|
+
{"version":3,"file":"useAttackMovement.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useAttackMovement.ts"],"sourcesContent":["/**\n * useAttackMovement Hook - Track Attack Movement for 3D Characters\n *\n * Custom hook for managing attack movement physics during animations.\n * Tracks lunge and recovery phases with smooth easing curves.\n *\n * @korean 공격이동훅 - 공격 이동 추적\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { AnimationType } from \"@/systems/animation\";\nimport { AttackMovementPhysics } from \"@/systems/physics\";\nimport { TrigramStance } from \"@/types/common\";\n\n/**\n * Configuration for useAttackMovement hook\n */\nexport interface UseAttackMovementConfig {\n /** Whether character is currently attacking */\n readonly isAttacking: boolean;\n /** Animation type for current attack */\n readonly animationType?: AnimationType;\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Base position of character (when not attacking) */\n readonly basePosition: [number, number, number];\n /** Direction vector for attack (normalized) */\n readonly attackDirection?: THREE.Vector3;\n /** Animation duration in seconds (default: 0.4) */\n readonly animationDuration?: number;\n}\n\n/**\n * Return value from useAttackMovement hook\n */\nexport interface UseAttackMovementResult {\n /** Current 3D position including attack movement */\n readonly currentPosition: [number, number, number];\n /** Whether in forward lunge phase */\n readonly isLunging: boolean;\n /** Whether in recovery return phase */\n readonly isRecovering: boolean;\n /** Progress through attack movement (0-1) */\n readonly progress: number;\n}\n\n/**\n * useAttackMovement hook\n *\n * Tracks attack movement physics and returns current position.\n * Automatically handles lunge and recovery phases with smooth easing.\n *\n * @param config - Attack movement configuration\n * @returns Current position and movement state\n *\n * @example\n * ```typescript\n * const { currentPosition, isLunging } = useAttackMovement({\n * isAttacking: playerIsAttacking,\n * animationType: AnimationType.ROUNDHOUSE_KICK,\n * currentStance: TrigramStance.LI,\n * basePosition: [0, 0, 0],\n * attackDirection: new THREE.Vector3(1, 0, 0),\n * });\n *\n * // Use currentPosition for rendering\n * <CharacterModel position={currentPosition} />\n * ```\n *\n * @korean 공격이동사용\n */\nexport function useAttackMovement(\n config: UseAttackMovementConfig\n): UseAttackMovementResult {\n const {\n isAttacking,\n animationType,\n currentStance,\n basePosition,\n attackDirection,\n animationDuration = 0.4,\n } = config;\n\n // Attack movement physics engine\n const physicsRef = useRef(new AttackMovementPhysics());\n\n // Attack timing\n const attackStartTimeRef = useRef<number | null>(null);\n const attackMovementResultRef = useRef<ReturnType<\n typeof physicsRef.current.calculateAttackMovement\n > | null>(null);\n\n // Current position state\n const [currentPosition, setCurrentPosition] = useState<\n [number, number, number]\n >(basePosition);\n const [isLunging, setIsLunging] = useState(false);\n const [isRecovering, setIsRecovering] = useState(false);\n const [progress, setProgress] = useState(0);\n\n // Track previous attack state\n const wasAttackingRef = useRef(false);\n\n // Keep position synced with basePosition when not attacking\n useEffect(() => {\n if (!isAttacking) {\n setCurrentPosition(basePosition);\n }\n }, [isAttacking, basePosition]);\n\n // Initialize attack movement and manage animation loop\n useEffect(() => {\n if (isAttacking && !wasAttackingRef.current) {\n // Attack just started - initialize movement\n attackStartTimeRef.current = performance.now() / 1000;\n\n // Calculate attack movement if we have animation type\n if (animationType && attackDirection) {\n attackMovementResultRef.current =\n physicsRef.current.calculateAttackMovement({\n animationType,\n currentStance,\n direction: attackDirection.clone().normalize(),\n animationDuration,\n });\n }\n } else if (!isAttacking && wasAttackingRef.current) {\n // Attack ended - clean up\n attackStartTimeRef.current = null;\n attackMovementResultRef.current = null;\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(0);\n }\n\n wasAttackingRef.current = isAttacking;\n\n // Only start animation loop if attacking\n if (!isAttacking || !attackStartTimeRef.current || !attackMovementResultRef.current) {\n return;\n }\n\n const result = attackMovementResultRef.current;\n let animationFrameId: number;\n\n const updatePosition = () => {\n if (attackStartTimeRef.current === null) return;\n\n const currentTime = performance.now() / 1000;\n const elapsedTime = currentTime - attackStartTimeRef.current;\n\n // Determine phase\n const lunging = physicsRef.current.isInLungePhase(\n elapsedTime,\n result.lungeDuration\n );\n const recovering = physicsRef.current.isInRecoveryPhase(\n elapsedTime,\n result\n );\n\n setIsLunging(lunging);\n setIsRecovering(recovering);\n\n // Calculate progress\n const totalProgress = Math.min(1.0, elapsedTime / result.totalDuration);\n setProgress(totalProgress);\n\n if (lunging || recovering) {\n // Apply attack movement physics\n const basePos = new THREE.Vector3(...basePosition);\n const newPosition = physicsRef.current.applyAttackMovement(\n basePos,\n result,\n elapsedTime,\n recovering\n );\n\n setCurrentPosition([newPosition.x, newPosition.y, newPosition.z]);\n\n // Continue animation\n animationFrameId = requestAnimationFrame(updatePosition);\n } else {\n // Attack movement complete - return to base\n setCurrentPosition(basePosition);\n setIsLunging(false);\n setIsRecovering(false);\n setProgress(1.0);\n }\n };\n\n // Start animation loop\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n }, [\n isAttacking,\n animationType,\n currentStance,\n attackDirection,\n animationDuration,\n basePosition,\n ]);\n\n return {\n currentPosition,\n isLunging,\n isRecovering,\n progress,\n };\n}\n\nexport default useAttackMovement;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwEA,SAAgB,kBACd,QACyB;CACzB,MAAM,EACJ,aACA,eACA,eACA,cACA,iBACA,oBAAoB,OAClB;CAGJ,MAAM,aAAa,OAAO,IAAI,uBAAuB,CAAC;CAGtD,MAAM,qBAAqB,OAAsB,KAAK;CACtD,MAAM,0BAA0B,OAEtB,KAAK;CAGf,MAAM,CAAC,iBAAiB,sBAAsB,SAE5C,aAAa;CACf,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CACjD,MAAM,CAAC,cAAc,mBAAmB,SAAS,MAAM;CACvD,MAAM,CAAC,UAAU,eAAe,SAAS,EAAE;CAG3C,MAAM,kBAAkB,OAAO,MAAM;CAGrC,gBAAgB;EACd,IAAI,CAAC,aACH,mBAAmB,aAAa;IAEjC,CAAC,aAAa,aAAa,CAAC;CAG/B,gBAAgB;EACd,IAAI,eAAe,CAAC,gBAAgB,SAAS;GAE3C,mBAAmB,UAAU,YAAY,KAAK,GAAG;GAGjD,IAAI,iBAAiB,iBACnB,wBAAwB,UACtB,WAAW,QAAQ,wBAAwB;IACzC;IACA;IACA,WAAW,gBAAgB,OAAO,CAAC,WAAW;IAC9C;IACD,CAAC;SAED,IAAI,CAAC,eAAe,gBAAgB,SAAS;GAElD,mBAAmB,UAAU;GAC7B,wBAAwB,UAAU;GAClC,aAAa,MAAM;GACnB,gBAAgB,MAAM;GACtB,YAAY,EAAE;;EAGhB,gBAAgB,UAAU;EAG1B,IAAI,CAAC,eAAe,CAAC,mBAAmB,WAAW,CAAC,wBAAwB,SAC1E;EAGF,MAAM,SAAS,wBAAwB;EACvC,IAAI;EAEJ,MAAM,uBAAuB;GAC3B,IAAI,mBAAmB,YAAY,MAAM;GAGzC,MAAM,cADc,YAAY,KAAK,GAAG,MACN,mBAAmB;GAGrD,MAAM,UAAU,WAAW,QAAQ,eACjC,aACA,OAAO,cACR;GACD,MAAM,aAAa,WAAW,QAAQ,kBACpC,aACA,OACD;GAED,aAAa,QAAQ;GACrB,gBAAgB,WAAW;GAI3B,YADsB,KAAK,IAAI,GAAK,cAAc,OAAO,cAC7C,CAAc;GAE1B,IAAI,WAAW,YAAY;IAEzB,MAAM,UAAU,IAAI,MAAM,QAAQ,GAAG,aAAa;IAClD,MAAM,cAAc,WAAW,QAAQ,oBACrC,SACA,QACA,aACA,WACD;IAED,mBAAmB;KAAC,YAAY;KAAG,YAAY;KAAG,YAAY;KAAE,CAAC;IAGjE,mBAAmB,sBAAsB,eAAe;UACnD;IAEL,mBAAmB,aAAa;IAChC,aAAa,MAAM;IACnB,gBAAgB,MAAM;IACtB,YAAY,EAAI;;;EAKpB,mBAAmB,sBAAsB,eAAe;EAExD,aAAa;GACX,IAAI,kBACF,qBAAqB,iBAAiB;;IAGzC;EACD;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;CAEF,OAAO;EACL;EACA;EACA;EACA;EACD"}
|