blacktrigram 0.7.47 → 0.7.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioAssetLoader.js.map +1 -1
- package/lib/audio/AudioAssetRegistry.js.map +1 -1
- package/lib/audio/AudioCache.js.map +1 -1
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioMonitor.js.map +1 -1
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/AudioProvider.js.map +1 -1
- package/lib/audio/AudioUtils.js.map +1 -1
- package/lib/audio/BoneImpactAudioMap.js.map +1 -1
- package/lib/audio/VariantSelector.js.map +1 -1
- package/lib/audio/types.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.d.ts.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +29 -25
- 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.d.ts.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js +2 -2
- 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 +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.d.ts.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js +2 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.d.ts.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.js +4 -2
- package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
- package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.d.ts.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js +11 -5
- 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 +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 +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.d.ts.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +3 -11
- 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.d.ts.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js +2 -2
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.d.ts +1 -0
- package/lib/components/screens/training/hooks/useTrainingActions.d.ts.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +6 -4
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.d.ts.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js +11 -5
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
- package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.d.ts.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js +7 -5
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/constants/bodyDimensions.js.map +1 -1
- package/lib/constants/bodyRenderingConstants.js.map +1 -1
- package/lib/data/archetypeClothing.js.map +1 -1
- package/lib/data/archetypePhysicalAttributes.js.map +1 -1
- package/lib/data/techniqueMappings.js.map +1 -1
- package/lib/data/techniques.js.map +1 -1
- package/lib/hooks/useActionFeedback.js.map +1 -1
- package/lib/hooks/useBalanceAnimations.js.map +1 -1
- package/lib/hooks/useCombatTimer.js.map +1 -1
- package/lib/hooks/useDebounce.js.map +1 -1
- package/lib/hooks/useHUDLayout.d.ts.map +1 -1
- package/lib/hooks/useHUDLayout.js +3 -2
- package/lib/hooks/useHUDLayout.js.map +1 -1
- package/lib/hooks/useHandPoseTransitions.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useMatchCountdown.js.map +1 -1
- package/lib/hooks/useMuscleActivation.js.map +1 -1
- package/lib/hooks/usePauseMenu.js.map +1 -1
- package/lib/hooks/usePlayerAnimation.js.map +1 -1
- package/lib/hooks/useResponsiveLayout.js.map +1 -1
- package/lib/hooks/useRoundTransition.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.d.ts.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +1 -1
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTechniqueSelection.js.map +1 -1
- package/lib/hooks/useThrottle.js.map +1 -1
- package/lib/hooks/useTouchControls.js.map +1 -1
- package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
- package/lib/hooks/useWindowSize.js.map +1 -1
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/EffectCalculator.js.map +1 -1
- package/lib/systems/LayoutSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ResponsiveScaling.js.map +1 -1
- package/lib/systems/TrigramSystem.js.map +1 -1
- package/lib/systems/VitalPointSystem.js.map +1 -1
- package/lib/systems/ai/AIPersonality.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js +16 -16
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
- package/lib/systems/ai/ComboSystem.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/ai/TrainingAI.js.map +1 -1
- package/lib/systems/ai/types.js.map +1 -1
- package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/HandPoses.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.d.ts +6 -0
- package/lib/systems/animation/builders/KickPhaseApplicator.d.ts.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.js +16 -9
- package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/KoreanGuardPositions.d.ts +4 -4
- package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js +5 -5
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.d.ts +112 -71
- package/lib/systems/animation/builders/MartialArtsConstants.d.ts.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.js +113 -72
- package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
- package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
- package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
- package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/EnhancedAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/EnhancedElbowKneeAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamStanceAnimations.js +21 -0
- package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -0
- package/lib/systems/animation/catalogs/GamTechniqueAnimations.js +34 -2
- package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts +9 -0
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts.map +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +288 -0
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -0
- package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/KickAnimations.d.ts +2 -2
- package/lib/systems/animation/catalogs/KickAnimations.js +2 -2
- package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js +14 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.d.ts +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceGuardPoses.d.ts +6 -6
- package/lib/systems/animation/catalogs/StanceGuardPoses.js +36 -36
- package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
- package/lib/systems/animation/constants/AnatomicalLimits.js.map +1 -1
- package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
- package/lib/systems/animation/core/AnimationPriority.js +15 -15
- package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.d.ts +30 -0
- package/lib/systems/animation/core/AnimationRegistry.d.ts.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.js +74 -12
- package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js +16 -16
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.d.ts.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.js +34 -0
- package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
- package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
- package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
- package/lib/systems/animation/core/index.d.ts +1 -1
- package/lib/systems/animation/core/index.d.ts.map +1 -1
- package/lib/systems/animation/core/types.d.ts +24 -0
- package/lib/systems/animation/core/types.d.ts.map +1 -1
- package/lib/systems/animation/core/types.js +27 -11
- package/lib/systems/animation/core/types.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
- package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
- package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
- package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
- package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
- package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/bodypart/types.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js +19 -19
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/breathing/feedback.js.map +1 -1
- package/lib/systems/breathing/integration.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js +19 -19
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js +17 -17
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js +24 -24
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/FallIntegration.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js +21 -21
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
- package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
- package/lib/systems/combat/typeGuards.js.map +1 -1
- package/lib/systems/effects.js.map +1 -1
- package/lib/systems/game.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
- package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
- package/lib/systems/movement/integration.js.map +1 -1
- package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
- package/lib/systems/physics/CollisionDetection.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
- package/lib/systems/physics/SpeedModifierSystem.js +6 -6
- package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
- package/lib/systems/trigram/KoreanCulture.js.map +1 -1
- package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
- package/lib/systems/trigram/StanceManager.js.map +1 -1
- package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
- package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
- package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GeonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/JinTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/LiTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TaeTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
- package/lib/systems/trigram/techniques/index.js.map +1 -1
- package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
- package/lib/systems/trigram/types.js.map +1 -1
- package/lib/systems/types.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
- package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
- package/lib/systems/vitalpoint/VitalPointsData.js.map +1 -1
- package/lib/types/AccessibilityTypes.js.map +1 -1
- package/lib/types/LayoutTypes.js.map +1 -1
- package/lib/types/PhysicsTypes.js.map +1 -1
- package/lib/types/common.js.map +1 -1
- package/lib/types/constants/animations.js.map +1 -1
- package/lib/types/constants/colors.js.map +1 -1
- package/lib/types/constants/designSystem.js.map +1 -1
- package/lib/types/constants/index.js.map +1 -1
- package/lib/types/constants/layout.d.ts +21 -0
- package/lib/types/constants/layout.d.ts.map +1 -1
- package/lib/types/constants/layout.js +22 -1
- package/lib/types/constants/layout.js.map +1 -1
- package/lib/types/constants/performance.js.map +1 -1
- package/lib/types/constants/typography.js.map +1 -1
- package/lib/types/constants/ui.js.map +1 -1
- package/lib/types/facial.js +19 -19
- package/lib/types/facial.js.map +1 -1
- package/lib/types/hand-animation.js.map +1 -1
- package/lib/types/injury.js.map +1 -1
- package/lib/types/muscle.js.map +1 -1
- package/lib/types/physics.js.map +1 -1
- package/lib/types/physicsConstants.js.map +1 -1
- package/lib/types/player-visual.d.ts +1 -1
- package/lib/types/player-visual.d.ts.map +1 -1
- package/lib/types/skeletal.js.map +1 -1
- package/lib/types/techniqueId.js.map +1 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/arenaWorldDimensions.js.map +1 -1
- package/lib/utils/assetConfig.js.map +1 -1
- package/lib/utils/characterScaling.js.map +1 -1
- package/lib/utils/colorHelpers.js.map +1 -1
- package/lib/utils/colorUtils.js.map +1 -1
- package/lib/utils/combatReadiness.js.map +1 -1
- package/lib/utils/controlMapping.js.map +1 -1
- package/lib/utils/deviceDetection.js +6 -7
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/fabricTextures.js.map +1 -1
- package/lib/utils/hapticFeedback.js.map +1 -1
- package/lib/utils/haptics.js.map +1 -1
- package/lib/utils/htmlOverlayHelpers.js.map +1 -1
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/math.js.map +1 -1
- package/lib/utils/mobileLayoutHelpers.js.map +1 -1
- package/lib/utils/mobileUIUtils.js.map +1 -1
- package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
- package/lib/utils/performanceOptimization.js.map +1 -1
- package/lib/utils/player3DHelpers.js.map +1 -1
- package/lib/utils/playerUtils.js.map +1 -1
- package/lib/utils/responsiveLayout.js.map +1 -1
- package/lib/utils/responsiveLayoutHelpers.d.ts +7 -0
- package/lib/utils/responsiveLayoutHelpers.d.ts.map +1 -1
- package/lib/utils/responsiveLayoutHelpers.js +16 -2
- 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 +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StanceTransitionEffect.js","names":[],"sources":["../../../../../src/components/shared/three/effects/StanceTransitionEffect.tsx"],"sourcesContent":["/**\n * StanceTransitionEffect - Smooth visual transition between trigram stances\n *\n * Manages the visual transition when a player changes stance, providing:\n * - 0.5s smooth color fade between old and new stance colors\n * - Expanding energy ring effect\n * - Bilingual stance name display (Korean + English) for 1s\n * - Audio synchronization for stance change SFX\n *\n * @module components/three/StanceTransitionEffect\n * @category 3D Components\n * @korean 자세전환효과\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { FONT_FAMILY } from \"../../../../types/constants\";\nimport { colorUtils } from \"../../../../types/constants/colors\";\nimport {\n getStanceColor,\n getStanceNames,\n} from \"../../../../utils/stanceHelpers\";\n\n/**\n * Props for StanceTransitionEffect component\n */\nexport interface StanceTransitionEffectProps {\n /** Previous stance (for color interpolation) */\n readonly fromStance: TrigramStance | null;\n /** New stance being transitioned to */\n readonly toStance: TrigramStance;\n /** Callback when transition completes */\n readonly onTransitionComplete?: () => void;\n /** Transition duration in seconds (default: 0.5) */\n readonly duration?: number;\n /** Show stance name overlay (default: true) */\n readonly showNameOverlay?: boolean;\n}\n\n/**\n * StanceTransitionEffect Component\n *\n * Provides smooth visual feedback during stance changes:\n * 1. Expanding energy ring effect from player center\n * 2. Color interpolation from old to new stance\n * 3. Bilingual stance name overlay (1 second display)\n *\n * Performance optimized:\n * - Single animation frame callback\n * - Auto-cleanup after transition completes\n * - Reuses Three.js materials and geometries\n *\n * @example\n * ```tsx\n * <StanceTransitionEffect\n * fromStance={TrigramStance.GEON}\n * toStance={TrigramStance.TAE}\n * onTransitionComplete={() => console.log('Transition done')}\n * duration={0.5}\n * />\n * ```\n */\nexport const StanceTransitionEffect: React.FC<StanceTransitionEffectProps> = ({\n fromStance,\n toStance,\n onTransitionComplete,\n duration = 0.5,\n showNameOverlay = true,\n}) => {\n const ringRef = useRef<THREE.Mesh>(null);\n const startTimeRef = useRef<number>(0);\n const isInitializedRef = useRef(false);\n const [isTransitioning, setIsTransitioning] = useState(true);\n const [showName, setShowName] = useState(showNameOverlay);\n\n const fromColor = useMemo(\n () => (fromStance ? getStanceColor(fromStance) : getStanceColor(toStance)),\n [fromStance, toStance]\n );\n const toColor = useMemo(() => getStanceColor(toStance), [toStance]);\n const stanceNames = useMemo(() => getStanceNames(toStance), [toStance]);\n\n\n useEffect(() => {\n isInitializedRef.current = false;\n startTimeRef.current = 0;\n setIsTransitioning(true);\n setShowName(showNameOverlay);\n\n if (showNameOverlay) {\n const nameTimer = setTimeout(() => {\n setShowName(false);\n }, 1000);\n\n return () => clearTimeout(nameTimer);\n }\n }, [toStance, showNameOverlay]);\n\n useFrame((state) => {\n if (!isTransitioning || !ringRef.current) return;\n\n if (!isInitializedRef.current) {\n startTimeRef.current = state.clock.elapsedTime;\n isInitializedRef.current = true;\n }\n\n const elapsed = state.clock.elapsedTime - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1.0);\n\n const currentColor = colorUtils.blend(fromColor, toColor, progress);\n (ringRef.current.material as THREE.MeshBasicMaterial).color.setHex(\n currentColor\n );\n\n const scale = 0.5 + progress * 2.5; // From 0.5 to 3.0\n ringRef.current.scale.setScalar(scale);\n\n const opacity = 1.0 - progress * 0.7; // From 1.0 to 0.3\n (ringRef.current.material as THREE.MeshBasicMaterial).opacity = opacity;\n\n if (progress >= 1.0) {\n setIsTransitioning(false);\n onTransitionComplete?.();\n }\n });\n\n const toColorHex = `#${toColor.toString(16).padStart(6, \"0\")}`;\n\n return (\n <group name=\"stance-transition-effect\">\n {/* Expanding energy ring */}\n <mesh\n ref={ringRef}\n position={[0, 0.05, 0]}\n rotation={[-Math.PI / 2, 0, 0]}\n name=\"transition-ring\"\n >\n <ringGeometry args={[0.8, 1.0, 32]} />\n <meshBasicMaterial\n color={fromColor}\n transparent\n opacity={1.0}\n side={THREE.DoubleSide}\n depthWrite={false}\n blending={THREE.AdditiveBlending}\n />\n </mesh>\n\n {/* Stance name overlay (Korean + English) */}\n {showName && (\n <Html\n position={[0, 2.0, 0]}\n center\n distanceFactor={10}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n data-testid=\"stance-name-overlay\"\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n padding: \"8px 16px\",\n backgroundColor: \"rgba(0, 0, 0, 0.7)\",\n borderRadius: \"8px\",\n border: `2px solid ${toColorHex}`,\n boxShadow: `0 0 20px ${toColorHex}`,\n animation: \"fadeInOut 1s ease-in-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: \"24px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"bold\",\n textShadow: `0 0 10px ${toColorHex}`,\n }}\n >\n {stanceNames.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: \"14px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"normal\",\n opacity: 0.8,\n }}\n >\n {stanceNames.english}\n </div>\n </div>\n\n {/* CSS animation */}\n <style>\n {`\n @keyframes fadeInOut {\n 0% { opacity: 0; transform: translateY(10px); }\n 20% { opacity: 1; transform: translateY(0); }\n 80% { opacity: 1; transform: translateY(0); }\n 100% { opacity: 0; transform: translateY(-10px); }\n }\n `}\n </style>\n </Html>\n )}\n </group>\n );\n};\n\nexport default StanceTransitionEffect;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA,IAAa,0BAAiE,EAC5E,YACA,UACA,sBACA,WAAW,IACX,kBAAkB,WACd;CACJ,MAAM,UAAU,OAAmB,
|
|
1
|
+
{"version":3,"file":"StanceTransitionEffect.js","names":[],"sources":["../../../../../src/components/shared/three/effects/StanceTransitionEffect.tsx"],"sourcesContent":["/**\n * StanceTransitionEffect - Smooth visual transition between trigram stances\n *\n * Manages the visual transition when a player changes stance, providing:\n * - 0.5s smooth color fade between old and new stance colors\n * - Expanding energy ring effect\n * - Bilingual stance name display (Korean + English) for 1s\n * - Audio synchronization for stance change SFX\n *\n * @module components/three/StanceTransitionEffect\n * @category 3D Components\n * @korean 자세전환효과\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { FONT_FAMILY } from \"../../../../types/constants\";\nimport { colorUtils } from \"../../../../types/constants/colors\";\nimport {\n getStanceColor,\n getStanceNames,\n} from \"../../../../utils/stanceHelpers\";\n\n/**\n * Props for StanceTransitionEffect component\n */\nexport interface StanceTransitionEffectProps {\n /** Previous stance (for color interpolation) */\n readonly fromStance: TrigramStance | null;\n /** New stance being transitioned to */\n readonly toStance: TrigramStance;\n /** Callback when transition completes */\n readonly onTransitionComplete?: () => void;\n /** Transition duration in seconds (default: 0.5) */\n readonly duration?: number;\n /** Show stance name overlay (default: true) */\n readonly showNameOverlay?: boolean;\n}\n\n/**\n * StanceTransitionEffect Component\n *\n * Provides smooth visual feedback during stance changes:\n * 1. Expanding energy ring effect from player center\n * 2. Color interpolation from old to new stance\n * 3. Bilingual stance name overlay (1 second display)\n *\n * Performance optimized:\n * - Single animation frame callback\n * - Auto-cleanup after transition completes\n * - Reuses Three.js materials and geometries\n *\n * @example\n * ```tsx\n * <StanceTransitionEffect\n * fromStance={TrigramStance.GEON}\n * toStance={TrigramStance.TAE}\n * onTransitionComplete={() => console.log('Transition done')}\n * duration={0.5}\n * />\n * ```\n */\nexport const StanceTransitionEffect: React.FC<StanceTransitionEffectProps> = ({\n fromStance,\n toStance,\n onTransitionComplete,\n duration = 0.5,\n showNameOverlay = true,\n}) => {\n const ringRef = useRef<THREE.Mesh>(null);\n const startTimeRef = useRef<number>(0);\n const isInitializedRef = useRef(false);\n const [isTransitioning, setIsTransitioning] = useState(true);\n const [showName, setShowName] = useState(showNameOverlay);\n\n const fromColor = useMemo(\n () => (fromStance ? getStanceColor(fromStance) : getStanceColor(toStance)),\n [fromStance, toStance]\n );\n const toColor = useMemo(() => getStanceColor(toStance), [toStance]);\n const stanceNames = useMemo(() => getStanceNames(toStance), [toStance]);\n\n\n useEffect(() => {\n isInitializedRef.current = false;\n startTimeRef.current = 0;\n setIsTransitioning(true);\n setShowName(showNameOverlay);\n\n if (showNameOverlay) {\n const nameTimer = setTimeout(() => {\n setShowName(false);\n }, 1000);\n\n return () => clearTimeout(nameTimer);\n }\n }, [toStance, showNameOverlay]);\n\n useFrame((state) => {\n if (!isTransitioning || !ringRef.current) return;\n\n if (!isInitializedRef.current) {\n startTimeRef.current = state.clock.elapsedTime;\n isInitializedRef.current = true;\n }\n\n const elapsed = state.clock.elapsedTime - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1.0);\n\n const currentColor = colorUtils.blend(fromColor, toColor, progress);\n (ringRef.current.material as THREE.MeshBasicMaterial).color.setHex(\n currentColor\n );\n\n const scale = 0.5 + progress * 2.5; // From 0.5 to 3.0\n ringRef.current.scale.setScalar(scale);\n\n const opacity = 1.0 - progress * 0.7; // From 1.0 to 0.3\n (ringRef.current.material as THREE.MeshBasicMaterial).opacity = opacity;\n\n if (progress >= 1.0) {\n setIsTransitioning(false);\n onTransitionComplete?.();\n }\n });\n\n const toColorHex = `#${toColor.toString(16).padStart(6, \"0\")}`;\n\n return (\n <group name=\"stance-transition-effect\">\n {/* Expanding energy ring */}\n <mesh\n ref={ringRef}\n position={[0, 0.05, 0]}\n rotation={[-Math.PI / 2, 0, 0]}\n name=\"transition-ring\"\n >\n <ringGeometry args={[0.8, 1.0, 32]} />\n <meshBasicMaterial\n color={fromColor}\n transparent\n opacity={1.0}\n side={THREE.DoubleSide}\n depthWrite={false}\n blending={THREE.AdditiveBlending}\n />\n </mesh>\n\n {/* Stance name overlay (Korean + English) */}\n {showName && (\n <Html\n position={[0, 2.0, 0]}\n center\n distanceFactor={10}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n data-testid=\"stance-name-overlay\"\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n padding: \"8px 16px\",\n backgroundColor: \"rgba(0, 0, 0, 0.7)\",\n borderRadius: \"8px\",\n border: `2px solid ${toColorHex}`,\n boxShadow: `0 0 20px ${toColorHex}`,\n animation: \"fadeInOut 1s ease-in-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: \"24px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"bold\",\n textShadow: `0 0 10px ${toColorHex}`,\n }}\n >\n {stanceNames.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: \"14px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"normal\",\n opacity: 0.8,\n }}\n >\n {stanceNames.english}\n </div>\n </div>\n\n {/* CSS animation */}\n <style>\n {`\n @keyframes fadeInOut {\n 0% { opacity: 0; transform: translateY(10px); }\n 20% { opacity: 1; transform: translateY(0); }\n 80% { opacity: 1; transform: translateY(0); }\n 100% { opacity: 0; transform: translateY(-10px); }\n }\n `}\n </style>\n </Html>\n )}\n </group>\n );\n};\n\nexport default StanceTransitionEffect;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitalPointMarkers3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/VitalPointMarkers3D.tsx"],"sourcesContent":["/**\n * VitalPointMarkers3D - 3D vital point visualization\n *\n * Renders anatomical vital points (급소) in 3D space around character models\n * Provides Korean martial arts targeting system visualization\n * Note: Currently displays points from KOREAN_VITAL_POINTS data (expandable to 70 points)\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { Position, VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Body region filter options\n */\nexport type BodyRegionFilter = \"all\" | \"head\" | \"torso\" | \"arms\" | \"legs\";\n\n/**\n * Props for the VitalPointMarkers3D component.\n * Controls visibility and interaction with anatomical targeting points.\n */\nexport interface VitalPointMarkers3DProps {\n /** 3D world position of the character [x, y, z]. Defaults to [0, 0, 0] */\n readonly position?: [number, number, number];\n /** Whether markers are visible. Defaults to true */\n readonly visible?: boolean;\n /** Selected vital point ID for highlighting */\n readonly selectedPoint?: string | null;\n /** Callback when a vital point is clicked */\n readonly onPointClick?: (vitalPointId: string) => void;\n /** Callback when a vital point is hovered */\n readonly onPointHover?: (vitalPointId: string | null) => void;\n /** Filter points by severity level */\n readonly severityFilter?: VitalPointSeverity[];\n /** Filter points by body region */\n readonly regionFilter?: BodyRegionFilter;\n /** Search query to filter points by name */\n readonly searchQuery?: string;\n /** Whether to show point labels with Korean names */\n readonly showLabels?: boolean;\n /** Scale multiplier for marker size. Defaults to 1.0 */\n readonly scale?: number;\n /** Whether to enable pulsing animation. Defaults to true */\n readonly animated?: boolean;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.SECONDARY_MAGENTA;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.SECONDARY_YELLOW;\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.ACCENT_CYAN;\n default:\n return KOREAN_COLORS.PRIMARY_CYAN;\n }\n};\n\nconst CHARACTER_CENTER_X = 100; // Pixel center of character sprite\nconst CHARACTER_SPRITE_HEIGHT = 200; // Pixel height of character sprite\nconst CHARACTER_3D_HEIGHT = 2.8; // 3D model height in world units\nconst CHARACTER_3D_WIDTH = 0.8; // Approximate 3D model width\nconst SPRITE_WIDTH = 100; // Half-width of sprite (total ~200 pixels)\n\nconst LABEL_STYLES = {\n padding: \"4px 8px\",\n borderRadius: \"4px\",\n fontSize: \"10px\",\n subtextSize: \"8px\",\n subtextOpacity: 0.8,\n borderWidth: \"1px\",\n} as const;\n\n/**\n * Convert color number to RGBA hex string\n * @param color - Color as number (e.g., 0xFF0000)\n * @param alpha - Alpha channel as hex string (e.g., \"dd\" for semi-transparent)\n * @returns RGBA hex string (e.g., \"#ff0000dd\")\n */\nconst colorToRgbaHex = (color: number, alpha: string = \"ff\"): string => {\n return `#${color.toString(16).padStart(6, \"0\")}${alpha}`;\n};\n\n/**\n * Convert 2D sprite coordinates to 3D body position\n * Maps vital point pixel coordinates to 3D character model space\n *\n * Input: 2D pixel coordinates where:\n * - X: ~50-150 (100 = center), positive = right side of character\n * - Y: 0 = top of head, 200 = feet (y increases downward)\n *\n * Output: 3D world coordinates relative to character position where:\n * - X: horizontal offset from character center\n * - Y: height from ground (0 = feet, 2.8 = top)\n * - Z: depth offset (positive = in front of character)\n */\nconst convert2DTo3D = (\n pos2D: Position,\n basePosition: [number, number, number]\n): [number, number, number] => {\n const normalizedX = (pos2D.x - CHARACTER_CENTER_X) / SPRITE_WIDTH;\n const x3D = normalizedX * CHARACTER_3D_WIDTH;\n\n const normalizedY = 1 - pos2D.y / CHARACTER_SPRITE_HEIGHT;\n const y3D = normalizedY * CHARACTER_3D_HEIGHT;\n\n const z3D = 0.15;\n\n return [basePosition[0] + x3D, basePosition[1] + y3D, basePosition[2] + z3D];\n};\n\n/**\n * Individual Vital Point Marker Component\n */\ninterface VitalPointMarkerProps {\n readonly vitalPoint: VitalPoint;\n readonly position3D: [number, number, number];\n readonly selected: boolean;\n readonly hovered: boolean;\n readonly showLabel: boolean;\n readonly scale: number;\n readonly animated: boolean;\n readonly onHover: (hovered: boolean) => void;\n readonly onClick: () => void;\n}\n\nconst VitalPointMarker: React.FC<VitalPointMarkerProps> = ({\n vitalPoint,\n position3D,\n selected,\n hovered,\n showLabel,\n scale,\n animated,\n onHover,\n onClick,\n}) => {\n const sphereRef = useRef<THREE.Mesh>(null);\n const ringRef = useRef<THREE.Mesh>(null);\n\n useFrame((state) => {\n if (!sphereRef.current || !animated) return;\n\n const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.1 + 1;\n sphereRef.current.scale.setScalar(pulse * scale);\n\n if (ringRef.current && (selected || hovered)) {\n ringRef.current.rotation.z += 0.05;\n }\n });\n\n const color = useMemo(() => {\n if (selected) return KOREAN_COLORS.ACCENT_GOLD;\n if (hovered) return KOREAN_COLORS.PRIMARY_CYAN;\n return getSeverityColor(vitalPoint.severity);\n }, [selected, hovered, vitalPoint.severity]);\n\n const markerSize = useMemo(() => {\n const DEFAULT_MARKER_SIZE = 0.08;\n\n switch (vitalPoint.severity) {\n case VitalPointSeverity.LETHAL:\n case VitalPointSeverity.CRITICAL:\n return DEFAULT_MARKER_SIZE * 1.6 * scale; // 0.128\n case VitalPointSeverity.MAJOR:\n return DEFAULT_MARKER_SIZE * 1.3 * scale; // 0.104\n case VitalPointSeverity.MODERATE:\n return DEFAULT_MARKER_SIZE * 1.0 * scale; // 0.08\n case VitalPointSeverity.MINOR:\n return DEFAULT_MARKER_SIZE * 0.8 * scale; // 0.064\n default:\n return DEFAULT_MARKER_SIZE * scale;\n }\n }, [vitalPoint.severity, scale]);\n\n return (\n <group position={position3D}>\n {/* Main sphere marker */}\n <mesh\n ref={sphereRef}\n onPointerOver={() => onHover(true)}\n onPointerOut={() => onHover(false)}\n onClick={(e) => {\n e.stopPropagation();\n onClick();\n }}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <meshStandardMaterial\n color={color}\n emissive={color}\n emissiveIntensity={hovered || selected ? 0.8 : 0.4}\n transparent\n opacity={hovered || selected ? 1.0 : 0.85}\n />\n </mesh>\n\n {/* Outer ring for selected/hovered */}\n {(selected || hovered) && (\n <mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry args={[markerSize * 1.5, markerSize * 1.8, 32]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={0.5}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Label overlay */}\n {showLabel && (hovered || selected) && (\n <Html\n position={[0, markerSize * 2, 0]}\n center\n distanceFactor={5}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n background: colorToRgbaHex(color, \"dd\"),\n color: \"#ffffff\",\n padding: LABEL_STYLES.padding,\n borderRadius: LABEL_STYLES.borderRadius,\n fontSize: LABEL_STYLES.fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n textAlign: \"center\",\n border: `${LABEL_STYLES.borderWidth} solid ${colorToRgbaHex(\n color\n )}`,\n }}\n >\n <div>{vitalPoint.names.korean}</div>\n <div\n style={{\n fontSize: LABEL_STYLES.subtextSize,\n opacity: LABEL_STYLES.subtextOpacity,\n }}\n >\n {vitalPoint.names.english}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\n/**\n * VitalPointMarkers3D Component\n * Renders all vital points as 3D markers around a character\n */\nexport const VitalPointMarkers3D: React.FC<VitalPointMarkers3DProps> = ({\n position = [0, 0, 0],\n visible = true,\n selectedPoint = null,\n onPointClick,\n onPointHover,\n severityFilter,\n regionFilter = \"all\",\n searchQuery = \"\",\n showLabels = true,\n scale = 1.0,\n animated = true,\n}) => {\n const [hoveredPoint, setHoveredPoint] = useState<string | null>(null);\n\n const visiblePoints = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilter && severityFilter.length > 0) {\n points = points.filter((vp) => severityFilter.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points;\n }, [severityFilter, regionFilter, searchQuery]);\n\n const handlePointHover = (vitalPointId: string, hovered: boolean) => {\n const newHovered = hovered ? vitalPointId : null;\n setHoveredPoint(newHovered);\n onPointHover?.(newHovered);\n };\n\n const handlePointClick = (vitalPointId: string) => {\n onPointClick?.(vitalPointId);\n };\n\n if (!visible) return null;\n\n return (\n <group position={position}>\n {visiblePoints.map((vitalPoint) => {\n const position3D = convert2DTo3D(vitalPoint.position, [0, 0, 0]);\n\n return (\n <VitalPointMarker\n key={vitalPoint.id}\n vitalPoint={vitalPoint}\n position3D={position3D}\n selected={selectedPoint === vitalPoint.id}\n hovered={hoveredPoint === vitalPoint.id}\n showLabel={showLabels}\n scale={scale}\n animated={animated}\n onHover={(hovered) => handlePointHover(vitalPoint.id, hovered)}\n onClick={() => handlePointClick(vitalPoint.id)}\n />\n );\n })}\n </group>\n );\n};\n\nexport default VitalPointMarkers3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VitalPointMarkers3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/VitalPointMarkers3D.tsx"],"sourcesContent":["/**\n * VitalPointMarkers3D - 3D vital point visualization\n *\n * Renders anatomical vital points (급소) in 3D space around character models\n * Provides Korean martial arts targeting system visualization\n * Note: Currently displays points from KOREAN_VITAL_POINTS data (expandable to 70 points)\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { Position, VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Body region filter options\n */\nexport type BodyRegionFilter = \"all\" | \"head\" | \"torso\" | \"arms\" | \"legs\";\n\n/**\n * Props for the VitalPointMarkers3D component.\n * Controls visibility and interaction with anatomical targeting points.\n */\nexport interface VitalPointMarkers3DProps {\n /** 3D world position of the character [x, y, z]. Defaults to [0, 0, 0] */\n readonly position?: [number, number, number];\n /** Whether markers are visible. Defaults to true */\n readonly visible?: boolean;\n /** Selected vital point ID for highlighting */\n readonly selectedPoint?: string | null;\n /** Callback when a vital point is clicked */\n readonly onPointClick?: (vitalPointId: string) => void;\n /** Callback when a vital point is hovered */\n readonly onPointHover?: (vitalPointId: string | null) => void;\n /** Filter points by severity level */\n readonly severityFilter?: VitalPointSeverity[];\n /** Filter points by body region */\n readonly regionFilter?: BodyRegionFilter;\n /** Search query to filter points by name */\n readonly searchQuery?: string;\n /** Whether to show point labels with Korean names */\n readonly showLabels?: boolean;\n /** Scale multiplier for marker size. Defaults to 1.0 */\n readonly scale?: number;\n /** Whether to enable pulsing animation. Defaults to true */\n readonly animated?: boolean;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.SECONDARY_MAGENTA;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.SECONDARY_YELLOW;\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.ACCENT_CYAN;\n default:\n return KOREAN_COLORS.PRIMARY_CYAN;\n }\n};\n\nconst CHARACTER_CENTER_X = 100; // Pixel center of character sprite\nconst CHARACTER_SPRITE_HEIGHT = 200; // Pixel height of character sprite\nconst CHARACTER_3D_HEIGHT = 2.8; // 3D model height in world units\nconst CHARACTER_3D_WIDTH = 0.8; // Approximate 3D model width\nconst SPRITE_WIDTH = 100; // Half-width of sprite (total ~200 pixels)\n\nconst LABEL_STYLES = {\n padding: \"4px 8px\",\n borderRadius: \"4px\",\n fontSize: \"10px\",\n subtextSize: \"8px\",\n subtextOpacity: 0.8,\n borderWidth: \"1px\",\n} as const;\n\n/**\n * Convert color number to RGBA hex string\n * @param color - Color as number (e.g., 0xFF0000)\n * @param alpha - Alpha channel as hex string (e.g., \"dd\" for semi-transparent)\n * @returns RGBA hex string (e.g., \"#ff0000dd\")\n */\nconst colorToRgbaHex = (color: number, alpha: string = \"ff\"): string => {\n return `#${color.toString(16).padStart(6, \"0\")}${alpha}`;\n};\n\n/**\n * Convert 2D sprite coordinates to 3D body position\n * Maps vital point pixel coordinates to 3D character model space\n *\n * Input: 2D pixel coordinates where:\n * - X: ~50-150 (100 = center), positive = right side of character\n * - Y: 0 = top of head, 200 = feet (y increases downward)\n *\n * Output: 3D world coordinates relative to character position where:\n * - X: horizontal offset from character center\n * - Y: height from ground (0 = feet, 2.8 = top)\n * - Z: depth offset (positive = in front of character)\n */\nconst convert2DTo3D = (\n pos2D: Position,\n basePosition: [number, number, number]\n): [number, number, number] => {\n const normalizedX = (pos2D.x - CHARACTER_CENTER_X) / SPRITE_WIDTH;\n const x3D = normalizedX * CHARACTER_3D_WIDTH;\n\n const normalizedY = 1 - pos2D.y / CHARACTER_SPRITE_HEIGHT;\n const y3D = normalizedY * CHARACTER_3D_HEIGHT;\n\n const z3D = 0.15;\n\n return [basePosition[0] + x3D, basePosition[1] + y3D, basePosition[2] + z3D];\n};\n\n/**\n * Individual Vital Point Marker Component\n */\ninterface VitalPointMarkerProps {\n readonly vitalPoint: VitalPoint;\n readonly position3D: [number, number, number];\n readonly selected: boolean;\n readonly hovered: boolean;\n readonly showLabel: boolean;\n readonly scale: number;\n readonly animated: boolean;\n readonly onHover: (hovered: boolean) => void;\n readonly onClick: () => void;\n}\n\nconst VitalPointMarker: React.FC<VitalPointMarkerProps> = ({\n vitalPoint,\n position3D,\n selected,\n hovered,\n showLabel,\n scale,\n animated,\n onHover,\n onClick,\n}) => {\n const sphereRef = useRef<THREE.Mesh>(null);\n const ringRef = useRef<THREE.Mesh>(null);\n\n useFrame((state) => {\n if (!sphereRef.current || !animated) return;\n\n const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.1 + 1;\n sphereRef.current.scale.setScalar(pulse * scale);\n\n if (ringRef.current && (selected || hovered)) {\n ringRef.current.rotation.z += 0.05;\n }\n });\n\n const color = useMemo(() => {\n if (selected) return KOREAN_COLORS.ACCENT_GOLD;\n if (hovered) return KOREAN_COLORS.PRIMARY_CYAN;\n return getSeverityColor(vitalPoint.severity);\n }, [selected, hovered, vitalPoint.severity]);\n\n const markerSize = useMemo(() => {\n const DEFAULT_MARKER_SIZE = 0.08;\n\n switch (vitalPoint.severity) {\n case VitalPointSeverity.LETHAL:\n case VitalPointSeverity.CRITICAL:\n return DEFAULT_MARKER_SIZE * 1.6 * scale; // 0.128\n case VitalPointSeverity.MAJOR:\n return DEFAULT_MARKER_SIZE * 1.3 * scale; // 0.104\n case VitalPointSeverity.MODERATE:\n return DEFAULT_MARKER_SIZE * 1.0 * scale; // 0.08\n case VitalPointSeverity.MINOR:\n return DEFAULT_MARKER_SIZE * 0.8 * scale; // 0.064\n default:\n return DEFAULT_MARKER_SIZE * scale;\n }\n }, [vitalPoint.severity, scale]);\n\n return (\n <group position={position3D}>\n {/* Main sphere marker */}\n <mesh\n ref={sphereRef}\n onPointerOver={() => onHover(true)}\n onPointerOut={() => onHover(false)}\n onClick={(e) => {\n e.stopPropagation();\n onClick();\n }}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <meshStandardMaterial\n color={color}\n emissive={color}\n emissiveIntensity={hovered || selected ? 0.8 : 0.4}\n transparent\n opacity={hovered || selected ? 1.0 : 0.85}\n />\n </mesh>\n\n {/* Outer ring for selected/hovered */}\n {(selected || hovered) && (\n <mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry args={[markerSize * 1.5, markerSize * 1.8, 32]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={0.5}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Label overlay */}\n {showLabel && (hovered || selected) && (\n <Html\n position={[0, markerSize * 2, 0]}\n center\n distanceFactor={5}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n background: colorToRgbaHex(color, \"dd\"),\n color: \"#ffffff\",\n padding: LABEL_STYLES.padding,\n borderRadius: LABEL_STYLES.borderRadius,\n fontSize: LABEL_STYLES.fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n textAlign: \"center\",\n border: `${LABEL_STYLES.borderWidth} solid ${colorToRgbaHex(\n color\n )}`,\n }}\n >\n <div>{vitalPoint.names.korean}</div>\n <div\n style={{\n fontSize: LABEL_STYLES.subtextSize,\n opacity: LABEL_STYLES.subtextOpacity,\n }}\n >\n {vitalPoint.names.english}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\n/**\n * VitalPointMarkers3D Component\n * Renders all vital points as 3D markers around a character\n */\nexport const VitalPointMarkers3D: React.FC<VitalPointMarkers3DProps> = ({\n position = [0, 0, 0],\n visible = true,\n selectedPoint = null,\n onPointClick,\n onPointHover,\n severityFilter,\n regionFilter = \"all\",\n searchQuery = \"\",\n showLabels = true,\n scale = 1.0,\n animated = true,\n}) => {\n const [hoveredPoint, setHoveredPoint] = useState<string | null>(null);\n\n const visiblePoints = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilter && severityFilter.length > 0) {\n points = points.filter((vp) => severityFilter.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points;\n }, [severityFilter, regionFilter, searchQuery]);\n\n const handlePointHover = (vitalPointId: string, hovered: boolean) => {\n const newHovered = hovered ? vitalPointId : null;\n setHoveredPoint(newHovered);\n onPointHover?.(newHovered);\n };\n\n const handlePointClick = (vitalPointId: string) => {\n onPointClick?.(vitalPointId);\n };\n\n if (!visible) return null;\n\n return (\n <group position={position}>\n {visiblePoints.map((vitalPoint) => {\n const position3D = convert2DTo3D(vitalPoint.position, [0, 0, 0]);\n\n return (\n <VitalPointMarker\n key={vitalPoint.id}\n vitalPoint={vitalPoint}\n position3D={position3D}\n selected={selectedPoint === vitalPoint.id}\n hovered={hoveredPoint === vitalPoint.id}\n showLabel={showLabels}\n scale={scale}\n animated={animated}\n onHover={(hovered) => handlePointHover(vitalPoint.id, hovered)}\n onClick={() => handlePointClick(vitalPoint.id)}\n />\n );\n })}\n </group>\n );\n};\n\nexport default VitalPointMarkers3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ElementalColorSystem.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/ElementalColorSystem.ts"],"sourcesContent":["/**\n * Elemental Color System for Korean Trigram Stances\n * Based on 오행 (Wu Xing / Five Elements) philosophy\n * \n * Maps each of the eight trigram stances to one of the five elements,\n * providing color-coded visual feedback for combat and training.\n * \n * Note: This module provides the correct element mappings based on traditional\n * Bagua philosophy. The element property in TRIGRAM_DATA currently uses placeholder\n * \"metal\" values and should be updated to use this system.\n * \n * @module components/shared/three/indicators/ElementalColorSystem\n * @category Combat UI\n * @korean 오행색체계\n */\n\nimport { TrigramStance } from \"../../../../types/common\";\nimport { TRIGRAM_DATA } from \"../../../../systems/trigram/types\";\n\n/**\n * Five Elements (오행) color palette\n * \n * Based on traditional Korean/Chinese five element theory:\n * - Wood (木): Growth, expansion - Green tones\n * - Fire (火): Energy, passion - Red/Orange tones \n * - Earth (土): Stability, grounding - Yellow/Brown tones\n * - Metal (金): Precision, strength - White/Gold tones\n * - Water (水): Flow, adaptation - Blue/Cyan tones\n * \n * @korean 오행색상\n */\nexport const ELEMENT_COLORS = {\n /** Wood element - Growth and expansion (木) */\n wood: 0x228b22, // Forest green\n /** Fire element - Energy and passion (火) */\n fire: 0xff4444, // Bright red\n /** Earth element - Stability and grounding (土) */\n earth: 0xffd700, // Gold/Yellow\n /** Metal element - Precision and strength (金) */\n metal: 0xffffff, // White/Silver\n /** Water element - Flow and adaptation (水) */\n water: 0x00ffff, // Cyan\n} as const;\n\n/**\n * Element type from the five elements system\n * @korean 원소타입\n */\nexport type Element = keyof typeof ELEMENT_COLORS;\n\n/**\n * Mapping of trigram stances to five elements\n * \n * Based on traditional Bagua (八卦) associations:\n * - Geon (Heaven) ☰: Metal - Represents strength and clarity\n * - Tae (Lake) ☱: Metal - Represents joy and openness\n * - Li (Fire) ☲: Fire - Represents illumination and awareness\n * - Jin (Thunder) ☳: Wood - Represents movement and initiative\n * - Son (Wind) ☴: Wood - Represents gentleness and penetration\n * - Gam (Water) ☵: Water - Represents depth and danger\n * - Gan (Mountain) ☶: Earth - Represents stillness and rest\n * - Gon (Earth) ☷: Earth - Represents receptivity and nourishment\n * \n * @korean 팔괘원소대응\n */\nexport const TRIGRAM_TO_ELEMENT: Record<TrigramStance, Element> = {\n [TrigramStance.GEON]: \"metal\", // ☰ Heaven (건)\n [TrigramStance.TAE]: \"metal\", // ☱ Lake (태)\n [TrigramStance.LI]: \"fire\", // ☲ Fire (리)\n [TrigramStance.JIN]: \"wood\", // ☳ Thunder (진)\n [TrigramStance.SON]: \"wood\", // ☴ Wind (손)\n [TrigramStance.GAM]: \"water\", // ☵ Water (감)\n [TrigramStance.GAN]: \"earth\", // ☶ Mountain (간)\n [TrigramStance.GON]: \"earth\", // ☷ Earth (곤)\n};\n\n\n\n/**\n * Get the elemental color for a trigram stance\n * \n * Returns the hexadecimal color value associated with the stance's element.\n * \n * @param stance - The trigram stance to get the color for\n * @returns Hexadecimal color value (e.g., 0x00ffff)\n * \n * @example\n * ```typescript\n * const color = getTrigramElementColor(TrigramStance.GEON);\n * // Returns 0xffffff (white/metal)\n * ```\n * \n * @korean 팔괘원소색상얻기\n */\nexport function getTrigramElementColor(stance: TrigramStance): number {\n const element = TRIGRAM_TO_ELEMENT[stance];\n return ELEMENT_COLORS[element];\n}\n\n/**\n * Get the Unicode symbol for a trigram stance\n * \n * Returns the Unicode trigram symbol character (☰☱☲☳☴☵☶☷).\n * Uses the symbol from TRIGRAM_DATA to maintain single source of truth.\n * \n * @param stance - The trigram stance to get the symbol for\n * @returns Unicode trigram symbol\n * \n * @example\n * ```typescript\n * const symbol = getTrigramSymbol(TrigramStance.GEON);\n * // Returns \"☰\" (Heaven)\n * ```\n * \n * @korean 팔괘기호얻기\n */\nexport function getTrigramSymbol(stance: TrigramStance): string {\n return TRIGRAM_DATA[stance].symbol;\n}\n\n/**\n * Get the element associated with a trigram stance\n * \n * @param stance - The trigram stance to get the element for\n * @returns Element name (wood, fire, earth, metal, water)\n * \n * @example\n * ```typescript\n * const element = getTrigramElement(TrigramStance.LI);\n * // Returns \"fire\"\n * ```\n * \n * @korean 팔괘원소얻기\n */\nexport function getTrigramElement(stance: TrigramStance): Element {\n return TRIGRAM_TO_ELEMENT[stance];\n}\n\n/**\n * Get Korean name for a trigram stance\n * \n * Uses the Korean name from TRIGRAM_DATA to maintain single source of truth.\n * \n * @param stance - The trigram stance to get the name for\n * @returns Korean name (e.g., \"건\" for Heaven)\n * \n * @example\n * ```typescript\n * const koreanName = getTrigramKoreanName(TrigramStance.GEON);\n * // Returns \"건\"\n * ```\n * \n * @korean 팔괘한글명얻기\n */\nexport function getTrigramKoreanName(stance: TrigramStance): string {\n return TRIGRAM_DATA[stance].name.korean;\n}\n\n/**\n * Get English name for a trigram stance\n * \n * Uses the English name from TRIGRAM_DATA to maintain single source of truth.\n * \n * @param stance - The trigram stance to get the name for\n * @returns English name (e.g., \"Heaven\" for Geon)\n * \n * @example\n * ```typescript\n * const englishName = getTrigramEnglishName(TrigramStance.GEON);\n * // Returns \"Heaven\"\n * ```\n * \n * @korean 팔괘영문명얻기\n */\nexport function getTrigramEnglishName(stance: TrigramStance): string {\n return TRIGRAM_DATA[stance].name.english;\n}\n\n/**\n * Get all trigram information for a stance\n * \n * Returns a complete set of information about the trigram stance including\n * symbol, colors, names, and element association.\n * \n * @param stance - The trigram stance to get information for\n * @returns Complete trigram information object\n * \n * @example\n * ```typescript\n * const info = getTrigramInfo(TrigramStance.LI);\n * // Returns {\n * // stance: TrigramStance.LI,\n * // symbol: \"☲\",\n * // element: \"fire\",\n * // color: 0xff4444,\n * // koreanName: \"리\",\n * // englishName: \"Fire\"\n * // }\n * ```\n * \n * @korean 팔괘정보얻기\n */\nexport function getTrigramInfo(stance: TrigramStance): {\n readonly stance: TrigramStance;\n readonly symbol: string;\n readonly element: Element;\n readonly color: number;\n readonly koreanName: string;\n readonly englishName: string;\n} {\n return {\n stance,\n symbol: getTrigramSymbol(stance),\n element: getTrigramElement(stance),\n color: getTrigramElementColor(stance),\n koreanName: getTrigramKoreanName(stance),\n englishName: getTrigramEnglishName(stance),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,IAAa,iBAAiB;;CAE5B,MAAM;;CAEN,MAAM;;CAEN,OAAO;;CAEP,OAAO;;CAEP,OAAO;
|
|
1
|
+
{"version":3,"file":"ElementalColorSystem.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/ElementalColorSystem.ts"],"sourcesContent":["/**\n * Elemental Color System for Korean Trigram Stances\n * Based on 오행 (Wu Xing / Five Elements) philosophy\n * \n * Maps each of the eight trigram stances to one of the five elements,\n * providing color-coded visual feedback for combat and training.\n * \n * Note: This module provides the correct element mappings based on traditional\n * Bagua philosophy. The element property in TRIGRAM_DATA currently uses placeholder\n * \"metal\" values and should be updated to use this system.\n * \n * @module components/shared/three/indicators/ElementalColorSystem\n * @category Combat UI\n * @korean 오행색체계\n */\n\nimport { TrigramStance } from \"../../../../types/common\";\nimport { TRIGRAM_DATA } from \"../../../../systems/trigram/types\";\n\n/**\n * Five Elements (오행) color palette\n * \n * Based on traditional Korean/Chinese five element theory:\n * - Wood (木): Growth, expansion - Green tones\n * - Fire (火): Energy, passion - Red/Orange tones \n * - Earth (土): Stability, grounding - Yellow/Brown tones\n * - Metal (金): Precision, strength - White/Gold tones\n * - Water (水): Flow, adaptation - Blue/Cyan tones\n * \n * @korean 오행색상\n */\nexport const ELEMENT_COLORS = {\n /** Wood element - Growth and expansion (木) */\n wood: 0x228b22, // Forest green\n /** Fire element - Energy and passion (火) */\n fire: 0xff4444, // Bright red\n /** Earth element - Stability and grounding (土) */\n earth: 0xffd700, // Gold/Yellow\n /** Metal element - Precision and strength (金) */\n metal: 0xffffff, // White/Silver\n /** Water element - Flow and adaptation (水) */\n water: 0x00ffff, // Cyan\n} as const;\n\n/**\n * Element type from the five elements system\n * @korean 원소타입\n */\nexport type Element = keyof typeof ELEMENT_COLORS;\n\n/**\n * Mapping of trigram stances to five elements\n * \n * Based on traditional Bagua (八卦) associations:\n * - Geon (Heaven) ☰: Metal - Represents strength and clarity\n * - Tae (Lake) ☱: Metal - Represents joy and openness\n * - Li (Fire) ☲: Fire - Represents illumination and awareness\n * - Jin (Thunder) ☳: Wood - Represents movement and initiative\n * - Son (Wind) ☴: Wood - Represents gentleness and penetration\n * - Gam (Water) ☵: Water - Represents depth and danger\n * - Gan (Mountain) ☶: Earth - Represents stillness and rest\n * - Gon (Earth) ☷: Earth - Represents receptivity and nourishment\n * \n * @korean 팔괘원소대응\n */\nexport const TRIGRAM_TO_ELEMENT: Record<TrigramStance, Element> = {\n [TrigramStance.GEON]: \"metal\", // ☰ Heaven (건)\n [TrigramStance.TAE]: \"metal\", // ☱ Lake (태)\n [TrigramStance.LI]: \"fire\", // ☲ Fire (리)\n [TrigramStance.JIN]: \"wood\", // ☳ Thunder (진)\n [TrigramStance.SON]: \"wood\", // ☴ Wind (손)\n [TrigramStance.GAM]: \"water\", // ☵ Water (감)\n [TrigramStance.GAN]: \"earth\", // ☶ Mountain (간)\n [TrigramStance.GON]: \"earth\", // ☷ Earth (곤)\n};\n\n\n\n/**\n * Get the elemental color for a trigram stance\n * \n * Returns the hexadecimal color value associated with the stance's element.\n * \n * @param stance - The trigram stance to get the color for\n * @returns Hexadecimal color value (e.g., 0x00ffff)\n * \n * @example\n * ```typescript\n * const color = getTrigramElementColor(TrigramStance.GEON);\n * // Returns 0xffffff (white/metal)\n * ```\n * \n * @korean 팔괘원소색상얻기\n */\nexport function getTrigramElementColor(stance: TrigramStance): number {\n const element = TRIGRAM_TO_ELEMENT[stance];\n return ELEMENT_COLORS[element];\n}\n\n/**\n * Get the Unicode symbol for a trigram stance\n * \n * Returns the Unicode trigram symbol character (☰☱☲☳☴☵☶☷).\n * Uses the symbol from TRIGRAM_DATA to maintain single source of truth.\n * \n * @param stance - The trigram stance to get the symbol for\n * @returns Unicode trigram symbol\n * \n * @example\n * ```typescript\n * const symbol = getTrigramSymbol(TrigramStance.GEON);\n * // Returns \"☰\" (Heaven)\n * ```\n * \n * @korean 팔괘기호얻기\n */\nexport function getTrigramSymbol(stance: TrigramStance): string {\n return TRIGRAM_DATA[stance].symbol;\n}\n\n/**\n * Get the element associated with a trigram stance\n * \n * @param stance - The trigram stance to get the element for\n * @returns Element name (wood, fire, earth, metal, water)\n * \n * @example\n * ```typescript\n * const element = getTrigramElement(TrigramStance.LI);\n * // Returns \"fire\"\n * ```\n * \n * @korean 팔괘원소얻기\n */\nexport function getTrigramElement(stance: TrigramStance): Element {\n return TRIGRAM_TO_ELEMENT[stance];\n}\n\n/**\n * Get Korean name for a trigram stance\n * \n * Uses the Korean name from TRIGRAM_DATA to maintain single source of truth.\n * \n * @param stance - The trigram stance to get the name for\n * @returns Korean name (e.g., \"건\" for Heaven)\n * \n * @example\n * ```typescript\n * const koreanName = getTrigramKoreanName(TrigramStance.GEON);\n * // Returns \"건\"\n * ```\n * \n * @korean 팔괘한글명얻기\n */\nexport function getTrigramKoreanName(stance: TrigramStance): string {\n return TRIGRAM_DATA[stance].name.korean;\n}\n\n/**\n * Get English name for a trigram stance\n * \n * Uses the English name from TRIGRAM_DATA to maintain single source of truth.\n * \n * @param stance - The trigram stance to get the name for\n * @returns English name (e.g., \"Heaven\" for Geon)\n * \n * @example\n * ```typescript\n * const englishName = getTrigramEnglishName(TrigramStance.GEON);\n * // Returns \"Heaven\"\n * ```\n * \n * @korean 팔괘영문명얻기\n */\nexport function getTrigramEnglishName(stance: TrigramStance): string {\n return TRIGRAM_DATA[stance].name.english;\n}\n\n/**\n * Get all trigram information for a stance\n * \n * Returns a complete set of information about the trigram stance including\n * symbol, colors, names, and element association.\n * \n * @param stance - The trigram stance to get information for\n * @returns Complete trigram information object\n * \n * @example\n * ```typescript\n * const info = getTrigramInfo(TrigramStance.LI);\n * // Returns {\n * // stance: TrigramStance.LI,\n * // symbol: \"☲\",\n * // element: \"fire\",\n * // color: 0xff4444,\n * // koreanName: \"리\",\n * // englishName: \"Fire\"\n * // }\n * ```\n * \n * @korean 팔괘정보얻기\n */\nexport function getTrigramInfo(stance: TrigramStance): {\n readonly stance: TrigramStance;\n readonly symbol: string;\n readonly element: Element;\n readonly color: number;\n readonly koreanName: string;\n readonly englishName: string;\n} {\n return {\n stance,\n symbol: getTrigramSymbol(stance),\n element: getTrigramElement(stance),\n color: getTrigramElementColor(stance),\n koreanName: getTrigramKoreanName(stance),\n englishName: getTrigramEnglishName(stance),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,IAAa,iBAAiB;;CAE5B,MAAM;;CAEN,MAAM;;CAEN,OAAO;;CAEP,OAAO;;CAEP,OAAO;AACT;;;;;;;;;;;;;;;;AAuBA,IAAa,qBAAqD;EAC/D,cAAc,OAAO;EACrB,cAAc,MAAM;EACpB,cAAc,KAAK;EACnB,cAAc,MAAM;EACpB,cAAc,MAAM;EACpB,cAAc,MAAM;EACpB,cAAc,MAAM;EACpB,cAAc,MAAM;AACvB;;;;;;;;;;;;;;;;;AAoBA,SAAgB,uBAAuB,QAA+B;CAEpE,OAAO,eADS,mBAAmB;AAErC;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,iBAAiB,QAA+B;CAC9D,OAAO,aAAa,QAAQ;AAC9B;;;;;;;;;;;;;;;;;AAoCA,SAAgB,qBAAqB,QAA+B;CAClE,OAAO,aAAa,QAAQ,KAAK;AACnC;;;;;;;;;;;;;;;;;AAkBA,SAAgB,sBAAsB,QAA+B;CACnE,OAAO,aAAa,QAAQ,KAAK;AACnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GuardIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/GuardIndicator.tsx"],"sourcesContent":["/**\n * GuardIndicator - Visual indicator showing current fighting stance guard position\n * Displays Korean traditional stance name, English translation, and guard characteristics\n *\n * @module components/shared/three/indicators/GuardIndicator\n * @category Combat UI\n * @korean 방어자세표시기\n */\n\nimport React, { useMemo } from \"react\";\nimport { STANCE_GUARD_CONFIGS } from \"../../../../systems/animation\";\nimport { TRIGRAM_DATA } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Props for GuardIndicator component\n */\nexport interface GuardIndicatorProps {\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Whether player is in guard animation state */\n readonly isInGuard: boolean;\n /** Player position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n}\n\n/**\n * Get guard height label based on arm positions\n *\n * Guard heights based on shoulder.x (vertical angle in radians):\n * - High guard (고위): shoulder at chin level (x <= -0.8)\n * GEON (-1.0), LI (-1.6), GAN (-1.0)\n * - Mid guard (중위): shoulder at chest level (-0.8 < x < -0.4)\n * TAE (-0.7), JIN (-0.6), SON (-0.8), GAM (-0.8)\n * - Low guard (저위): shoulder low (x >= -0.4)\n * GON (-0.4)\n */\nfunction getGuardHeight(stance: TrigramStance): {\n korean: string;\n english: string;\n} {\n const config = STANCE_GUARD_CONFIGS[stance];\n const shoulderY = config.guardPose.leftArm.shoulder.x; // x is vertical in Euler\n\n if (shoulderY <= -0.8) {\n return { korean: \"고위\", english: \"High\" };\n } else if (shoulderY < -0.4) {\n return { korean: \"중위\", english: \"Mid\" };\n } else {\n return { korean: \"저위\", english: \"Low\" };\n }\n}\n\n/**\n * Get traditional Korean stance name from guard config\n */\nfunction getTraditionalStanceName(stance: TrigramStance): {\n korean: string;\n romanized: string;\n} {\n const stanceNames: Record<\n TrigramStance,\n { korean: string; romanized: string }\n > = {\n [TrigramStance.GEON]: { korean: \"앞서기\", romanized: \"Ap Seogi\" },\n [TrigramStance.TAE]: { korean: \"범서기\", romanized: \"Beom Seogi\" },\n [TrigramStance.LI]: { korean: \"겨루기 준비\", romanized: \"Gyeorugi Junbi\" },\n [TrigramStance.JIN]: { korean: \"주춤서기\", romanized: \"Juchum Seogi\" },\n [TrigramStance.SON]: { korean: \"학다리서기\", romanized: \"Hakdari Seogi\" },\n [TrigramStance.GAM]: { korean: \"뒷발서기\", romanized: \"Dwitbal Seogi\" },\n [TrigramStance.GAN]: { korean: \"모아서기\", romanized: \"Moa Seogi\" },\n [TrigramStance.GON]: { korean: \"중하\", romanized: \"Joong Ha Seogi\" },\n };\n return stanceNames[stance];\n}\n\n/**\n * Get weight distribution icon\n */\nfunction getWeightIcon(weight: \"forward\" | \"neutral\" | \"back\"): string {\n switch (weight) {\n case \"forward\":\n return \"▲\"; // Forward lean\n case \"back\":\n return \"▼\"; // Back lean\n case \"neutral\":\n return \"●\"; // Centered\n }\n}\n\n/**\n * GuardIndicator Component\n *\n * Displays current guard position information with Korean martial arts terminology.\n * Shows only when player is in a stance guard animation state.\n *\n * Features:\n * - Traditional Korean stance name (앞서기, 앞굽이, etc.)\n * - Romanized pronunciation\n * - Guard height indicator (High/Mid/Low)\n * - Weight distribution visualization\n * - Korean cyberpunk styling with glow effects\n * - Responsive mobile layout\n * - Accessible with proper ARIA labels\n *\n * @example\n * ```tsx\n * <GuardIndicator\n * currentStance={TrigramStance.GEON}\n * isInGuard={true}\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n *\n * @korean 방어자세표시기\n */\nexport const GuardIndicator: React.FC<GuardIndicatorProps> = ({\n currentStance,\n isInGuard,\n position: _position, // Currently unused - indicator always positioned bottom-right\n isMobile = false,\n}) => {\n const config = useMemo(\n () => STANCE_GUARD_CONFIGS[currentStance],\n [currentStance],\n );\n\n const trigramData = useMemo(\n () => TRIGRAM_DATA[currentStance],\n [currentStance],\n );\n\n const stanceName = useMemo(\n () => getTraditionalStanceName(currentStance),\n [currentStance],\n );\n\n const guardHeight = useMemo(\n () => getGuardHeight(currentStance),\n [currentStance],\n );\n\n const weightIcon = useMemo(\n () => getWeightIcon(config.guardPose.weight),\n [config.guardPose.weight],\n );\n\n const layout = useMemo(\n () => ({\n fontSize: isMobile ? 10 : 12,\n titleSize: isMobile ? 13 : 16,\n iconSize: isMobile ? 14 : 18,\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n gap: isMobile ? \"3px\" : \"4px\",\n }),\n [isMobile],\n );\n\n const containerStyle = useMemo(\n () => ({\n position: \"relative\" as const,\n display: \"flex\",\n flexDirection: \"column\" as const,\n gap: layout.gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n padding: layout.padding,\n pointerEvents: \"none\" as const,\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [layout],\n );\n\n if (!isInGuard) return null;\n\n return (\n <div\n data-testid=\"guard-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Guard position: ${stanceName.romanized}, ${guardHeight.english} guard, ${config.guardPose.weight} weight`}\n style={containerStyle}\n >\n {/* Title: GUARD with trigram symbol */}\n <div\n style={{\n fontSize: layout.titleSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(\n KOREAN_COLORS.ACCENT_GOLD,\n 0.6,\n )}`,\n borderBottom: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.4,\n )}`,\n paddingBottom: layout.gap,\n }}\n >\n {trigramData.symbol} GUARD\n </div>\n\n {/* Traditional stance name (Korean) */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: \"center\",\n fontWeight: \"600\",\n }}\n >\n {stanceName.korean}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8),\n textAlign: \"center\",\n fontStyle: \"italic\",\n }}\n >\n {stanceName.romanized}\n </div>\n\n {/* Guard characteristics row */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-around\",\n marginTop: layout.gap,\n paddingTop: layout.gap,\n borderTop: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n }}\n >\n {/* Guard height */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n ⚔️\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {guardHeight.korean}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n }}\n >\n {guardHeight.english}\n </div>\n </div>\n\n {/* Weight distribution */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n {weightIcon}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {config.guardPose.weight === \"forward\" && \"전방\"}\n {config.guardPose.weight === \"neutral\" && \"중립\"}\n {config.guardPose.weight === \"back\" && \"후방\"}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n textTransform: \"capitalize\" as const,\n }}\n >\n {config.guardPose.weight}\n </div>\n </div>\n </div>\n </div>\n );\n};\n\n/**\n * Memoized GuardIndicator to prevent unnecessary re-renders\n */\nexport default React.memo(GuardIndicator);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAS,eAAe,QAGtB;CAEA,MAAM,YADS,qBAAqB,QACX,UAAU,QAAQ,SAAS;CAEpD,IAAI,aAAa,KACf,OAAO;EAAE,QAAQ;EAAM,SAAS;EAAQ;MACnC,IAAI,YAAY,KACrB,OAAO;EAAE,QAAQ;EAAM,SAAS;EAAO;MAEvC,OAAO;EAAE,QAAQ;EAAM,SAAS;EAAO;;;;;AAO3C,SAAS,yBAAyB,QAGhC;CAcA,OAAO;GATJ,cAAc,OAAO;GAAE,QAAQ;GAAO,WAAW;GAAY;GAC7D,cAAc,MAAM;GAAE,QAAQ;GAAO,WAAW;GAAc;GAC9D,cAAc,KAAK;GAAE,QAAQ;GAAU,WAAW;GAAkB;GACpE,cAAc,MAAM;GAAE,QAAQ;GAAQ,WAAW;GAAgB;GACjE,cAAc,MAAM;GAAE,QAAQ;GAAS,WAAW;GAAiB;GACnE,cAAc,MAAM;GAAE,QAAQ;GAAQ,WAAW;GAAiB;GAClE,cAAc,MAAM;GAAE,QAAQ;GAAQ,WAAW;GAAa;GAC9D,cAAc,MAAM;GAAE,QAAQ;GAAM,WAAW;GAAkB;EAE7D,CAAY;;;;;AAMrB,SAAS,cAAc,QAAgD;CACrE,QAAQ,QAAR;EACE,KAAK,WACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,WACH,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+Bb,IAAa,kBAAiD,EAC5D,eACA,WACA,UAAU,WACV,WAAW,YACP;CACJ,MAAM,SAAS,cACP,qBAAqB,gBAC3B,CAAC,cAAc,CAChB;CAED,MAAM,cAAc,cACZ,aAAa,gBACnB,CAAC,cAAc,CAChB;CAED,MAAM,aAAa,cACX,yBAAyB,cAAc,EAC7C,CAAC,cAAc,CAChB;CAED,MAAM,cAAc,cACZ,eAAe,cAAc,EACnC,CAAC,cAAc,CAChB;CAED,MAAM,aAAa,cACX,cAAc,OAAO,UAAU,OAAO,EAC5C,CAAC,OAAO,UAAU,OAAO,CAC1B;CAED,MAAM,SAAS,eACN;EACL,UAAU,WAAW,KAAK;EAC1B,WAAW,WAAW,KAAK;EAC3B,UAAU,WAAW,KAAK;EAC1B,SAAS,WAAW,aAAa;EACjC,KAAK,WAAW,QAAQ;EACzB,GACD,CAAC,SAAS,CACX;CAED,MAAM,iBAAiB,eACd;EACL,UAAU;EACV,SAAS;EACT,eAAe;EACf,KAAK,OAAO;EACZ,iBAAiB,gBAAgB,cAAc,oBAAoB,IAAK;EACxE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,GAAI;EACrE,cAAc;EACd,SAAS,OAAO;EAChB,eAAe;EACf,OAAO;EACP,WAAW;EACX,WAAW,YAAY,gBAAgB,cAAc,cAAc,GAAI;EACxE,GACD,CAAC,OAAO,CACT;CAED,IAAI,CAAC,WAAW,OAAO;CAEvB,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,MAAK;EACL,aAAU;EACV,cAAY,mBAAmB,WAAW,UAAU,IAAI,YAAY,QAAQ,UAAU,OAAO,UAAU,OAAO;EAC9G,OAAO;YALT;GAQE,qBAAC,OAAD;IACE,OAAO;KACL,UAAU,OAAO;KACjB,OAAO,gBAAgB,cAAc,aAAa,EAAE;KACpD,YAAY;KACZ,WAAW;KACX,YAAY,YAAY,gBACtB,cAAc,aACd,GACD;KACD,cAAc,aAAa,gBACzB,cAAc,cACd,GACD;KACD,eAAe,OAAO;KACvB;cAfH,CAiBG,YAAY,QAAO,SAChB;;GAGN,oBAAC,OAAD;IACE,OAAO;KACL,UAAU,OAAO;KACjB,OAAO,gBAAgB,cAAc,cAAc,EAAE;KACrD,WAAW;KACX,YAAY;KACb;cAEA,WAAW;IACR,CAAA;GAGN,oBAAC,OAAD;IACE,OAAO;KACL,UAAU,OAAO;KACjB,OAAO,gBAAgB,cAAc,aAAa,GAAI;KACtD,WAAW;KACX,WAAW;KACZ;cAEA,WAAW;IACR,CAAA;GAGN,qBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,gBAAgB;KAChB,WAAW,OAAO;KAClB,YAAY,OAAO;KACnB,WAAW,aAAa,gBACtB,cAAc,cACd,GACD;KACF;cAVH,CAaE,qBAAC,OAAD;KAAK,OAAO,EAAE,WAAW,UAAU;eAAnC;MACE,oBAAC,OAAD;OACE,OAAO;QACL,UAAU,OAAO;QACjB,OAAO,gBAAgB,cAAc,cAAc,EAAE;QACtD;iBACF;OAEK,CAAA;MACN,oBAAC,OAAD;OACE,OAAO;QACL,UAAU,OAAO,WAAW;QAC5B,OAAO,gBAAgB,cAAc,aAAa,GAAI;QACvD;iBAEA,YAAY;OACT,CAAA;MACN,oBAAC,OAAD;OACE,OAAO;QACL,UAAU,OAAO,WAAW;QAC5B,OAAO,gBAAgB,cAAc,aAAa,GAAI;QACvD;iBAEA,YAAY;OACT,CAAA;MACF;QAGN,qBAAC,OAAD;KAAK,OAAO,EAAE,WAAW,UAAU;eAAnC;MACE,oBAAC,OAAD;OACE,OAAO;QACL,UAAU,OAAO;QACjB,OAAO,gBAAgB,cAAc,cAAc,EAAE;QACtD;iBAEA;OACG,CAAA;MACN,qBAAC,OAAD;OACE,OAAO;QACL,UAAU,OAAO,WAAW;QAC5B,OAAO,gBAAgB,cAAc,aAAa,GAAI;QACvD;iBAJH;QAMG,OAAO,UAAU,WAAW,aAAa;QACzC,OAAO,UAAU,WAAW,aAAa;QACzC,OAAO,UAAU,WAAW,UAAU;QACnC;;MACN,oBAAC,OAAD;OACE,OAAO;QACL,UAAU,OAAO,WAAW;QAC5B,OAAO,gBAAgB,cAAc,aAAa,GAAI;QACtD,eAAe;QAChB;iBAEA,OAAO,UAAU;OACd,CAAA;MACF;OACF;;GACF;;;AAOK,MAAM,KAAK,eAAe"}
|
|
1
|
+
{"version":3,"file":"GuardIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/GuardIndicator.tsx"],"sourcesContent":["/**\n * GuardIndicator - Visual indicator showing current fighting stance guard position\n * Displays Korean traditional stance name, English translation, and guard characteristics\n *\n * @module components/shared/three/indicators/GuardIndicator\n * @category Combat UI\n * @korean 방어자세표시기\n */\n\nimport React, { useMemo } from \"react\";\nimport { STANCE_GUARD_CONFIGS } from \"../../../../systems/animation\";\nimport { TRIGRAM_DATA } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Props for GuardIndicator component\n */\nexport interface GuardIndicatorProps {\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Whether player is in guard animation state */\n readonly isInGuard: boolean;\n /** Player position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n}\n\n/**\n * Get guard height label based on arm positions\n *\n * Guard heights based on shoulder.x (vertical angle in radians):\n * - High guard (고위): shoulder at chin level (x <= -0.8)\n * GEON (-1.0), LI (-1.6), GAN (-1.0)\n * - Mid guard (중위): shoulder at chest level (-0.8 < x < -0.4)\n * TAE (-0.7), JIN (-0.6), SON (-0.8), GAM (-0.8)\n * - Low guard (저위): shoulder low (x >= -0.4)\n * GON (-0.4)\n */\nfunction getGuardHeight(stance: TrigramStance): {\n korean: string;\n english: string;\n} {\n const config = STANCE_GUARD_CONFIGS[stance];\n const shoulderY = config.guardPose.leftArm.shoulder.x; // x is vertical in Euler\n\n if (shoulderY <= -0.8) {\n return { korean: \"고위\", english: \"High\" };\n } else if (shoulderY < -0.4) {\n return { korean: \"중위\", english: \"Mid\" };\n } else {\n return { korean: \"저위\", english: \"Low\" };\n }\n}\n\n/**\n * Get traditional Korean stance name from guard config\n */\nfunction getTraditionalStanceName(stance: TrigramStance): {\n korean: string;\n romanized: string;\n} {\n const stanceNames: Record<\n TrigramStance,\n { korean: string; romanized: string }\n > = {\n [TrigramStance.GEON]: { korean: \"앞서기\", romanized: \"Ap Seogi\" },\n [TrigramStance.TAE]: { korean: \"범서기\", romanized: \"Beom Seogi\" },\n [TrigramStance.LI]: { korean: \"겨루기 준비\", romanized: \"Gyeorugi Junbi\" },\n [TrigramStance.JIN]: { korean: \"주춤서기\", romanized: \"Juchum Seogi\" },\n [TrigramStance.SON]: { korean: \"학다리서기\", romanized: \"Hakdari Seogi\" },\n [TrigramStance.GAM]: { korean: \"뒷발서기\", romanized: \"Dwitbal Seogi\" },\n [TrigramStance.GAN]: { korean: \"모아서기\", romanized: \"Moa Seogi\" },\n [TrigramStance.GON]: { korean: \"중하\", romanized: \"Joong Ha Seogi\" },\n };\n return stanceNames[stance];\n}\n\n/**\n * Get weight distribution icon\n */\nfunction getWeightIcon(weight: \"forward\" | \"neutral\" | \"back\"): string {\n switch (weight) {\n case \"forward\":\n return \"▲\"; // Forward lean\n case \"back\":\n return \"▼\"; // Back lean\n case \"neutral\":\n return \"●\"; // Centered\n }\n}\n\n/**\n * GuardIndicator Component\n *\n * Displays current guard position information with Korean martial arts terminology.\n * Shows only when player is in a stance guard animation state.\n *\n * Features:\n * - Traditional Korean stance name (앞서기, 앞굽이, etc.)\n * - Romanized pronunciation\n * - Guard height indicator (High/Mid/Low)\n * - Weight distribution visualization\n * - Korean cyberpunk styling with glow effects\n * - Responsive mobile layout\n * - Accessible with proper ARIA labels\n *\n * @example\n * ```tsx\n * <GuardIndicator\n * currentStance={TrigramStance.GEON}\n * isInGuard={true}\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n *\n * @korean 방어자세표시기\n */\nexport const GuardIndicator: React.FC<GuardIndicatorProps> = ({\n currentStance,\n isInGuard,\n position: _position, // Currently unused - indicator always positioned bottom-right\n isMobile = false,\n}) => {\n const config = useMemo(\n () => STANCE_GUARD_CONFIGS[currentStance],\n [currentStance],\n );\n\n const trigramData = useMemo(\n () => TRIGRAM_DATA[currentStance],\n [currentStance],\n );\n\n const stanceName = useMemo(\n () => getTraditionalStanceName(currentStance),\n [currentStance],\n );\n\n const guardHeight = useMemo(\n () => getGuardHeight(currentStance),\n [currentStance],\n );\n\n const weightIcon = useMemo(\n () => getWeightIcon(config.guardPose.weight),\n [config.guardPose.weight],\n );\n\n const layout = useMemo(\n () => ({\n fontSize: isMobile ? 10 : 12,\n titleSize: isMobile ? 13 : 16,\n iconSize: isMobile ? 14 : 18,\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n gap: isMobile ? \"3px\" : \"4px\",\n }),\n [isMobile],\n );\n\n const containerStyle = useMemo(\n () => ({\n position: \"relative\" as const,\n display: \"flex\",\n flexDirection: \"column\" as const,\n gap: layout.gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n padding: layout.padding,\n pointerEvents: \"none\" as const,\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [layout],\n );\n\n if (!isInGuard) return null;\n\n return (\n <div\n data-testid=\"guard-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Guard position: ${stanceName.romanized}, ${guardHeight.english} guard, ${config.guardPose.weight} weight`}\n style={containerStyle}\n >\n {/* Title: GUARD with trigram symbol */}\n <div\n style={{\n fontSize: layout.titleSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(\n KOREAN_COLORS.ACCENT_GOLD,\n 0.6,\n )}`,\n borderBottom: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.4,\n )}`,\n paddingBottom: layout.gap,\n }}\n >\n {trigramData.symbol} GUARD\n </div>\n\n {/* Traditional stance name (Korean) */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: \"center\",\n fontWeight: \"600\",\n }}\n >\n {stanceName.korean}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8),\n textAlign: \"center\",\n fontStyle: \"italic\",\n }}\n >\n {stanceName.romanized}\n </div>\n\n {/* Guard characteristics row */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-around\",\n marginTop: layout.gap,\n paddingTop: layout.gap,\n borderTop: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n }}\n >\n {/* Guard height */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n ⚔️\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {guardHeight.korean}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n }}\n >\n {guardHeight.english}\n </div>\n </div>\n\n {/* Weight distribution */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n {weightIcon}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {config.guardPose.weight === \"forward\" && \"전방\"}\n {config.guardPose.weight === \"neutral\" && \"중립\"}\n {config.guardPose.weight === \"back\" && \"후방\"}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n textTransform: \"capitalize\" as const,\n }}\n >\n {config.guardPose.weight}\n </div>\n </div>\n </div>\n </div>\n );\n};\n\n/**\n * Memoized GuardIndicator to prevent unnecessary re-renders\n */\nexport default React.memo(GuardIndicator);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HapticFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/HapticFeedback.ts"],"sourcesContent":["/**\n * Haptic Feedback System for Combat UI\n * \n * Provides tactile feedback for mobile devices during combat interactions\n * such as guard activation, stance changes, and guard breaks.\n * \n * Uses the Vibration API with fallback for unsupported devices.\n * \n * @module components/shared/three/indicators/HapticFeedback\n * @category Combat UI\n * @korean 햅틱피드백\n */\n\n/**\n * Haptic pattern type\n * @korean 햅틱패턴타입\n */\nexport type HapticPattern = number | number[];\n\n/**\n * Check if haptic feedback is supported on this device\n * \n * @returns True if the Vibration API is available\n * \n * @example\n * ```typescript\n * if (isHapticSupported()) {\n * triggerGuardHaptic('activate');\n * }\n * ```\n * \n * @korean 햅틱지원여부\n */\nexport function isHapticSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"vibrate\" in navigator &&\n typeof navigator.vibrate === \"function\"\n );\n}\n\n/**\n * Trigger haptic feedback for guard activation or break\n * \n * Provides tactile feedback when a player activates their guard\n * or when their guard is broken by an opponent.\n * \n * Patterns:\n * - **activate**: Light single vibration (50ms) for guard activation\n * - **break**: Strong triple-pulse pattern (100ms, 50ms pause, 100ms) for guard break\n * \n * @param type - Type of guard haptic feedback\n * \n * @example\n * ```typescript\n * // When player activates guard\n * triggerGuardHaptic('activate');\n * \n * // When guard is broken\n * triggerGuardHaptic('break');\n * ```\n * \n * @korean 방어햅틱트리거\n */\nexport function triggerGuardHaptic(type: \"activate\" | \"break\"): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n if (type === \"activate\") {\n navigator.vibrate(50);\n } else if (type === \"break\") {\n navigator.vibrate([100, 50, 100]);\n }\n } catch (error) {\n console.warn(\"Haptic feedback failed:\", error);\n }\n}\n\n/**\n * Trigger haptic feedback for stance change\n * \n * Provides medium-strength tactile feedback when a player transitions\n * between trigram stances (건→태→리→진→손→감→간→곤).\n * \n * @example\n * ```typescript\n * // When stance changes from Geon to Tae\n * triggerStanceChangeHaptic();\n * ```\n * \n * @korean 자세변경햅틱트리거\n */\nexport function triggerStanceChangeHaptic(): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n navigator.vibrate(75);\n } catch (error) {\n console.warn(\"Haptic feedback failed:\", error);\n }\n}\n\n/**\n * Trigger custom haptic pattern\n * \n * Allows for custom vibration patterns using the Vibration API.\n * Can specify either a single duration or a pattern array.\n * \n * Pattern arrays alternate between vibration and pause:\n * - [200, 100, 200] = vibrate 200ms, pause 100ms, vibrate 200ms\n * \n * @param pattern - Vibration duration in ms or pattern array\n * \n * @example\n * ```typescript\n * // Single vibration\n * triggerCustomHaptic(200);\n * \n * // Complex pattern\n * triggerCustomHaptic([100, 50, 100, 50, 100]);\n * ```\n * \n * @korean 사용자정의햅틱트리거\n */\nexport function triggerCustomHaptic(pattern: HapticPattern): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n navigator.vibrate(pattern);\n } catch (error) {\n console.warn(\"Haptic feedback failed:\", error);\n }\n}\n\n/**\n * Stop all haptic feedback\n * \n * Immediately stops any ongoing vibration. Useful for interrupting\n * long or repeated patterns.\n * \n * @example\n * ```typescript\n * // Stop any ongoing haptic feedback\n * stopHaptic();\n * ```\n * \n * @korean 햅틱중지\n */\nexport function stopHaptic(): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n navigator.vibrate(0);\n } catch (error) {\n console.warn(\"Haptic stop failed:\", error);\n }\n}\n\n/**\n * Check if device is mobile\n * \n * Simple heuristic to detect mobile devices based on screen size,\n * user agent, and touch support.\n * \n * @returns True if device is likely mobile\n * \n * @example\n * ```typescript\n * if (isMobileDevice() && isHapticSupported()) {\n * triggerStanceChangeHaptic();\n * }\n * ```\n * \n * @korean 모바일기기여부\n */\nexport function isMobileDevice(): boolean {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return false;\n }\n\n const isMobileSize = window.innerWidth < 768;\n\n const mobileKeywords = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileKeywords.test(navigator.userAgent);\n\n const hasTouch =\n \"ontouchstart\" in window ||\n navigator.maxTouchPoints > 0;\n\n return (isMobileSize || isMobileUA) && hasTouch;\n}\n\n/**\n * Haptic feedback settings\n * \n * Allows for global configuration of haptic feedback intensity\n * and enable/disable state.\n * \n * @korean 햅틱설정\n */\nexport interface HapticSettings {\n /** Whether haptic feedback is enabled */\n readonly enabled: boolean;\n /** Intensity multiplier (0.0 to 1.0) */\n readonly intensity: number;\n}\n\n/**\n * Default haptic settings\n * @korean 기본햅틱설정\n */\nexport const DEFAULT_HAPTIC_SETTINGS: HapticSettings = {\n enabled: true,\n intensity: 1.0,\n};\n\n/**\n * Apply intensity modifier to haptic pattern\n * \n * Scales vibration durations based on intensity setting.\n * \n * @param pattern - Original haptic pattern\n * @param intensity - Intensity multiplier (0.0 to 1.0)\n * @returns Scaled haptic pattern\n * \n * @internal\n * @korean 햅틱강도적용\n */\nexport function applyIntensity(\n pattern: HapticPattern,\n intensity: number\n): HapticPattern {\n const clampedIntensity = Math.max(0, Math.min(1, intensity));\n\n if (typeof pattern === \"number\") {\n return Math.round(pattern * clampedIntensity);\n }\n\n return pattern.map((duration) => Math.round(duration * clampedIntensity));\n}\n\n/**\n * Trigger haptic with settings\n * \n * Wrapper function that applies haptic settings before triggering.\n * \n * @param pattern - Haptic pattern to trigger\n * @param settings - Haptic settings to apply\n * \n * @example\n * ```typescript\n * const settings = { enabled: true, intensity: 0.7 };\n * triggerWithSettings(100, settings); // Triggers 70ms vibration\n * ```\n * \n * @korean 설정포함햅틱트리거\n */\nexport function triggerWithSettings(\n pattern: HapticPattern,\n settings: HapticSettings = DEFAULT_HAPTIC_SETTINGS\n): void {\n if (!settings.enabled || !isHapticSupported()) {\n return;\n }\n\n const scaledPattern = applyIntensity(pattern, settings.intensity);\n triggerCustomHaptic(scaledPattern);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiCA,SAAgB,oBAA6B;CAC3C,OACE,OAAO,cAAc,eACrB,aAAa,aACb,OAAO,UAAU,YAAY
|
|
1
|
+
{"version":3,"file":"HapticFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/HapticFeedback.ts"],"sourcesContent":["/**\n * Haptic Feedback System for Combat UI\n * \n * Provides tactile feedback for mobile devices during combat interactions\n * such as guard activation, stance changes, and guard breaks.\n * \n * Uses the Vibration API with fallback for unsupported devices.\n * \n * @module components/shared/three/indicators/HapticFeedback\n * @category Combat UI\n * @korean 햅틱피드백\n */\n\n/**\n * Haptic pattern type\n * @korean 햅틱패턴타입\n */\nexport type HapticPattern = number | number[];\n\n/**\n * Check if haptic feedback is supported on this device\n * \n * @returns True if the Vibration API is available\n * \n * @example\n * ```typescript\n * if (isHapticSupported()) {\n * triggerGuardHaptic('activate');\n * }\n * ```\n * \n * @korean 햅틱지원여부\n */\nexport function isHapticSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"vibrate\" in navigator &&\n typeof navigator.vibrate === \"function\"\n );\n}\n\n/**\n * Trigger haptic feedback for guard activation or break\n * \n * Provides tactile feedback when a player activates their guard\n * or when their guard is broken by an opponent.\n * \n * Patterns:\n * - **activate**: Light single vibration (50ms) for guard activation\n * - **break**: Strong triple-pulse pattern (100ms, 50ms pause, 100ms) for guard break\n * \n * @param type - Type of guard haptic feedback\n * \n * @example\n * ```typescript\n * // When player activates guard\n * triggerGuardHaptic('activate');\n * \n * // When guard is broken\n * triggerGuardHaptic('break');\n * ```\n * \n * @korean 방어햅틱트리거\n */\nexport function triggerGuardHaptic(type: \"activate\" | \"break\"): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n if (type === \"activate\") {\n navigator.vibrate(50);\n } else if (type === \"break\") {\n navigator.vibrate([100, 50, 100]);\n }\n } catch (error) {\n console.warn(\"Haptic feedback failed:\", error);\n }\n}\n\n/**\n * Trigger haptic feedback for stance change\n * \n * Provides medium-strength tactile feedback when a player transitions\n * between trigram stances (건→태→리→진→손→감→간→곤).\n * \n * @example\n * ```typescript\n * // When stance changes from Geon to Tae\n * triggerStanceChangeHaptic();\n * ```\n * \n * @korean 자세변경햅틱트리거\n */\nexport function triggerStanceChangeHaptic(): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n navigator.vibrate(75);\n } catch (error) {\n console.warn(\"Haptic feedback failed:\", error);\n }\n}\n\n/**\n * Trigger custom haptic pattern\n * \n * Allows for custom vibration patterns using the Vibration API.\n * Can specify either a single duration or a pattern array.\n * \n * Pattern arrays alternate between vibration and pause:\n * - [200, 100, 200] = vibrate 200ms, pause 100ms, vibrate 200ms\n * \n * @param pattern - Vibration duration in ms or pattern array\n * \n * @example\n * ```typescript\n * // Single vibration\n * triggerCustomHaptic(200);\n * \n * // Complex pattern\n * triggerCustomHaptic([100, 50, 100, 50, 100]);\n * ```\n * \n * @korean 사용자정의햅틱트리거\n */\nexport function triggerCustomHaptic(pattern: HapticPattern): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n navigator.vibrate(pattern);\n } catch (error) {\n console.warn(\"Haptic feedback failed:\", error);\n }\n}\n\n/**\n * Stop all haptic feedback\n * \n * Immediately stops any ongoing vibration. Useful for interrupting\n * long or repeated patterns.\n * \n * @example\n * ```typescript\n * // Stop any ongoing haptic feedback\n * stopHaptic();\n * ```\n * \n * @korean 햅틱중지\n */\nexport function stopHaptic(): void {\n if (!isHapticSupported()) {\n return;\n }\n\n try {\n navigator.vibrate(0);\n } catch (error) {\n console.warn(\"Haptic stop failed:\", error);\n }\n}\n\n/**\n * Check if device is mobile\n * \n * Simple heuristic to detect mobile devices based on screen size,\n * user agent, and touch support.\n * \n * @returns True if device is likely mobile\n * \n * @example\n * ```typescript\n * if (isMobileDevice() && isHapticSupported()) {\n * triggerStanceChangeHaptic();\n * }\n * ```\n * \n * @korean 모바일기기여부\n */\nexport function isMobileDevice(): boolean {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return false;\n }\n\n const isMobileSize = window.innerWidth < 768;\n\n const mobileKeywords = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;\n const isMobileUA = mobileKeywords.test(navigator.userAgent);\n\n const hasTouch =\n \"ontouchstart\" in window ||\n navigator.maxTouchPoints > 0;\n\n return (isMobileSize || isMobileUA) && hasTouch;\n}\n\n/**\n * Haptic feedback settings\n * \n * Allows for global configuration of haptic feedback intensity\n * and enable/disable state.\n * \n * @korean 햅틱설정\n */\nexport interface HapticSettings {\n /** Whether haptic feedback is enabled */\n readonly enabled: boolean;\n /** Intensity multiplier (0.0 to 1.0) */\n readonly intensity: number;\n}\n\n/**\n * Default haptic settings\n * @korean 기본햅틱설정\n */\nexport const DEFAULT_HAPTIC_SETTINGS: HapticSettings = {\n enabled: true,\n intensity: 1.0,\n};\n\n/**\n * Apply intensity modifier to haptic pattern\n * \n * Scales vibration durations based on intensity setting.\n * \n * @param pattern - Original haptic pattern\n * @param intensity - Intensity multiplier (0.0 to 1.0)\n * @returns Scaled haptic pattern\n * \n * @internal\n * @korean 햅틱강도적용\n */\nexport function applyIntensity(\n pattern: HapticPattern,\n intensity: number\n): HapticPattern {\n const clampedIntensity = Math.max(0, Math.min(1, intensity));\n\n if (typeof pattern === \"number\") {\n return Math.round(pattern * clampedIntensity);\n }\n\n return pattern.map((duration) => Math.round(duration * clampedIntensity));\n}\n\n/**\n * Trigger haptic with settings\n * \n * Wrapper function that applies haptic settings before triggering.\n * \n * @param pattern - Haptic pattern to trigger\n * @param settings - Haptic settings to apply\n * \n * @example\n * ```typescript\n * const settings = { enabled: true, intensity: 0.7 };\n * triggerWithSettings(100, settings); // Triggers 70ms vibration\n * ```\n * \n * @korean 설정포함햅틱트리거\n */\nexport function triggerWithSettings(\n pattern: HapticPattern,\n settings: HapticSettings = DEFAULT_HAPTIC_SETTINGS\n): void {\n if (!settings.enabled || !isHapticSupported()) {\n return;\n }\n\n const scaledPattern = applyIntensity(pattern, settings.intensity);\n triggerCustomHaptic(scaledPattern);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiCA,SAAgB,oBAA6B;CAC3C,OACE,OAAO,cAAc,eACrB,aAAa,aACb,OAAO,UAAU,YAAY;AAEjC;;;;;;;;;;;;;;;AAuDA,SAAgB,4BAAkC;CAChD,IAAI,CAAC,kBAAkB,GACrB;CAGF,IAAI;EACF,UAAU,QAAQ,EAAE;CACtB,SAAS,OAAO;EACd,QAAQ,KAAK,2BAA2B,KAAK;CAC/C;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StanceChangeIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/StanceChangeIndicator.tsx"],"sourcesContent":["/**\n * StanceChangeIndicator - Visual feedback for stance changes\n * Displays Korean and English stance names with trigram symbols\n * \n * @module components/shared/three/indicators/StanceChangeIndicator\n * @category Combat UI\n * @korean 자세변경표시기\n */\n\n \nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useState, useMemo, useRef } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { \n getTrigramElementColor, \n getTrigramKoreanName, \n getTrigramEnglishName,\n getTrigramSymbol\n} from \"./ElementalColorSystem\";\nimport { triggerStanceChangeHaptic } from \"./HapticFeedback\";\n\n/**\n * Props for StanceChangeIndicator component\n */\nexport interface StanceChangeIndicatorProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Previous stance index (0-7) for change detection */\n readonly previousStance: number;\n /** Mobile layout flag */\n readonly isMobile?: boolean;\n /** Display duration in milliseconds (default: 1000ms) */\n readonly duration?: number;\n /** Show transition progress bar (default: true) */\n readonly showProgress?: boolean;\n /** Transition duration in milliseconds for progress bar (default: 600ms) */\n readonly transitionDuration?: number;\n}\n\n/**\n * StanceChangeIndicator Component\n * \n * Displays a temporary overlay showing the current trigram stance\n * with Korean name, English name, and trigram symbol.\n * \n * Features:\n * - Fade in/out animation\n * - Korean cyberpunk styling\n * - Responsive mobile layout\n * - Trigram symbol display\n * - Glow effect for visual emphasis\n * \n * @example\n * ```tsx\n * <StanceChangeIndicator\n * currentStance={player.stance}\n * previousStance={previousStance}\n * isMobile={isMobile}\n * />\n * ```\n * \n * @korean 자세변경표시기\n */\nexport const StanceChangeIndicator: React.FC<StanceChangeIndicatorProps> = ({\n currentStance,\n previousStance,\n isMobile = false,\n duration = 1000,\n showProgress = true,\n transitionDuration = 600,\n}) => {\n const [showIndicator, setShowIndicator] = useState(false);\n const [progress, setProgress] = useState(0);\n const isMountedRef = useRef(true);\n const startTimeRef = useRef<number>(0);\n const animationFrameRef = useRef<number | undefined>(undefined);\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (animationFrameRef.current !== undefined) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = undefined;\n }\n\n if (currentStance !== previousStance) {\n setShowIndicator(true);\n setProgress(0);\n startTimeRef.current = 0;\n\n triggerStanceChangeHaptic();\n\n if (showProgress) {\n const animate = (timestamp: number) => {\n if (!isMountedRef.current) return;\n \n if (startTimeRef.current === 0) {\n startTimeRef.current = timestamp;\n }\n \n const elapsed = timestamp - startTimeRef.current;\n const newProgress = Math.min((elapsed / transitionDuration) * 100, 100);\n \n setProgress(newProgress);\n\n if (newProgress < 100) {\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n };\n\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n\n const timer = setTimeout(() => {\n if (isMountedRef.current) {\n setShowIndicator(false);\n }\n }, duration);\n\n return () => {\n clearTimeout(timer);\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n };\n }\n }, [currentStance, previousStance, duration, showProgress, transitionDuration]);\n\n const stance = useMemo(\n () => TRIGRAM_STANCES_ORDER[currentStance] ?? TrigramStance.GEON,\n [currentStance]\n );\n\n const elementalColor = useMemo(\n () => getTrigramElementColor(stance),\n [stance]\n );\n\n const koreanName = useMemo(\n () => getTrigramKoreanName(stance),\n [stance]\n );\n\n const englishName = useMemo(\n () => getTrigramEnglishName(stance),\n [stance]\n );\n\n const trigramSymbol = useMemo(\n () => getTrigramSymbol(stance),\n [stance]\n );\n\n const animationStyles = useMemo(() => (\n <style>\n {`\n @keyframes fadeInOut {\n 0% {\n opacity: 0;\n transform: translateY(-20px);\n }\n 10% {\n opacity: 1;\n transform: translateY(0);\n }\n 90% {\n opacity: 1;\n transform: translateY(0);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px);\n }\n }\n `}\n </style>\n ), []);\n\n if (!showIndicator) return null;\n\n const fontSize = isMobile ? 24 : 36;\n const subFontSize = isMobile ? 14 : 18;\n const top = isMobile ? \"30%\" : \"20%\";\n\n const primaryColorHex = hexToRgbaString(elementalColor, 1);\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"stance-change-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Stance changed to ${englishName}`}\n style={{\n position: \"absolute\",\n top,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n textAlign: \"center\",\n animation: \"fadeInOut 1s\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n >\n {/* Main stance display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: primaryColorHex,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n textShadow: `0 0 20px ${hexToRgbaString(elementalColor, 0.8)}, \n 0 0 40px ${hexToRgbaString(elementalColor, 0.5)}`,\n marginBottom: \"8px\",\n }}\n >\n {koreanName} {trigramSymbol}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6)}`,\n marginBottom: showProgress ? \"12px\" : \"0\",\n }}\n >\n {englishName}\n </div>\n\n {/* Transition progress bar (600ms) */}\n {showProgress && (\n <div\n data-testid=\"stance-transition-progress\"\n style={{\n width: \"200px\",\n margin: \"0 auto\",\n padding: \"8px 0\",\n }}\n >\n {/* Progress label */}\n <div\n style={{\n fontSize: \"11px\",\n color: \"rgba(255, 255, 255, 0.7)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n letterSpacing: \"1px\",\n }}\n >\n 팔괘전환 | Transition\n </div>\n \n {/* Progress bar container */}\n <div\n style={{\n width: \"100%\",\n height: \"6px\",\n backgroundColor: \"rgba(0, 0, 0, 0.6)\",\n borderRadius: \"3px\",\n overflow: \"hidden\",\n border: `1px solid ${hexToRgbaString(elementalColor, 0.3)}`,\n }}\n >\n {/* Progress bar fill */}\n <div\n style={{\n width: `${progress}%`,\n height: \"100%\",\n backgroundColor: primaryColorHex,\n transition: \"width 0.05s linear\",\n boxShadow: `0 0 8px ${primaryColorHex}`,\n }}\n />\n </div>\n \n {/* Time remaining */}\n <div\n style={{\n fontSize: \"10px\",\n color: \"rgba(255, 255, 255, 0.5)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginTop: \"4px\",\n }}\n >\n {Math.max(0, Math.ceil(transitionDuration * (1 - progress / 100)))}ms\n </div>\n </div>\n )}\n\n {/* CSS Animation - Memoized to prevent redefinition */}\n {animationStyles}\n </div>\n </Html>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StanceChangeIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/StanceChangeIndicator.tsx"],"sourcesContent":["/**\n * StanceChangeIndicator - Visual feedback for stance changes\n * Displays Korean and English stance names with trigram symbols\n * \n * @module components/shared/three/indicators/StanceChangeIndicator\n * @category Combat UI\n * @korean 자세변경표시기\n */\n\n \nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useState, useMemo, useRef } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { \n getTrigramElementColor, \n getTrigramKoreanName, \n getTrigramEnglishName,\n getTrigramSymbol\n} from \"./ElementalColorSystem\";\nimport { triggerStanceChangeHaptic } from \"./HapticFeedback\";\n\n/**\n * Props for StanceChangeIndicator component\n */\nexport interface StanceChangeIndicatorProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Previous stance index (0-7) for change detection */\n readonly previousStance: number;\n /** Mobile layout flag */\n readonly isMobile?: boolean;\n /** Display duration in milliseconds (default: 1000ms) */\n readonly duration?: number;\n /** Show transition progress bar (default: true) */\n readonly showProgress?: boolean;\n /** Transition duration in milliseconds for progress bar (default: 600ms) */\n readonly transitionDuration?: number;\n}\n\n/**\n * StanceChangeIndicator Component\n * \n * Displays a temporary overlay showing the current trigram stance\n * with Korean name, English name, and trigram symbol.\n * \n * Features:\n * - Fade in/out animation\n * - Korean cyberpunk styling\n * - Responsive mobile layout\n * - Trigram symbol display\n * - Glow effect for visual emphasis\n * \n * @example\n * ```tsx\n * <StanceChangeIndicator\n * currentStance={player.stance}\n * previousStance={previousStance}\n * isMobile={isMobile}\n * />\n * ```\n * \n * @korean 자세변경표시기\n */\nexport const StanceChangeIndicator: React.FC<StanceChangeIndicatorProps> = ({\n currentStance,\n previousStance,\n isMobile = false,\n duration = 1000,\n showProgress = true,\n transitionDuration = 600,\n}) => {\n const [showIndicator, setShowIndicator] = useState(false);\n const [progress, setProgress] = useState(0);\n const isMountedRef = useRef(true);\n const startTimeRef = useRef<number>(0);\n const animationFrameRef = useRef<number | undefined>(undefined);\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (animationFrameRef.current !== undefined) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = undefined;\n }\n\n if (currentStance !== previousStance) {\n setShowIndicator(true);\n setProgress(0);\n startTimeRef.current = 0;\n\n triggerStanceChangeHaptic();\n\n if (showProgress) {\n const animate = (timestamp: number) => {\n if (!isMountedRef.current) return;\n \n if (startTimeRef.current === 0) {\n startTimeRef.current = timestamp;\n }\n \n const elapsed = timestamp - startTimeRef.current;\n const newProgress = Math.min((elapsed / transitionDuration) * 100, 100);\n \n setProgress(newProgress);\n\n if (newProgress < 100) {\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n };\n\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n\n const timer = setTimeout(() => {\n if (isMountedRef.current) {\n setShowIndicator(false);\n }\n }, duration);\n\n return () => {\n clearTimeout(timer);\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n };\n }\n }, [currentStance, previousStance, duration, showProgress, transitionDuration]);\n\n const stance = useMemo(\n () => TRIGRAM_STANCES_ORDER[currentStance] ?? TrigramStance.GEON,\n [currentStance]\n );\n\n const elementalColor = useMemo(\n () => getTrigramElementColor(stance),\n [stance]\n );\n\n const koreanName = useMemo(\n () => getTrigramKoreanName(stance),\n [stance]\n );\n\n const englishName = useMemo(\n () => getTrigramEnglishName(stance),\n [stance]\n );\n\n const trigramSymbol = useMemo(\n () => getTrigramSymbol(stance),\n [stance]\n );\n\n const animationStyles = useMemo(() => (\n <style>\n {`\n @keyframes fadeInOut {\n 0% {\n opacity: 0;\n transform: translateY(-20px);\n }\n 10% {\n opacity: 1;\n transform: translateY(0);\n }\n 90% {\n opacity: 1;\n transform: translateY(0);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px);\n }\n }\n `}\n </style>\n ), []);\n\n if (!showIndicator) return null;\n\n const fontSize = isMobile ? 24 : 36;\n const subFontSize = isMobile ? 14 : 18;\n const top = isMobile ? \"30%\" : \"20%\";\n\n const primaryColorHex = hexToRgbaString(elementalColor, 1);\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"stance-change-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Stance changed to ${englishName}`}\n style={{\n position: \"absolute\",\n top,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n textAlign: \"center\",\n animation: \"fadeInOut 1s\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n >\n {/* Main stance display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: primaryColorHex,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n textShadow: `0 0 20px ${hexToRgbaString(elementalColor, 0.8)}, \n 0 0 40px ${hexToRgbaString(elementalColor, 0.5)}`,\n marginBottom: \"8px\",\n }}\n >\n {koreanName} {trigramSymbol}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6)}`,\n marginBottom: showProgress ? \"12px\" : \"0\",\n }}\n >\n {englishName}\n </div>\n\n {/* Transition progress bar (600ms) */}\n {showProgress && (\n <div\n data-testid=\"stance-transition-progress\"\n style={{\n width: \"200px\",\n margin: \"0 auto\",\n padding: \"8px 0\",\n }}\n >\n {/* Progress label */}\n <div\n style={{\n fontSize: \"11px\",\n color: \"rgba(255, 255, 255, 0.7)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n letterSpacing: \"1px\",\n }}\n >\n 팔괘전환 | Transition\n </div>\n \n {/* Progress bar container */}\n <div\n style={{\n width: \"100%\",\n height: \"6px\",\n backgroundColor: \"rgba(0, 0, 0, 0.6)\",\n borderRadius: \"3px\",\n overflow: \"hidden\",\n border: `1px solid ${hexToRgbaString(elementalColor, 0.3)}`,\n }}\n >\n {/* Progress bar fill */}\n <div\n style={{\n width: `${progress}%`,\n height: \"100%\",\n backgroundColor: primaryColorHex,\n transition: \"width 0.05s linear\",\n boxShadow: `0 0 8px ${primaryColorHex}`,\n }}\n />\n </div>\n \n {/* Time remaining */}\n <div\n style={{\n fontSize: \"10px\",\n color: \"rgba(255, 255, 255, 0.5)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginTop: \"4px\",\n }}\n >\n {Math.max(0, Math.ceil(transitionDuration * (1 - progress / 100)))}ms\n </div>\n </div>\n )}\n\n {/* CSS Animation - Memoized to prevent redefinition */}\n {animationStyles}\n </div>\n </Html>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,yBAA+D,EAC1E,eACA,gBACA,WAAW,OACX,WAAW,KACX,eAAe,MACf,qBAAqB,UACjB;CACJ,MAAM,CAAC,eAAe,oBAAoB,SAAS,KAAK;CACxD,MAAM,CAAC,UAAU,eAAe,SAAS,CAAC;CAC1C,MAAM,eAAe,OAAO,IAAI;CAChC,MAAM,eAAe,OAAe,CAAC;CACrC,MAAM,oBAAoB,OAA2B,KAAA,CAAS;CAE9D,gBAAgB;EACd,aAAa,UAAU;EACvB,aAAa;GACX,aAAa,UAAU;EACzB;CACF,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,IAAI,kBAAkB,YAAY,KAAA,GAAW;GAC3C,qBAAqB,kBAAkB,OAAO;GAC9C,kBAAkB,UAAU,KAAA;EAC9B;EAEA,IAAI,kBAAkB,gBAAgB;GACpC,iBAAiB,IAAI;GACrB,YAAY,CAAC;GACb,aAAa,UAAU;GAEvB,0BAA0B;GAE1B,IAAI,cAAc;IAChB,MAAM,WAAW,cAAsB;KACrC,IAAI,CAAC,aAAa,SAAS;KAE3B,IAAI,aAAa,YAAY,GAC3B,aAAa,UAAU;KAGzB,MAAM,UAAU,YAAY,aAAa;KACzC,MAAM,cAAc,KAAK,IAAK,UAAU,qBAAsB,KAAK,GAAG;KAEtE,YAAY,WAAW;KAEvB,IAAI,cAAc,KAChB,kBAAkB,UAAU,sBAAsB,OAAO;IAE7D;IAEA,kBAAkB,UAAU,sBAAsB,OAAO;GAC3D;GAEA,MAAM,QAAQ,iBAAiB;IAC7B,IAAI,aAAa,SACf,iBAAiB,KAAK;GAE1B,GAAG,QAAQ;GAEX,aAAa;IACX,aAAa,KAAK;IAClB,IAAI,kBAAkB,SACpB,qBAAqB,kBAAkB,OAAO;GAElD;EACF;CACF,GAAG;EAAC;EAAe;EAAgB;EAAU;EAAc;CAAkB,CAAC;CAE9E,MAAM,SAAS,cACP,sBAAsB,kBAAkB,cAAc,MAC5D,CAAC,aAAa,CAChB;CAEA,MAAM,iBAAiB,cACf,uBAAuB,MAAM,GACnC,CAAC,MAAM,CACT;CAEA,MAAM,aAAa,cACX,qBAAqB,MAAM,GACjC,CAAC,MAAM,CACT;CAEA,MAAM,cAAc,cACZ,sBAAsB,MAAM,GAClC,CAAC,MAAM,CACT;CAEA,MAAM,gBAAgB,cACd,iBAAiB,MAAM,GAC7B,CAAC,MAAM,CACT;CAEA,MAAM,kBAAkB,cACtB,oBAAC,SAAD,EAAA,UACG;;;;;;;;;;;;;;;;;;;QAoBI,CAAA,GACN,CAAC,CAAC;CAEL,IAAI,CAAC,eAAe,OAAO;CAE3B,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,cAAc,WAAW,KAAK;CACpC,MAAM,MAAM,WAAW,QAAQ;CAE/B,MAAM,kBAAkB,gBAAgB,gBAAgB,CAAC;CAEzD,OACE,oBAAC,MAAD;EAAM,YAAA;YACJ,qBAAC,OAAD;GACE,eAAY;GACZ,MAAK;GACL,aAAU;GACV,cAAY,qBAAqB;GACjC,OAAO;IACL,UAAU;IACV;IACA,MAAM;IACN,WAAW;IACX,WAAW;IACX,WAAW;IACX,eAAe;IACf,QAAQ;GACV;aAdF;IAiBE,qBAAC,OAAD;KACE,OAAO;MACL,UAAU,GAAG,SAAS;MACtB,OAAO;MACP,YAAY,YAAY;MACxB,YAAY;MACZ,YAAY,YAAY,gBAAgB,gBAAgB,EAAG,EAAE;mCACtC,gBAAgB,gBAAgB,EAAG;MAC1D,cAAc;KAChB;eATF;MAWG;MAAW;MAAE;KACX;;IAGL,oBAAC,OAAD;KACE,OAAO;MACL,UAAU,GAAG,YAAY;MACzB,OAAO,gBAAgB,cAAc,aAAa,CAAC;MACnD,YAAY,YAAY;MACxB,YAAY,YAAY,gBAAgB,cAAc,aAAa,EAAG;MACtE,cAAc,eAAe,SAAS;KACxC;eAEC;IACE,CAAA;IAGJ,gBACC,qBAAC,OAAD;KACE,eAAY;KACZ,OAAO;MACL,OAAO;MACP,QAAQ;MACR,SAAS;KACX;eANF;MASE,oBAAC,OAAD;OACE,OAAO;QACL,UAAU;QACV,OAAO;QACP,YAAY,YAAY;QACxB,cAAc;QACd,eAAe;OACjB;iBACD;MAEI,CAAA;MAGL,oBAAC,OAAD;OACE,OAAO;QACL,OAAO;QACP,QAAQ;QACR,iBAAiB;QACjB,cAAc;QACd,UAAU;QACV,QAAQ,aAAa,gBAAgB,gBAAgB,EAAG;OAC1D;iBAGA,oBAAC,OAAD,EACE,OAAO;QACL,OAAO,GAAG,SAAS;QACnB,QAAQ;QACR,iBAAiB;QACjB,YAAY;QACZ,WAAW,WAAW;OACxB,EACD,CAAA;MACE,CAAA;MAGL,qBAAC,OAAD;OACE,OAAO;QACL,UAAU;QACV,OAAO;QACP,YAAY,YAAY;QACxB,WAAW;OACb;iBANF,CAQG,KAAK,IAAI,GAAG,KAAK,KAAK,sBAAsB,IAAI,WAAW,IAAI,CAAC,GAAE,IAChE;;KACF;;IAIN;GACE;;CACD,CAAA;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Player3DWithTransitions.js","names":[],"sources":["../../../../../src/components/shared/three/models/Player3DWithTransitions.tsx"],"sourcesContent":["/**\n * Enhanced Player3D component with stance transition animations\n *\n * Demonstrates integration of stance change visual effects:\n * - StanceSymbol3D for floating trigram symbol\n * - StanceTransitionEffect for smooth transitions\n *\n * This wrapper can be used to enhance SkeletalPlayer3D with automatic\n * stance change detection and visual effects.\n *\n * @module components/three/Player3DWithTransitions\n * @category 3D Components\n * @korean 자세전환플레이어3D\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport StanceSymbol3D from \"../effects/StanceSymbol3D\";\nimport StanceTransitionEffect from \"../effects/StanceTransitionEffect\";\nimport { SkeletalPlayer3D } from \"./SkeletalPlayer3D\";\n\n/**\n * Props for Player3DWithTransitions component\n */\nexport interface Player3DWithTransitionsProps extends Player3DUnifiedProps {\n /** Specific attack animation name (for attack state) */\n readonly attackAnimation?: string;\n /** Enable stance transition effects (default: true) */\n readonly enableTransitionEffects?: boolean;\n /** Enable floating stance symbol (default: true) */\n readonly enableStanceSymbol?: boolean;\n /** Enable stance change audio (default: true) */\n readonly enableStanceAudio?: boolean;\n /** Transition duration in seconds (default: 0.5) */\n readonly transitionDuration?: number;\n /** Callback when stance transition starts */\n readonly onStanceTransitionStart?: (\n fromStance: TrigramStance,\n toStance: TrigramStance\n ) => void;\n /** Callback when stance transition completes */\n readonly onStanceTransitionComplete?: (stance: TrigramStance) => void;\n}\n\n/**\n * Audio asset IDs for stance transitions\n */\nconst AUDIO_ASSETS = {\n STANCE_CHANGE: \"stance_change\",\n} as const;\n\n/**\n * Player3DWithTransitions Component\n *\n * Enhanced player component with automatic stance change detection and visual effects.\n * Wraps SkeletalPlayer3D and adds:\n * - Floating trigram symbol\n * - Smooth transition effects\n * - Audio synchronization\n *\n * Performance optimized:\n * - Effects can be individually disabled for mobile\n * - Uses stance change detection to minimize updates\n * - Reuses components efficiently\n *\n * @example\n * ```tsx\n * <Player3DWithTransitions\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={currentStance}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * pain={20}\n * balance=\"READY\"\n * consciousness={100}\n * bloodLoss={0}\n * currentAnimation=\"idle\"\n * isMobile={false}\n * enableTransitionEffects={true}\n * enableStanceSymbol={true}\n * onStanceTransitionComplete={(stance) => console.log('Transitioned to:', stance)}\n * />\n * ```\n */\nexport const Player3DWithTransitions: React.FC<\n Player3DWithTransitionsProps\n> = ({\n stance,\n ki,\n isMobile = false,\n attackAnimation,\n enableTransitionEffects = true,\n enableStanceSymbol = true,\n enableStanceAudio = true,\n transitionDuration = 0.5,\n onStanceTransitionStart,\n onStanceTransitionComplete,\n ...playerProps\n}) => {\n const audio = useAudio();\n const prevStanceRef = useRef<TrigramStance>(stance);\n const [isTransitioning, setIsTransitioning] = useState(false);\n const [fromStance, setFromStance] = useState<TrigramStance>(stance);\n\n\n useEffect(() => {\n const previousStance = prevStanceRef.current;\n\n if (previousStance !== stance) {\n prevStanceRef.current = stance;\n\n setIsTransitioning(true);\n setFromStance(previousStance);\n onStanceTransitionStart?.(previousStance, stance);\n\n if (enableStanceAudio) {\n audio.playSFX(AUDIO_ASSETS.STANCE_CHANGE);\n }\n }\n }, [stance, audio, enableStanceAudio, onStanceTransitionStart]);\n\n const handleTransitionComplete = useCallback(() => {\n setIsTransitioning(false);\n onStanceTransitionComplete?.(stance);\n }, [stance, onStanceTransitionComplete]);\n\n return (\n <group name=\"player3d-with-transitions\">\n {/* Base player model */}\n <SkeletalPlayer3D\n stance={stance}\n ki={ki}\n isMobile={isMobile}\n attackAnimation={attackAnimation}\n {...playerProps}\n />\n\n {/* Floating stance symbol */}\n {enableStanceSymbol && (\n <StanceSymbol3D\n stance={stance}\n heightOffset={2.5}\n animated={true}\n scale={isMobile ? 0.8 : 1.0} // Smaller on mobile\n showName={!isMobile} // Hide Korean name on mobile for clarity\n />\n )}\n\n {/* Stance transition effect */}\n {enableTransitionEffects && isTransitioning && (\n <StanceTransitionEffect\n fromStance={fromStance}\n toStance={stance}\n onTransitionComplete={handleTransitionComplete}\n duration={transitionDuration}\n showNameOverlay={!isMobile} // Hide overlay on mobile\n />\n )}\n </group>\n );\n};\n\nexport default Player3DWithTransitions;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAM,eAAe,EACnB,eAAe,
|
|
1
|
+
{"version":3,"file":"Player3DWithTransitions.js","names":[],"sources":["../../../../../src/components/shared/three/models/Player3DWithTransitions.tsx"],"sourcesContent":["/**\n * Enhanced Player3D component with stance transition animations\n *\n * Demonstrates integration of stance change visual effects:\n * - StanceSymbol3D for floating trigram symbol\n * - StanceTransitionEffect for smooth transitions\n *\n * This wrapper can be used to enhance SkeletalPlayer3D with automatic\n * stance change detection and visual effects.\n *\n * @module components/three/Player3DWithTransitions\n * @category 3D Components\n * @korean 자세전환플레이어3D\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport StanceSymbol3D from \"../effects/StanceSymbol3D\";\nimport StanceTransitionEffect from \"../effects/StanceTransitionEffect\";\nimport { SkeletalPlayer3D } from \"./SkeletalPlayer3D\";\n\n/**\n * Props for Player3DWithTransitions component\n */\nexport interface Player3DWithTransitionsProps extends Player3DUnifiedProps {\n /** Specific attack animation name (for attack state) */\n readonly attackAnimation?: string;\n /** Enable stance transition effects (default: true) */\n readonly enableTransitionEffects?: boolean;\n /** Enable floating stance symbol (default: true) */\n readonly enableStanceSymbol?: boolean;\n /** Enable stance change audio (default: true) */\n readonly enableStanceAudio?: boolean;\n /** Transition duration in seconds (default: 0.5) */\n readonly transitionDuration?: number;\n /** Callback when stance transition starts */\n readonly onStanceTransitionStart?: (\n fromStance: TrigramStance,\n toStance: TrigramStance\n ) => void;\n /** Callback when stance transition completes */\n readonly onStanceTransitionComplete?: (stance: TrigramStance) => void;\n}\n\n/**\n * Audio asset IDs for stance transitions\n */\nconst AUDIO_ASSETS = {\n STANCE_CHANGE: \"stance_change\",\n} as const;\n\n/**\n * Player3DWithTransitions Component\n *\n * Enhanced player component with automatic stance change detection and visual effects.\n * Wraps SkeletalPlayer3D and adds:\n * - Floating trigram symbol\n * - Smooth transition effects\n * - Audio synchronization\n *\n * Performance optimized:\n * - Effects can be individually disabled for mobile\n * - Uses stance change detection to minimize updates\n * - Reuses components efficiently\n *\n * @example\n * ```tsx\n * <Player3DWithTransitions\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={currentStance}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * pain={20}\n * balance=\"READY\"\n * consciousness={100}\n * bloodLoss={0}\n * currentAnimation=\"idle\"\n * isMobile={false}\n * enableTransitionEffects={true}\n * enableStanceSymbol={true}\n * onStanceTransitionComplete={(stance) => console.log('Transitioned to:', stance)}\n * />\n * ```\n */\nexport const Player3DWithTransitions: React.FC<\n Player3DWithTransitionsProps\n> = ({\n stance,\n ki,\n isMobile = false,\n attackAnimation,\n enableTransitionEffects = true,\n enableStanceSymbol = true,\n enableStanceAudio = true,\n transitionDuration = 0.5,\n onStanceTransitionStart,\n onStanceTransitionComplete,\n ...playerProps\n}) => {\n const audio = useAudio();\n const prevStanceRef = useRef<TrigramStance>(stance);\n const [isTransitioning, setIsTransitioning] = useState(false);\n const [fromStance, setFromStance] = useState<TrigramStance>(stance);\n\n\n useEffect(() => {\n const previousStance = prevStanceRef.current;\n\n if (previousStance !== stance) {\n prevStanceRef.current = stance;\n\n setIsTransitioning(true);\n setFromStance(previousStance);\n onStanceTransitionStart?.(previousStance, stance);\n\n if (enableStanceAudio) {\n audio.playSFX(AUDIO_ASSETS.STANCE_CHANGE);\n }\n }\n }, [stance, audio, enableStanceAudio, onStanceTransitionStart]);\n\n const handleTransitionComplete = useCallback(() => {\n setIsTransitioning(false);\n onStanceTransitionComplete?.(stance);\n }, [stance, onStanceTransitionComplete]);\n\n return (\n <group name=\"player3d-with-transitions\">\n {/* Base player model */}\n <SkeletalPlayer3D\n stance={stance}\n ki={ki}\n isMobile={isMobile}\n attackAnimation={attackAnimation}\n {...playerProps}\n />\n\n {/* Floating stance symbol */}\n {enableStanceSymbol && (\n <StanceSymbol3D\n stance={stance}\n heightOffset={2.5}\n animated={true}\n scale={isMobile ? 0.8 : 1.0} // Smaller on mobile\n showName={!isMobile} // Hide Korean name on mobile for clarity\n />\n )}\n\n {/* Stance transition effect */}\n {enableTransitionEffects && isTransitioning && (\n <StanceTransitionEffect\n fromStance={fromStance}\n toStance={stance}\n onTransitionComplete={handleTransitionComplete}\n duration={transitionDuration}\n showNameOverlay={!isMobile} // Hide overlay on mobile\n />\n )}\n </group>\n );\n};\n\nexport default Player3DWithTransitions;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAM,eAAe,EACnB,eAAe,gBACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAa,2BAER,EACH,QACA,IACA,WAAW,OACX,iBACA,0BAA0B,MAC1B,qBAAqB,MACrB,oBAAoB,MACpB,qBAAqB,IACrB,yBACA,4BACA,GAAG,kBACC;CACJ,MAAM,QAAQ,SAAS;CACvB,MAAM,gBAAgB,OAAsB,MAAM;CAClD,MAAM,CAAC,iBAAiB,sBAAsB,SAAS,KAAK;CAC5D,MAAM,CAAC,YAAY,iBAAiB,SAAwB,MAAM;CAGlE,gBAAgB;EACd,MAAM,iBAAiB,cAAc;EAErC,IAAI,mBAAmB,QAAQ;GAC7B,cAAc,UAAU;GAExB,mBAAmB,IAAI;GACvB,cAAc,cAAc;GAC5B,0BAA0B,gBAAgB,MAAM;GAEhD,IAAI,mBACF,MAAM,QAAQ,aAAa,aAAa;EAE5C;CACF,GAAG;EAAC;EAAQ;EAAO;EAAmB;CAAuB,CAAC;CAE9D,MAAM,2BAA2B,kBAAkB;EACjD,mBAAmB,KAAK;EACxB,6BAA6B,MAAM;CACrC,GAAG,CAAC,QAAQ,0BAA0B,CAAC;CAEvC,OACE,qBAAC,SAAD;EAAO,MAAK;YAAZ;GAEE,oBAAC,kBAAD;IACU;IACJ;IACM;IACO;IACjB,GAAI;GACL,CAAA;GAGA,sBACC,oBAAC,gBAAD;IACU;IACR,cAAc;IACd,UAAU;IACV,OAAO,WAAW,KAAM;IACxB,UAAU,CAAC;GACZ,CAAA;GAIF,2BAA2B,mBAC1B,oBAAC,wBAAD;IACc;IACZ,UAAU;IACV,sBAAsB;IACtB,UAAU;IACV,iBAAiB,CAAC;GACnB,CAAA;EAEE;;AAEX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SkeletalPlayer3D.d.ts","sourceRoot":"","sources":["../../../../../src/components/shared/three/models/SkeletalPlayer3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAA+C,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SkeletalPlayer3D.d.ts","sourceRoot":"","sources":["../../../../../src/components/shared/three/models/SkeletalPlayer3D.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAA+C,MAAM,OAAO,CAAC;AAmBpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAiD5E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CACrC,oBAAoB,GAAG;IACrB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC,CA8XF,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MAX_VISUAL_FRAME_DELTA_SECONDS } from "../../../../systems/animation/core/types.js";
|
|
1
2
|
import { KOREAN_COLORS } from "../../../../types/constants/colors.js";
|
|
2
3
|
import { FONT_FAMILY } from "../../../../types/constants/typography.js";
|
|
3
4
|
import { FacialExpression } from "../../../../types/facial.js";
|
|
@@ -180,6 +181,7 @@ var SkeletalPlayer3D = ({ playerId, archetype, stance, position, rotation, healt
|
|
|
180
181
|
}, [opponentPosition, facing]);
|
|
181
182
|
const frameCounter = useRef(0);
|
|
182
183
|
useFrame((_state, delta) => {
|
|
184
|
+
const safeDelta = Math.min(delta, MAX_VISUAL_FRAME_DELTA_SECONDS);
|
|
183
185
|
const isWalkingAnimation = currentAnimation === "walk" || typeof currentAnimation === "string" && currentAnimation.startsWith("step_");
|
|
184
186
|
if (bodyFacing && opponentPosition && onBodyFacingUpdate && !isWalkingAnimation) {
|
|
185
187
|
const playerPos = {
|
|
@@ -196,14 +198,14 @@ var SkeletalPlayer3D = ({ playerId, archetype, stance, position, rotation, healt
|
|
|
196
198
|
let updatedFacing = bodyFacing;
|
|
197
199
|
if (shouldLock && !bodyFacing.isLocked) updatedFacing = lockFacing(bodyFacing);
|
|
198
200
|
else if (!shouldLock && bodyFacing.isLocked) updatedFacing = unlockFacing(bodyFacing);
|
|
199
|
-
if (!updatedFacing.isLocked) updatedFacing = updateFacingTowardOpponent(updatedFacing, playerPos, opponentPos,
|
|
201
|
+
if (!updatedFacing.isLocked) updatedFacing = updateFacingTowardOpponent(updatedFacing, playerPos, opponentPos, safeDelta, Date.now());
|
|
200
202
|
if (updatedFacing !== bodyFacing) onBodyFacingUpdate(updatedFacing);
|
|
201
203
|
}
|
|
202
204
|
frameCounter.current = (frameCounter.current + 1) % 10;
|
|
203
|
-
updateRigAnimation(rig,
|
|
204
|
-
updateHandAnimations(
|
|
205
|
-
updateBalanceAnimations(
|
|
206
|
-
updateMuscleActivations(
|
|
205
|
+
updateRigAnimation(rig, safeDelta);
|
|
206
|
+
updateHandAnimations(safeDelta);
|
|
207
|
+
updateBalanceAnimations(safeDelta, frameCounter.current);
|
|
208
|
+
updateMuscleActivations(safeDelta, frameCounter.current);
|
|
207
209
|
if (bodyFacing) {
|
|
208
210
|
const head = rig.bones.get("head");
|
|
209
211
|
if (head) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SkeletalPlayer3D.js","names":[],"sources":["../../../../../src/components/shared/three/models/SkeletalPlayer3D.tsx"],"sourcesContent":["/**\n * SkeletalPlayer3D component with articulated body model\n *\n * Implements full skeletal rigging system for realistic fighter animations\n * with independent limb movement, elbow/knee joints, and attack animations.\n *\n * @module components/three/SkeletalPlayer3D\n * @category 3D Components\n * @korean 골격플레이어3D컴포넌트\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport { useBalanceAnimations } from \"../../../../hooks/useBalanceAnimations\";\nimport { useHandPoseTransitions } from \"../../../../hooks/useHandPoseTransitions\";\nimport { useMuscleActivation } from \"../../../../hooks/useMuscleActivation\";\nimport { useSkeletalAnimation } from \"../../../../hooks/useSkeletalAnimation\";\nimport {\n createDefaultFacialDamage,\n createScaledHumanoidRig,\n getExpressionFromCombatState,\n getHeadAngleRadians,\n lockFacing,\n unlockFacing,\n updateFacingTowardOpponent,\n} from \"../../../../systems/animation\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { FacialExpression } from \"../../../../types/facial\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport type { SkeletalRig } from \"../../../../types/skeletal\";\nimport { toHexColor } from \"../../../../utils/colorHelpers\";\nimport { getArchetypeSkinTone } from \"../../../../utils/colorUtils\";\nimport BoneRenderer from \"../anatomy/BoneRenderer\";\nimport PlayerStateIndicators from \"../effects/PlayerStateIndicators\";\n\n/**\n * Get stance-specific color from Korean theming\n *\n * @param stance - Current trigram stance\n * @returns Hex color number\n * @korean 자세색상가져오기\n */\nconst getStanceColor = (stance: string): number => {\n const stanceColors: Record<string, number> = {\n geon: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY,\n tae: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY,\n li: KOREAN_COLORS.TRIGRAM_LI_PRIMARY,\n jin: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY,\n son: KOREAN_COLORS.TRIGRAM_SON_PRIMARY,\n gam: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY,\n gan: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY,\n gon: KOREAN_COLORS.TRIGRAM_GON_PRIMARY,\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * Get trigram symbol for stance\n *\n * @param stance - Current trigram stance\n * @returns Unicode trigram symbol\n * @korean 팔괘기호가져오기\n */\nconst getTrigramSymbol = (stance: string): string => {\n const symbols: Record<string, string> = {\n geon: \"☰\",\n tae: \"☱\",\n li: \"☲\",\n jin: \"☳\",\n son: \"☴\",\n gam: \"☵\",\n gan: \"☶\",\n gon: \"☷\",\n };\n return symbols[stance] ?? \"☰\";\n};\n\n/**\n * SkeletalPlayer3D Component\n *\n * Complete skeletal player with 28-bone rig and realistic animations.\n * Supports all Korean martial arts attack animations (jab, cross, kicks, block).\n *\n * @example\n * ```tsx\n * <SkeletalPlayer3D\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={TrigramStance.GEON}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * currentAnimation=\"attack\"\n * attackAnimation=\"jab\"\n * showDetails={true}\n * />\n * ```\n *\n * @korean 골격플레이어3D컴포넌트\n */\nexport const SkeletalPlayer3D: React.FC<\n Player3DUnifiedProps & {\n readonly attackAnimation?: string;\n readonly showSkeleton?: boolean;\n }\n> = ({\n playerId,\n archetype,\n stance,\n position,\n rotation,\n health,\n maxHealth,\n stamina,\n ki,\n pain,\n balance,\n consciousness,\n bloodLoss,\n isBlocking,\n isStunned = false,\n isCountering = false,\n currentAnimation,\n isMobile,\n name,\n scale = 1,\n showDetails = true,\n facing = \"right\",\n showStanceIndicator = true,\n onAnimationComplete,\n attackAnimation,\n showSkeleton = false,\n facialExpression,\n facialDamage,\n enableFacialExpressions = false,\n enableEyeTracking = true,\n opponentPosition,\n bodyFacing,\n onBodyFacingUpdate,\n laterality, // Stance laterality (left/right foot forward)\n}) => {\n const effectiveLaterality = laterality ?? \"right\";\n const physicalAttributes = useMemo(\n () => getArchetypePhysicalAttributes(archetype),\n [archetype],\n );\n\n const rig = useMemo<SkeletalRig>(\n () => createScaledHumanoidRig(physicalAttributes),\n [physicalAttributes],\n );\n\n\n const { updateRigAnimation, diagonalRotationY } = useSkeletalAnimation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stance,\n laterality: effectiveLaterality, // Pass laterality for animation mirroring\n onAnimationComplete,\n });\n\n const { leftHandState, rightHandState, updateHandAnimations } =\n useHandPoseTransitions({\n currentAnimation,\n attackAnimation,\n isBlocking,\n });\n\n\n const { swayPosition, helplessRotation, updateBalanceAnimations } =\n useBalanceAnimations({\n balance,\n });\n\n const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stamina,\n });\n\n const skinTone = useMemo(() => getArchetypeSkinTone(archetype), [archetype]);\n\n const bodyColor = useMemo(() => {\n if (isStunned) return KOREAN_COLORS.WARNING_YELLOW;\n if (health / maxHealth < 0.3) return KOREAN_COLORS.ACCENT_RED;\n if (ki / 100 > 0.8) return KOREAN_COLORS.PRIMARY_CYAN;\n return skinTone; // Use archetype skin tone instead of primary color\n }, [isStunned, health, maxHealth, ki, skinTone]);\n\n const stanceColor = useMemo(() => getStanceColor(stance), [stance]);\n const trigramSymbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n\n const [justHit, setJustHit] = useState(false);\n const [justLanded, setJustLanded] = useState(false);\n const lastHealthRef = useRef(health);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (health < lastHealthRef.current) {\n setJustHit(true);\n timeoutId = setTimeout(() => setJustHit(false), 1000); // Clear after 1 second\n }\n\n lastHealthRef.current = health;\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [health]);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (currentAnimation === \"attack\") {\n setJustLanded(true);\n timeoutId = setTimeout(() => setJustLanded(false), 500); // Clear after 0.5 seconds\n }\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [currentAnimation]);\n\n const calculatedExpression = useMemo(() => {\n if (!enableFacialExpressions) {\n return FacialExpression.NEUTRAL;\n }\n\n if (facialExpression) {\n return facialExpression;\n }\n\n return getExpressionFromCombatState(\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n );\n }, [\n enableFacialExpressions,\n facialExpression,\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n ]);\n\n const calculatedFacialDamage = useMemo(() => {\n return facialDamage ?? createDefaultFacialDamage();\n }, [facialDamage]);\n\n const opponentPos = useMemo(() => {\n if (opponentPosition) {\n return new THREE.Vector3(...opponentPosition);\n }\n return new THREE.Vector3(facing === \"right\" ? 5 : -5, 2, 0);\n }, [opponentPosition, facing]);\n\n\n const frameCounter = useRef(0);\n\n useFrame((_state, delta) => {\n const isWalkingAnimation =\n currentAnimation === \"walk\" ||\n (typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\"));\n\n if (\n bodyFacing &&\n opponentPosition &&\n onBodyFacingUpdate &&\n !isWalkingAnimation\n ) {\n const playerPos = { x: position[0], y: position[2] }; // X and Z for 2D top-down\n const opponentPos = { x: opponentPosition[0], y: opponentPosition[2] };\n\n const isStepAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\");\n const isTurnAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"turn_\");\n\n const shouldLock =\n currentAnimation === \"attack\" ||\n currentAnimation === \"defend\" ||\n isStepAnimation ||\n isTurnAnimation;\n\n let updatedFacing = bodyFacing;\n\n if (shouldLock && !bodyFacing.isLocked) {\n updatedFacing = lockFacing(bodyFacing);\n } else if (!shouldLock && bodyFacing.isLocked) {\n updatedFacing = unlockFacing(bodyFacing);\n }\n\n if (!updatedFacing.isLocked) {\n updatedFacing = updateFacingTowardOpponent(\n updatedFacing,\n playerPos,\n opponentPos,\n delta,\n Date.now(),\n );\n }\n\n if (updatedFacing !== bodyFacing) {\n onBodyFacingUpdate(updatedFacing);\n }\n }\n\n\n frameCounter.current = (frameCounter.current + 1) % 10;\n\n updateRigAnimation(rig, delta);\n\n updateHandAnimations(delta);\n\n updateBalanceAnimations(delta, frameCounter.current);\n\n updateMuscleActivations(delta, frameCounter.current);\n\n if (bodyFacing) {\n const head = rig.bones.get(\"head\");\n if (head) {\n const headRotation = getHeadAngleRadians(bodyFacing);\n head.rotation.y = headRotation;\n }\n }\n });\n\n const effectiveRotation = diagonalRotationY ?? rotation;\n\n return (\n <group\n position={position}\n rotation={[0, effectiveRotation, 0]}\n scale={[facing === \"left\" ? -scale : scale, scale, scale]}\n name={`skeletal-player3d-${playerId}`}\n >\n {/* Inner group for sway animation and helpless lean */}\n <group position={swayPosition} rotation={[helplessRotation, 0, 0]}>\n {/* Skeletal rig rendering with bone-attached muscles */}\n <BoneRenderer\n rig={rig}\n color={bodyColor}\n showBones={true}\n renderMode={showSkeleton ? \"debug\" : \"solid\"}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={10}\n facialExpression={calculatedExpression}\n facialDamage={calculatedFacialDamage}\n opponentPosition={opponentPos}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth,\n torsoLength: physicalAttributes.torsoLength,\n armLength: physicalAttributes.armLength,\n legLength: physicalAttributes.legLength,\n }}\n muscleStates={muscleStates}\n isExhausted={stamina < 20}\n archetype={archetype}\n />\n\n {/* Clothing is now rendered via BoneClothing inside BoneRenderer */}\n {/* This ensures clothing inherits bone transforms automatically */}\n\n {/* Blocking shield effect */}\n {isBlocking && (\n <mesh position={[0, 1.2, 0.3]}>\n <circleGeometry args={[0.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_BLUE}\n transparent\n opacity={0.5}\n side={2}\n />\n </mesh>\n )}\n\n {/* Counter indicator */}\n {isCountering && (\n <mesh position={[0, 1.5, 0]}>\n <torusGeometry args={[0.4, 0.05, 8, 32]} />\n <meshBasicMaterial color={KOREAN_COLORS.ACCENT_PURPLE} />\n </mesh>\n )}\n\n {/* Player name overlay */}\n {showDetails && name && (\n <Html\n position={[0, 2.8, 0]}\n center\n distanceFactor={isMobile ? 15 : 10}\n occlude={false}\n style={{ pointerEvents: \"none\", userSelect: \"none\" }}\n >\n <div\n style={{\n fontFamily: FONT_FAMILY.KOREAN,\n textAlign: \"center\",\n color: \"white\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n >\n {/* Player name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n data-testid=\"player-name\"\n >\n {name.korean}\n </div>\n\n {/* Trigram symbol */}\n {showStanceIndicator && (\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: toHexColor(stanceColor),\n }}\n data-testid=\"trigram-symbol\"\n >\n {trigramSymbol}\n </div>\n )}\n\n {/* Combat state text */}\n {(isBlocking || isStunned || isCountering) && (\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontWeight: \"bold\",\n color: \"#ffff00\",\n marginTop: \"4px\",\n }}\n data-testid=\"combat-state\"\n >\n {isBlocking ? \"방어\" : isStunned ? \"기절\" : \"반격\"}\n </div>\n )}\n </div>\n </Html>\n )}\n\n {/* State indicators (health, stamina, Ki, balance) */}\n {showDetails && (\n <PlayerStateIndicators\n health={health}\n maxHealth={maxHealth}\n stamina={stamina}\n ki={ki}\n balance={balance}\n consciousness={consciousness}\n pain={pain}\n bloodLoss={bloodLoss}\n isMobile={isMobile}\n />\n )}\n </group>{\" \"}\n {/* Close inner sway group */}\n </group>\n );\n};\n\nexport default SkeletalPlayer3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SkeletalPlayer3D.js","names":[],"sources":["../../../../../src/components/shared/three/models/SkeletalPlayer3D.tsx"],"sourcesContent":["/**\n * SkeletalPlayer3D component with articulated body model\n *\n * Implements full skeletal rigging system for realistic fighter animations\n * with independent limb movement, elbow/knee joints, and attack animations.\n *\n * @module components/three/SkeletalPlayer3D\n * @category 3D Components\n * @korean 골격플레이어3D컴포넌트\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport { useBalanceAnimations } from \"../../../../hooks/useBalanceAnimations\";\nimport { useHandPoseTransitions } from \"../../../../hooks/useHandPoseTransitions\";\nimport { useMuscleActivation } from \"../../../../hooks/useMuscleActivation\";\nimport { useSkeletalAnimation } from \"../../../../hooks/useSkeletalAnimation\";\nimport {\n createDefaultFacialDamage,\n createScaledHumanoidRig,\n MAX_VISUAL_FRAME_DELTA_SECONDS,\n getExpressionFromCombatState,\n getHeadAngleRadians,\n lockFacing,\n unlockFacing,\n updateFacingTowardOpponent,\n} from \"../../../../systems/animation\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { FacialExpression } from \"../../../../types/facial\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport type { SkeletalRig } from \"../../../../types/skeletal\";\nimport { toHexColor } from \"../../../../utils/colorHelpers\";\nimport { getArchetypeSkinTone } from \"../../../../utils/colorUtils\";\nimport BoneRenderer from \"../anatomy/BoneRenderer\";\nimport PlayerStateIndicators from \"../effects/PlayerStateIndicators\";\n\n/**\n * Get stance-specific color from Korean theming\n *\n * @param stance - Current trigram stance\n * @returns Hex color number\n * @korean 자세색상가져오기\n */\nconst getStanceColor = (stance: string): number => {\n const stanceColors: Record<string, number> = {\n geon: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY,\n tae: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY,\n li: KOREAN_COLORS.TRIGRAM_LI_PRIMARY,\n jin: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY,\n son: KOREAN_COLORS.TRIGRAM_SON_PRIMARY,\n gam: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY,\n gan: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY,\n gon: KOREAN_COLORS.TRIGRAM_GON_PRIMARY,\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * Get trigram symbol for stance\n *\n * @param stance - Current trigram stance\n * @returns Unicode trigram symbol\n * @korean 팔괘기호가져오기\n */\nconst getTrigramSymbol = (stance: string): string => {\n const symbols: Record<string, string> = {\n geon: \"☰\",\n tae: \"☱\",\n li: \"☲\",\n jin: \"☳\",\n son: \"☴\",\n gam: \"☵\",\n gan: \"☶\",\n gon: \"☷\",\n };\n return symbols[stance] ?? \"☰\";\n};\n\n/**\n * SkeletalPlayer3D Component\n *\n * Complete skeletal player with 28-bone rig and realistic animations.\n * Supports all Korean martial arts attack animations (jab, cross, kicks, block).\n *\n * @example\n * ```tsx\n * <SkeletalPlayer3D\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={TrigramStance.GEON}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * currentAnimation=\"attack\"\n * attackAnimation=\"jab\"\n * showDetails={true}\n * />\n * ```\n *\n * @korean 골격플레이어3D컴포넌트\n */\nexport const SkeletalPlayer3D: React.FC<\n Player3DUnifiedProps & {\n readonly attackAnimation?: string;\n readonly showSkeleton?: boolean;\n }\n> = ({\n playerId,\n archetype,\n stance,\n position,\n rotation,\n health,\n maxHealth,\n stamina,\n ki,\n pain,\n balance,\n consciousness,\n bloodLoss,\n isBlocking,\n isStunned = false,\n isCountering = false,\n currentAnimation,\n isMobile,\n name,\n scale = 1,\n showDetails = true,\n facing = \"right\",\n showStanceIndicator = true,\n onAnimationComplete,\n attackAnimation,\n showSkeleton = false,\n facialExpression,\n facialDamage,\n enableFacialExpressions = false,\n enableEyeTracking = true,\n opponentPosition,\n bodyFacing,\n onBodyFacingUpdate,\n laterality, // Stance laterality (left/right foot forward)\n}) => {\n const effectiveLaterality = laterality ?? \"right\";\n const physicalAttributes = useMemo(\n () => getArchetypePhysicalAttributes(archetype),\n [archetype],\n );\n\n const rig = useMemo<SkeletalRig>(\n () => createScaledHumanoidRig(physicalAttributes),\n [physicalAttributes],\n );\n\n\n const { updateRigAnimation, diagonalRotationY } = useSkeletalAnimation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stance,\n laterality: effectiveLaterality, // Pass laterality for animation mirroring\n onAnimationComplete,\n });\n\n const { leftHandState, rightHandState, updateHandAnimations } =\n useHandPoseTransitions({\n currentAnimation,\n attackAnimation,\n isBlocking,\n });\n\n\n const { swayPosition, helplessRotation, updateBalanceAnimations } =\n useBalanceAnimations({\n balance,\n });\n\n const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stamina,\n });\n\n const skinTone = useMemo(() => getArchetypeSkinTone(archetype), [archetype]);\n\n const bodyColor = useMemo(() => {\n if (isStunned) return KOREAN_COLORS.WARNING_YELLOW;\n if (health / maxHealth < 0.3) return KOREAN_COLORS.ACCENT_RED;\n if (ki / 100 > 0.8) return KOREAN_COLORS.PRIMARY_CYAN;\n return skinTone; // Use archetype skin tone instead of primary color\n }, [isStunned, health, maxHealth, ki, skinTone]);\n\n const stanceColor = useMemo(() => getStanceColor(stance), [stance]);\n const trigramSymbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n\n const [justHit, setJustHit] = useState(false);\n const [justLanded, setJustLanded] = useState(false);\n const lastHealthRef = useRef(health);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (health < lastHealthRef.current) {\n setJustHit(true);\n timeoutId = setTimeout(() => setJustHit(false), 1000); // Clear after 1 second\n }\n\n lastHealthRef.current = health;\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [health]);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (currentAnimation === \"attack\") {\n setJustLanded(true);\n timeoutId = setTimeout(() => setJustLanded(false), 500); // Clear after 0.5 seconds\n }\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [currentAnimation]);\n\n const calculatedExpression = useMemo(() => {\n if (!enableFacialExpressions) {\n return FacialExpression.NEUTRAL;\n }\n\n if (facialExpression) {\n return facialExpression;\n }\n\n return getExpressionFromCombatState(\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n );\n }, [\n enableFacialExpressions,\n facialExpression,\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n ]);\n\n const calculatedFacialDamage = useMemo(() => {\n return facialDamage ?? createDefaultFacialDamage();\n }, [facialDamage]);\n\n const opponentPos = useMemo(() => {\n if (opponentPosition) {\n return new THREE.Vector3(...opponentPosition);\n }\n return new THREE.Vector3(facing === \"right\" ? 5 : -5, 2, 0);\n }, [opponentPosition, facing]);\n\n\n const frameCounter = useRef(0);\n\n useFrame((_state, delta) => {\n const safeDelta = Math.min(delta, MAX_VISUAL_FRAME_DELTA_SECONDS);\n const isWalkingAnimation =\n currentAnimation === \"walk\" ||\n (typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\"));\n\n if (\n bodyFacing &&\n opponentPosition &&\n onBodyFacingUpdate &&\n !isWalkingAnimation\n ) {\n const playerPos = { x: position[0], y: position[2] }; // X and Z for 2D top-down\n const opponentPos = { x: opponentPosition[0], y: opponentPosition[2] };\n\n const isStepAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\");\n const isTurnAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"turn_\");\n\n const shouldLock =\n currentAnimation === \"attack\" ||\n currentAnimation === \"defend\" ||\n isStepAnimation ||\n isTurnAnimation;\n\n let updatedFacing = bodyFacing;\n\n if (shouldLock && !bodyFacing.isLocked) {\n updatedFacing = lockFacing(bodyFacing);\n } else if (!shouldLock && bodyFacing.isLocked) {\n updatedFacing = unlockFacing(bodyFacing);\n }\n\n if (!updatedFacing.isLocked) {\n updatedFacing = updateFacingTowardOpponent(\n updatedFacing,\n playerPos,\n opponentPos,\n safeDelta,\n Date.now(),\n );\n }\n\n if (updatedFacing !== bodyFacing) {\n onBodyFacingUpdate(updatedFacing);\n }\n }\n\n\n frameCounter.current = (frameCounter.current + 1) % 10;\n\n updateRigAnimation(rig, safeDelta);\n\n updateHandAnimations(safeDelta);\n\n updateBalanceAnimations(safeDelta, frameCounter.current);\n\n updateMuscleActivations(safeDelta, frameCounter.current);\n\n if (bodyFacing) {\n const head = rig.bones.get(\"head\");\n if (head) {\n const headRotation = getHeadAngleRadians(bodyFacing);\n head.rotation.y = headRotation;\n }\n }\n });\n\n const effectiveRotation = diagonalRotationY ?? rotation;\n\n return (\n <group\n position={position}\n rotation={[0, effectiveRotation, 0]}\n scale={[facing === \"left\" ? -scale : scale, scale, scale]}\n name={`skeletal-player3d-${playerId}`}\n >\n {/* Inner group for sway animation and helpless lean */}\n <group position={swayPosition} rotation={[helplessRotation, 0, 0]}>\n {/* Skeletal rig rendering with bone-attached muscles */}\n <BoneRenderer\n rig={rig}\n color={bodyColor}\n showBones={true}\n renderMode={showSkeleton ? \"debug\" : \"solid\"}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={10}\n facialExpression={calculatedExpression}\n facialDamage={calculatedFacialDamage}\n opponentPosition={opponentPos}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth,\n torsoLength: physicalAttributes.torsoLength,\n armLength: physicalAttributes.armLength,\n legLength: physicalAttributes.legLength,\n }}\n muscleStates={muscleStates}\n isExhausted={stamina < 20}\n archetype={archetype}\n />\n\n {/* Clothing is now rendered via BoneClothing inside BoneRenderer */}\n {/* This ensures clothing inherits bone transforms automatically */}\n\n {/* Blocking shield effect */}\n {isBlocking && (\n <mesh position={[0, 1.2, 0.3]}>\n <circleGeometry args={[0.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_BLUE}\n transparent\n opacity={0.5}\n side={2}\n />\n </mesh>\n )}\n\n {/* Counter indicator */}\n {isCountering && (\n <mesh position={[0, 1.5, 0]}>\n <torusGeometry args={[0.4, 0.05, 8, 32]} />\n <meshBasicMaterial color={KOREAN_COLORS.ACCENT_PURPLE} />\n </mesh>\n )}\n\n {/* Player name overlay */}\n {showDetails && name && (\n <Html\n position={[0, 2.8, 0]}\n center\n distanceFactor={isMobile ? 15 : 10}\n occlude={false}\n style={{ pointerEvents: \"none\", userSelect: \"none\" }}\n >\n <div\n style={{\n fontFamily: FONT_FAMILY.KOREAN,\n textAlign: \"center\",\n color: \"white\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n >\n {/* Player name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n data-testid=\"player-name\"\n >\n {name.korean}\n </div>\n\n {/* Trigram symbol */}\n {showStanceIndicator && (\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: toHexColor(stanceColor),\n }}\n data-testid=\"trigram-symbol\"\n >\n {trigramSymbol}\n </div>\n )}\n\n {/* Combat state text */}\n {(isBlocking || isStunned || isCountering) && (\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontWeight: \"bold\",\n color: \"#ffff00\",\n marginTop: \"4px\",\n }}\n data-testid=\"combat-state\"\n >\n {isBlocking ? \"방어\" : isStunned ? \"기절\" : \"반격\"}\n </div>\n )}\n </div>\n </Html>\n )}\n\n {/* State indicators (health, stamina, Ki, balance) */}\n {showDetails && (\n <PlayerStateIndicators\n health={health}\n maxHealth={maxHealth}\n stamina={stamina}\n ki={ki}\n balance={balance}\n consciousness={consciousness}\n pain={pain}\n bloodLoss={bloodLoss}\n isMobile={isMobile}\n />\n )}\n </group>{\" \"}\n {/* Close inner sway group */}\n </group>\n );\n};\n\nexport default SkeletalPlayer3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAM,kBAAkB,WAA2B;CAWjD,OAAO;EATL,MAAM,cAAc;EACpB,KAAK,cAAc;EACnB,IAAI,cAAc;EAClB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,cAAc;CAEd,EAAa,WAAW,cAAc;AAC/C;;;;;;;;AASA,IAAM,oBAAoB,WAA2B;CAWnD,OAAO;EATL,MAAM;EACN,KAAK;EACL,IAAI;EACJ,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;CAEA,EAAQ,WAAW;AAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,oBAKR,EACH,UACA,WACA,QACA,UACA,UACA,QACA,WACA,SACA,IACA,MACA,SACA,eACA,WACA,YACA,YAAY,OACZ,eAAe,OACf,kBACA,UACA,MACA,QAAQ,GACR,cAAc,MACd,SAAS,SACT,sBAAsB,MACtB,qBACA,iBACA,eAAe,OACf,kBACA,cACA,0BAA0B,OAC1B,oBAAoB,MACpB,kBACA,YACA,oBACA,iBACI;CACJ,MAAM,sBAAsB,cAAc;CAC1C,MAAM,qBAAqB,cACnB,+BAA+B,SAAS,GAC9C,CAAC,SAAS,CACZ;CAEA,MAAM,MAAM,cACJ,wBAAwB,kBAAkB,GAChD,CAAC,kBAAkB,CACrB;CAGA,MAAM,EAAE,oBAAoB,sBAAsB,qBAAqB;EACrE;EACA;EACA;EACA;EACA,YAAY;EACZ;CACF,CAAC;CAED,MAAM,EAAE,eAAe,gBAAgB,yBACrC,uBAAuB;EACrB;EACA;EACA;CACF,CAAC;CAGH,MAAM,EAAE,cAAc,kBAAkB,4BACtC,qBAAqB,EACnB,QACF,CAAC;CAEH,MAAM,EAAE,cAAc,4BAA4B,oBAAoB;EACpE;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,WAAW,cAAc,qBAAqB,SAAS,GAAG,CAAC,SAAS,CAAC;CAE3E,MAAM,YAAY,cAAc;EAC9B,IAAI,WAAW,OAAO,cAAc;EACpC,IAAI,SAAS,YAAY,IAAK,OAAO,cAAc;EACnD,IAAI,KAAK,MAAM,IAAK,OAAO,cAAc;EACzC,OAAO;CACT,GAAG;EAAC;EAAW;EAAQ;EAAW;EAAI;CAAQ,CAAC;CAE/C,MAAM,cAAc,cAAc,eAAe,MAAM,GAAG,CAAC,MAAM,CAAC;CAClE,MAAM,gBAAgB,cAAc,iBAAiB,MAAM,GAAG,CAAC,MAAM,CAAC;CAEtE,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,CAAC,YAAY,iBAAiB,SAAS,KAAK;CAClD,MAAM,gBAAgB,OAAO,MAAM;CAEnC,gBAAgB;EACd,IAAI;EAEJ,IAAI,SAAS,cAAc,SAAS;GAClC,WAAW,IAAI;GACf,YAAY,iBAAiB,WAAW,KAAK,GAAG,GAAI;EACtD;EAEA,cAAc,UAAU;EAExB,aAAa;GACX,IAAI,cAAc,KAAA,GAChB,aAAa,SAAS;EAE1B;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACd,IAAI;EAEJ,IAAI,qBAAqB,UAAU;GACjC,cAAc,IAAI;GAClB,YAAY,iBAAiB,cAAc,KAAK,GAAG,GAAG;EACxD;EAEA,aAAa;GACX,IAAI,cAAc,KAAA,GAChB,aAAa,SAAS;EAE1B;CACF,GAAG,CAAC,gBAAgB,CAAC;CAErB,MAAM,uBAAuB,cAAc;EACzC,IAAI,CAAC,yBACH,OAAO,iBAAiB;EAG1B,IAAI,kBACF,OAAO;EAGT,OAAO,6BACL,QACA,WACA,SACA,MACA,eACA,SACA,UACF;CACF,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,yBAAyB,cAAc;EAC3C,OAAO,gBAAgB,0BAA0B;CACnD,GAAG,CAAC,YAAY,CAAC;CAEjB,MAAM,cAAc,cAAc;EAChC,IAAI,kBACF,OAAO,IAAI,MAAM,QAAQ,GAAG,gBAAgB;EAE9C,OAAO,IAAI,MAAM,QAAQ,WAAW,UAAU,IAAI,IAAI,GAAG,CAAC;CAC5D,GAAG,CAAC,kBAAkB,MAAM,CAAC;CAG7B,MAAM,eAAe,OAAO,CAAC;CAE7B,UAAU,QAAQ,UAAU;EAC1B,MAAM,YAAY,KAAK,IAAI,OAAO,8BAA8B;EAChE,MAAM,qBACJ,qBAAqB,UACpB,OAAO,qBAAqB,YAC3B,iBAAiB,WAAW,OAAO;EAEvC,IACE,cACA,oBACA,sBACA,CAAC,oBACD;GACA,MAAM,YAAY;IAAE,GAAG,SAAS;IAAI,GAAG,SAAS;GAAG;GACnD,MAAM,cAAc;IAAE,GAAG,iBAAiB;IAAI,GAAG,iBAAiB;GAAG;GAErE,MAAM,kBACJ,OAAO,qBAAqB,YAC5B,iBAAiB,WAAW,OAAO;GACrC,MAAM,kBACJ,OAAO,qBAAqB,YAC5B,iBAAiB,WAAW,OAAO;GAErC,MAAM,aACJ,qBAAqB,YACrB,qBAAqB,YACrB,mBACA;GAEF,IAAI,gBAAgB;GAEpB,IAAI,cAAc,CAAC,WAAW,UAC5B,gBAAgB,WAAW,UAAU;QAChC,IAAI,CAAC,cAAc,WAAW,UACnC,gBAAgB,aAAa,UAAU;GAGzC,IAAI,CAAC,cAAc,UACjB,gBAAgB,2BACd,eACA,WACA,aACA,WACA,KAAK,IAAI,CACX;GAGF,IAAI,kBAAkB,YACpB,mBAAmB,aAAa;EAEpC;EAGA,aAAa,WAAW,aAAa,UAAU,KAAK;EAEpD,mBAAmB,KAAK,SAAS;EAEjC,qBAAqB,SAAS;EAE9B,wBAAwB,WAAW,aAAa,OAAO;EAEvD,wBAAwB,WAAW,aAAa,OAAO;EAEvD,IAAI,YAAY;GACd,MAAM,OAAO,IAAI,MAAM,IAAI,MAAM;GACjC,IAAI,MAAM;IACR,MAAM,eAAe,oBAAoB,UAAU;IACnD,KAAK,SAAS,IAAI;GACpB;EACF;CACF,CAAC;CAID,OACE,qBAAC,SAAD;EACY;EACV,UAAU;GAAC;GALW,qBAAqB;GAKV;EAAC;EAClC,OAAO;GAAC,WAAW,SAAS,CAAC,QAAQ;GAAO;GAAO;EAAK;EACxD,MAAM,qBAAqB;YAJ7B,CAOE,qBAAC,SAAD;GAAO,UAAU;GAAc,UAAU;IAAC;IAAkB;IAAG;GAAC;aAAhE;IAEE,oBAAC,cAAD;KACO;KACL,OAAO;KACP,WAAW;KACX,YAAY,eAAe,UAAU;KACtB;KACC;KAChB,gBAAgB;KAChB,kBAAkB;KAClB,cAAc;KACd,kBAAkB;KACO;KACN;KACnB,oBAAoB;MAClB,YAAY,mBAAmB;MAC/B,SAAS,mBAAmB;MAC5B,eAAe,mBAAmB;MAClC,aAAa,mBAAmB;MAChC,WAAW,mBAAmB;MAC9B,WAAW,mBAAmB;KAChC;KACc;KACd,aAAa,UAAU;KACZ;IACZ,CAAA;IAMA,cACC,qBAAC,QAAD;KAAM,UAAU;MAAC;MAAG;MAAK;KAAG;eAA5B,CACE,oBAAC,kBAAD,EAAgB,MAAM,CAAC,IAAK,EAAE,EAAI,CAAA,GAClC,oBAAC,qBAAD;MACE,OAAO,cAAc;MACrB,aAAA;MACA,SAAS;MACT,MAAM;KACP,CAAA,CACG;;IAIP,gBACC,qBAAC,QAAD;KAAM,UAAU;MAAC;MAAG;MAAK;KAAC;eAA1B,CACE,oBAAC,iBAAD,EAAe,MAAM;MAAC;MAAK;MAAM;MAAG;KAAE,EAAI,CAAA,GAC1C,oBAAC,qBAAD,EAAmB,OAAO,cAAc,cAAgB,CAAA,CACpD;;IAIP,eAAe,QACd,oBAAC,MAAD;KACE,UAAU;MAAC;MAAG;MAAK;KAAC;KACpB,QAAA;KACA,gBAAgB,WAAW,KAAK;KAChC,SAAS;KACT,OAAO;MAAE,eAAe;MAAQ,YAAY;KAAO;eAEnD,qBAAC,OAAD;MACE,OAAO;OACL,YAAY,YAAY;OACxB,WAAW;OACX,OAAO;OACP,YAAY;MACd;gBANF;OASE,oBAAC,OAAD;QACE,OAAO;SACL,UAAU,WAAW,SAAS;SAC9B,YAAY;SACZ,cAAc;QAChB;QACA,eAAY;kBAEX,KAAK;OACH,CAAA;OAGJ,uBACC,oBAAC,OAAD;QACE,OAAO;SACL,UAAU,WAAW,SAAS;SAC9B,OAAO,WAAW,WAAW;QAC/B;QACA,eAAY;kBAEX;OACE,CAAA;QAIL,cAAc,aAAa,iBAC3B,oBAAC,OAAD;QACE,OAAO;SACL,UAAU,WAAW,SAAS;SAC9B,YAAY;SACZ,OAAO;SACP,WAAW;QACb;QACA,eAAY;kBAEX,aAAa,OAAO,YAAY,OAAO;OACrC,CAAA;MAEJ;;IACD,CAAA;IAIP,eACC,oBAAC,uBAAD;KACU;KACG;KACF;KACL;KACK;KACM;KACT;KACK;KACD;IACX,CAAA;GAEE;MAAE,GAEJ;;AAEX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdaptiveQuality.js","names":[],"sources":["../../../../../src/components/shared/three/optimization/AdaptiveQuality.ts"],"sourcesContent":["/**\n * AdaptiveQuality - Dynamic quality adjustment system for mobile performance\n *\n * Monitors real-time FPS and automatically adjusts rendering quality to maintain\n * 55fps target on mobile devices. Provides quality presets and smooth transitions.\n *\n * Features:\n * - Real-time FPS monitoring\n * - Automatic quality adjustment (high/medium/low)\n * - Debounced quality changes to prevent thrashing\n * - Mobile-optimized thresholds\n * - React hook integration\n *\n * @module components/shared/three/optimization/AdaptiveQuality\n * @category Performance Optimization\n * @korean 적응형품질시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * Quality levels for adaptive rendering\n */\nexport type QualityLevel = \"high\" | \"medium\" | \"low\";\n\n/**\n * Quality settings for each level\n */\nexport interface QualitySettings {\n readonly level: QualityLevel;\n readonly shadowMapSize: number;\n readonly maxParticles: number;\n readonly postProcessing: boolean;\n readonly effectsQuality: number; // 0.0-1.0 multiplier\n}\n\n/**\n * FPS thresholds for quality adjustment\n */\nexport interface AdaptiveQualityThresholds {\n /** FPS threshold to downgrade quality */\n readonly downgradeThreshold: number;\n /** FPS threshold to upgrade quality */\n readonly upgradeThreshold: number;\n /** Minimum time between quality changes (ms) */\n readonly debounceTime: number;\n /** Number of frames to average for stability */\n readonly sampleSize: number;\n}\n\n/**\n * Default thresholds for mobile optimization\n */\nconst DEFAULT_MOBILE_THRESHOLDS: AdaptiveQualityThresholds = {\n downgradeThreshold: 45, // Downgrade if fps drops below 45\n upgradeThreshold: 58, // Upgrade if fps consistently above 58\n debounceTime: 2000, // 2 seconds between changes\n sampleSize: 60, // Average over 60 frames (~1 second at 60fps)\n};\n\n/**\n * Quality presets optimized for mobile performance\n */\nexport const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {\n high: {\n level: \"high\",\n shadowMapSize: 1536,\n maxParticles: 60,\n postProcessing: false, // Keep disabled on mobile\n effectsQuality: 1.0,\n },\n medium: {\n level: \"medium\",\n shadowMapSize: 1024,\n maxParticles: 40,\n postProcessing: false,\n effectsQuality: 0.75,\n },\n low: {\n level: \"low\",\n shadowMapSize: 512,\n maxParticles: 20,\n postProcessing: false,\n effectsQuality: 0.5,\n },\n} as const;\n\n/**\n * Adaptive quality system class\n */\nexport class AdaptiveQualitySystem {\n private currentQuality: QualityLevel = \"high\";\n private fpsHistory: number[] = [];\n private lastQualityChange = 0;\n private readonly thresholds: AdaptiveQualityThresholds;\n\n constructor(thresholds: Partial<AdaptiveQualityThresholds> = {}) {\n this.thresholds = { ...DEFAULT_MOBILE_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update system with current FPS\n * Returns new quality level if changed, null otherwise\n */\n update(currentFps: number): QualityLevel | null {\n const now = performance.now();\n\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n if (this.currentQuality === \"high\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"low\";\n }\n } else if (\n avgFps > this.thresholds.upgradeThreshold &&\n this.currentQuality !== \"high\"\n ) {\n if (this.currentQuality === \"low\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"high\";\n }\n }\n\n if (newQuality !== null) {\n this.currentQuality = newQuality;\n this.lastQualityChange = now;\n this.fpsHistory = []; // Reset history after change\n\n if (import.meta.env.DEV) {\n console.log(\n `[AdaptiveQuality] Quality changed to ${newQuality} (avg fps: ${avgFps.toFixed(1)})`,\n );\n }\n\n return newQuality;\n }\n\n return null;\n }\n\n /**\n * Get current quality level\n */\n getCurrentQuality(): QualityLevel {\n return this.currentQuality;\n }\n\n /**\n * Get settings for current quality level\n */\n getCurrentSettings(): QualitySettings {\n return QUALITY_PRESETS[this.currentQuality];\n }\n\n /**\n * Manually set quality level\n */\n setQuality(quality: QualityLevel): void {\n this.currentQuality = quality;\n this.lastQualityChange = performance.now();\n this.fpsHistory = [];\n }\n\n /**\n * Reset the system\n */\n reset(): void {\n this.fpsHistory = [];\n this.lastQualityChange = 0;\n }\n}\n\n/**\n * React hook for adaptive quality management\n *\n * Automatically monitors FPS and adjusts quality settings.\n * Returns current quality level and settings.\n *\n * @param enabled - Whether adaptive quality is enabled\n * @param isMobile - Whether device is mobile (stricter thresholds)\n * @param onQualityChange - Callback when quality level changes\n * @returns Current quality settings\n *\n * @example\n * ```tsx\n * function CombatScene({ isMobile }) {\n * const quality = useAdaptiveQuality(true, isMobile, (level) => {\n * console.log(`Quality changed to ${level}`);\n * });\n *\n * return (\n * <Canvas shadowMap={{ size: quality.shadowMapSize }}>\n * <ParticleSystem maxParticles={quality.maxParticles} />\n * </Canvas>\n * );\n * }\n * ```\n */\nexport function useAdaptiveQuality(\n enabled: boolean = true,\n isMobile: boolean = false,\n onQualityChange?: (quality: QualityLevel) => void,\n): QualitySettings {\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n const thresholds = isMobile\n ? {\n downgradeThreshold: 45, // More aggressive on mobile\n upgradeThreshold: 58,\n debounceTime: 2000,\n sampleSize: 60,\n }\n : {\n downgradeThreshold: 50,\n upgradeThreshold: 59,\n debounceTime: 3000,\n sampleSize: 90,\n };\n\n systemRef.current = new AdaptiveQualitySystem(thresholds);\n systemRef.current.setQuality(initialQuality);\n }, [isMobile, initialQuality]);\n\n const lastTimeRef = useRef(0);\n const frameCountRef = useRef(0);\n const initializedRef = useRef(false);\n\n useFrame(() => {\n if (!enabled || !systemRef.current) return;\n\n const now = performance.now();\n\n if (!initializedRef.current) {\n lastTimeRef.current = now;\n initializedRef.current = true;\n return;\n }\n\n const delta = now - lastTimeRef.current;\n\n if (delta > 0) {\n const fps = 1000 / delta;\n frameCountRef.current++;\n\n const newQuality = systemRef.current.update(fps);\n\n if (newQuality !== null) {\n setCurrentQuality(newQuality);\n onQualityChange?.(newQuality);\n }\n }\n\n lastTimeRef.current = now;\n });\n\n return QUALITY_PRESETS[currentQuality];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdaptiveQuality.js","names":[],"sources":["../../../../../src/components/shared/three/optimization/AdaptiveQuality.ts"],"sourcesContent":["/**\n * AdaptiveQuality - Dynamic quality adjustment system for mobile performance\n *\n * Monitors real-time FPS and automatically adjusts rendering quality to maintain\n * 55fps target on mobile devices. Provides quality presets and smooth transitions.\n *\n * Features:\n * - Real-time FPS monitoring\n * - Automatic quality adjustment (high/medium/low)\n * - Debounced quality changes to prevent thrashing\n * - Mobile-optimized thresholds\n * - React hook integration\n *\n * @module components/shared/three/optimization/AdaptiveQuality\n * @category Performance Optimization\n * @korean 적응형품질시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * Quality levels for adaptive rendering\n */\nexport type QualityLevel = \"high\" | \"medium\" | \"low\";\n\n/**\n * Quality settings for each level\n */\nexport interface QualitySettings {\n readonly level: QualityLevel;\n readonly shadowMapSize: number;\n readonly maxParticles: number;\n readonly postProcessing: boolean;\n readonly effectsQuality: number; // 0.0-1.0 multiplier\n}\n\n/**\n * FPS thresholds for quality adjustment\n */\nexport interface AdaptiveQualityThresholds {\n /** FPS threshold to downgrade quality */\n readonly downgradeThreshold: number;\n /** FPS threshold to upgrade quality */\n readonly upgradeThreshold: number;\n /** Minimum time between quality changes (ms) */\n readonly debounceTime: number;\n /** Number of frames to average for stability */\n readonly sampleSize: number;\n}\n\n/**\n * Default thresholds for mobile optimization\n */\nconst DEFAULT_MOBILE_THRESHOLDS: AdaptiveQualityThresholds = {\n downgradeThreshold: 45, // Downgrade if fps drops below 45\n upgradeThreshold: 58, // Upgrade if fps consistently above 58\n debounceTime: 2000, // 2 seconds between changes\n sampleSize: 60, // Average over 60 frames (~1 second at 60fps)\n};\n\n/**\n * Quality presets optimized for mobile performance\n */\nexport const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {\n high: {\n level: \"high\",\n shadowMapSize: 1536,\n maxParticles: 60,\n postProcessing: false, // Keep disabled on mobile\n effectsQuality: 1.0,\n },\n medium: {\n level: \"medium\",\n shadowMapSize: 1024,\n maxParticles: 40,\n postProcessing: false,\n effectsQuality: 0.75,\n },\n low: {\n level: \"low\",\n shadowMapSize: 512,\n maxParticles: 20,\n postProcessing: false,\n effectsQuality: 0.5,\n },\n} as const;\n\n/**\n * Adaptive quality system class\n */\nexport class AdaptiveQualitySystem {\n private currentQuality: QualityLevel = \"high\";\n private fpsHistory: number[] = [];\n private lastQualityChange = 0;\n private readonly thresholds: AdaptiveQualityThresholds;\n\n constructor(thresholds: Partial<AdaptiveQualityThresholds> = {}) {\n this.thresholds = { ...DEFAULT_MOBILE_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update system with current FPS\n * Returns new quality level if changed, null otherwise\n */\n update(currentFps: number): QualityLevel | null {\n const now = performance.now();\n\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n if (this.currentQuality === \"high\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"low\";\n }\n } else if (\n avgFps > this.thresholds.upgradeThreshold &&\n this.currentQuality !== \"high\"\n ) {\n if (this.currentQuality === \"low\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"high\";\n }\n }\n\n if (newQuality !== null) {\n this.currentQuality = newQuality;\n this.lastQualityChange = now;\n this.fpsHistory = []; // Reset history after change\n\n if (import.meta.env.DEV) {\n console.log(\n `[AdaptiveQuality] Quality changed to ${newQuality} (avg fps: ${avgFps.toFixed(1)})`,\n );\n }\n\n return newQuality;\n }\n\n return null;\n }\n\n /**\n * Get current quality level\n */\n getCurrentQuality(): QualityLevel {\n return this.currentQuality;\n }\n\n /**\n * Get settings for current quality level\n */\n getCurrentSettings(): QualitySettings {\n return QUALITY_PRESETS[this.currentQuality];\n }\n\n /**\n * Manually set quality level\n */\n setQuality(quality: QualityLevel): void {\n this.currentQuality = quality;\n this.lastQualityChange = performance.now();\n this.fpsHistory = [];\n }\n\n /**\n * Reset the system\n */\n reset(): void {\n this.fpsHistory = [];\n this.lastQualityChange = 0;\n }\n}\n\n/**\n * React hook for adaptive quality management\n *\n * Automatically monitors FPS and adjusts quality settings.\n * Returns current quality level and settings.\n *\n * @param enabled - Whether adaptive quality is enabled\n * @param isMobile - Whether device is mobile (stricter thresholds)\n * @param onQualityChange - Callback when quality level changes\n * @returns Current quality settings\n *\n * @example\n * ```tsx\n * function CombatScene({ isMobile }) {\n * const quality = useAdaptiveQuality(true, isMobile, (level) => {\n * console.log(`Quality changed to ${level}`);\n * });\n *\n * return (\n * <Canvas shadowMap={{ size: quality.shadowMapSize }}>\n * <ParticleSystem maxParticles={quality.maxParticles} />\n * </Canvas>\n * );\n * }\n * ```\n */\nexport function useAdaptiveQuality(\n enabled: boolean = true,\n isMobile: boolean = false,\n onQualityChange?: (quality: QualityLevel) => void,\n): QualitySettings {\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n const thresholds = isMobile\n ? {\n downgradeThreshold: 45, // More aggressive on mobile\n upgradeThreshold: 58,\n debounceTime: 2000,\n sampleSize: 60,\n }\n : {\n downgradeThreshold: 50,\n upgradeThreshold: 59,\n debounceTime: 3000,\n sampleSize: 90,\n };\n\n systemRef.current = new AdaptiveQualitySystem(thresholds);\n systemRef.current.setQuality(initialQuality);\n }, [isMobile, initialQuality]);\n\n const lastTimeRef = useRef(0);\n const frameCountRef = useRef(0);\n const initializedRef = useRef(false);\n\n useFrame(() => {\n if (!enabled || !systemRef.current) return;\n\n const now = performance.now();\n\n if (!initializedRef.current) {\n lastTimeRef.current = now;\n initializedRef.current = true;\n return;\n }\n\n const delta = now - lastTimeRef.current;\n\n if (delta > 0) {\n const fps = 1000 / delta;\n frameCountRef.current++;\n\n const newQuality = systemRef.current.update(fps);\n\n if (newQuality !== null) {\n setCurrentQuality(newQuality);\n onQualityChange?.(newQuality);\n }\n }\n\n lastTimeRef.current = now;\n });\n\n return QUALITY_PRESETS[currentQuality];\n}\n"],"mappings":"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"}
|