blacktrigram 0.7.39 → 0.7.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioAssetLoader.js.map +1 -1
- package/lib/audio/AudioAssetRegistry.js.map +1 -1
- package/lib/audio/AudioCache.js.map +1 -1
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioMonitor.js.map +1 -1
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/AudioProvider.js.map +1 -1
- package/lib/audio/AudioUtils.js.map +1 -1
- package/lib/audio/BoneImpactAudioMap.js.map +1 -1
- package/lib/audio/VariantSelector.js.map +1 -1
- package/lib/audio/types.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
- package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
- package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
- package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/Key3D.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
- package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
- package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
- package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/constants/bodyDimensions.js.map +1 -1
- package/lib/constants/bodyRenderingConstants.js.map +1 -1
- package/lib/data/archetypeClothing.js.map +1 -1
- package/lib/data/archetypePhysicalAttributes.js.map +1 -1
- package/lib/data/techniqueMappings.js.map +1 -1
- package/lib/data/techniques.js.map +1 -1
- package/lib/hooks/useActionFeedback.js.map +1 -1
- package/lib/hooks/useBalanceAnimations.js.map +1 -1
- package/lib/hooks/useCombatTimer.js.map +1 -1
- package/lib/hooks/useDebounce.js.map +1 -1
- package/lib/hooks/useHUDLayout.js.map +1 -1
- package/lib/hooks/useHandPoseTransitions.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useMatchCountdown.js.map +1 -1
- package/lib/hooks/useMuscleActivation.js.map +1 -1
- package/lib/hooks/usePauseMenu.js.map +1 -1
- package/lib/hooks/usePlayerAnimation.js.map +1 -1
- package/lib/hooks/useResponsiveLayout.js.map +1 -1
- package/lib/hooks/useRoundTransition.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTechniqueSelection.js.map +1 -1
- package/lib/hooks/useThrottle.js.map +1 -1
- package/lib/hooks/useTouchControls.js.map +1 -1
- package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
- package/lib/hooks/useWindowSize.js.map +1 -1
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/EffectCalculator.js.map +1 -1
- package/lib/systems/LayoutSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ResponsiveScaling.js.map +1 -1
- package/lib/systems/TrigramSystem.js.map +1 -1
- package/lib/systems/VitalPointSystem.js.map +1 -1
- package/lib/systems/ai/AIPersonality.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
- package/lib/systems/ai/ComboSystem.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/ai/TrainingAI.js.map +1 -1
- package/lib/systems/ai/types.js.map +1 -1
- package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/HandPoses.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeInterpolation.js +3 -90
- package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
- package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
- package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
- package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
- package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
- package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
- package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
- package/lib/systems/animation/core/types.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
- package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
- package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
- package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
- package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
- package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/bodypart/types.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/breathing/feedback.js.map +1 -1
- package/lib/systems/breathing/integration.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/FallIntegration.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
- package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
- package/lib/systems/combat/typeGuards.js.map +1 -1
- package/lib/systems/effects.js.map +1 -1
- package/lib/systems/game.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
- package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
- package/lib/systems/movement/integration.js.map +1 -1
- package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
- package/lib/systems/physics/CollisionDetection.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
- package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
- package/lib/systems/trigram/KoreanCulture.js.map +1 -1
- package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
- package/lib/systems/trigram/StanceManager.js.map +1 -1
- package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
- package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
- package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
- package/lib/systems/trigram/techniques/index.js.map +1 -1
- package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
- package/lib/systems/trigram/types.js.map +1 -1
- package/lib/systems/types.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
- package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
- package/lib/types/AccessibilityTypes.js.map +1 -1
- package/lib/types/PhysicsTypes.js.map +1 -1
- package/lib/types/common.js.map +1 -1
- package/lib/types/constants/colors.js.map +1 -1
- package/lib/types/constants/designSystem.js.map +1 -1
- package/lib/types/constants/layout.js.map +1 -1
- package/lib/types/constants/performance.js.map +1 -1
- package/lib/types/constants/typography.js.map +1 -1
- package/lib/types/facial.js.map +1 -1
- package/lib/types/hand-animation.js.map +1 -1
- package/lib/types/injury.js.map +1 -1
- package/lib/types/physics.js.map +1 -1
- package/lib/types/skeletal.js.map +1 -1
- package/lib/types/techniqueId.js.map +1 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/arenaWorldDimensions.js.map +1 -1
- package/lib/utils/assetConfig.js.map +1 -1
- package/lib/utils/characterScaling.js.map +1 -1
- package/lib/utils/colorHelpers.js.map +1 -1
- package/lib/utils/colorUtils.js.map +1 -1
- package/lib/utils/combatReadiness.js.map +1 -1
- package/lib/utils/controlMapping.js.map +1 -1
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/fabricTextures.js.map +1 -1
- package/lib/utils/hapticFeedback.js.map +1 -1
- package/lib/utils/haptics.js.map +1 -1
- package/lib/utils/htmlOverlayHelpers.js.map +1 -1
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/math.js.map +1 -1
- package/lib/utils/mobileLayoutHelpers.js.map +1 -1
- package/lib/utils/mobileUIUtils.js.map +1 -1
- package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
- package/lib/utils/performanceOptimization.js.map +1 -1
- package/lib/utils/player3DHelpers.js.map +1 -1
- package/lib/utils/playerUtils.js.map +1 -1
- package/lib/utils/responsiveLayout.js.map +1 -1
- package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
- package/lib/utils/responsiveOrientationConstants.js.map +1 -1
- package/lib/utils/safeAreaUtils.js.map +1 -1
- package/lib/utils/sharedPhysicsConfig.js.map +1 -1
- package/lib/utils/skeletonScaling.js.map +1 -1
- package/lib/utils/stanceHelpers.js.map +1 -1
- package/lib/utils/threeObjectPool.js.map +1 -1
- package/lib/utils/visualEffects.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"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 // Detect stance changes - external effect (audio) justifies useEffect\n\n useEffect(() => {\n const previousStance = prevStanceRef.current;\n\n // Only trigger if stance actually changed\n if (previousStance !== stance) {\n prevStanceRef.current = stance;\n\n // External effects: audio playback (external system) and callbacks\n // These setState calls are intentional - triggered by prop change, not creating infinite loops\n setIsTransitioning(true);\n setFromStance(previousStance);\n onStanceTransitionStart?.(previousStance, stance);\n\n // External system: audio playback\n if (enableStanceAudio) {\n audio.playSFX(AUDIO_ASSETS.STANCE_CHANGE);\n }\n }\n }, [stance, audio, enableStanceAudio, onStanceTransitionStart]);\n\n // Handle transition completion\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,iBAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCD,IAAa,2BAER,EACH,QACA,IACA,WAAW,OACX,iBACA,0BAA0B,MAC1B,qBAAqB,MACrB,oBAAoB,MACpB,qBAAqB,IACrB,yBACA,4BACA,GAAG,kBACC;CACJ,MAAM,QAAQ,UAAU;CACxB,MAAM,gBAAgB,OAAsB,OAAO;CACnD,MAAM,CAAC,iBAAiB,sBAAsB,SAAS,MAAM;CAC7D,MAAM,CAAC,YAAY,iBAAiB,SAAwB,OAAO;
|
|
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 // Detect stance changes - external effect (audio) justifies useEffect\n\n useEffect(() => {\n const previousStance = prevStanceRef.current;\n\n // Only trigger if stance actually changed\n if (previousStance !== stance) {\n prevStanceRef.current = stance;\n\n // External effects: audio playback (external system) and callbacks\n // These setState calls are intentional - triggered by prop change, not creating infinite loops\n setIsTransitioning(true);\n setFromStance(previousStance);\n onStanceTransitionStart?.(previousStance, stance);\n\n // External system: audio playback\n if (enableStanceAudio) {\n audio.playSFX(AUDIO_ASSETS.STANCE_CHANGE);\n }\n }\n }, [stance, audio, enableStanceAudio, onStanceTransitionStart]);\n\n // Handle transition completion\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,iBAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCD,IAAa,2BAER,EACH,QACA,IACA,WAAW,OACX,iBACA,0BAA0B,MAC1B,qBAAqB,MACrB,oBAAoB,MACpB,qBAAqB,IACrB,yBACA,4BACA,GAAG,kBACC;CACJ,MAAM,QAAQ,UAAU;CACxB,MAAM,gBAAgB,OAAsB,OAAO;CACnD,MAAM,CAAC,iBAAiB,sBAAsB,SAAS,MAAM;CAC7D,MAAM,CAAC,YAAY,iBAAiB,SAAwB,OAAO;CAInE,gBAAgB;EACd,MAAM,iBAAiB,cAAc;EAGrC,IAAI,mBAAmB,QAAQ;GAC7B,cAAc,UAAU;GAIxB,mBAAmB,KAAK;GACxB,cAAc,eAAe;GAC7B,0BAA0B,gBAAgB,OAAO;GAGjD,IAAI,mBACF,MAAM,QAAQ,aAAa,cAAc;;IAG5C;EAAC;EAAQ;EAAO;EAAmB;EAAwB,CAAC;CAG/D,MAAM,2BAA2B,kBAAkB;EACjD,mBAAmB,MAAM;EACzB,6BAA6B,OAAO;IACnC,CAAC,QAAQ,2BAA2B,CAAC;CAExC,OACE,qBAAC,SAAD;EAAO,MAAK;YAAZ;GAEE,oBAAC,kBAAD;IACU;IACJ;IACM;IACO;IACjB,GAAI;IACJ,CAAA;GAGD,sBACC,oBAAC,gBAAD;IACU;IACR,cAAc;IACd,UAAU;IACV,OAAO,WAAW,KAAM;IACxB,UAAU,CAAC;IACX,CAAA;GAIH,2BAA2B,mBAC1B,oBAAC,wBAAD;IACc;IACZ,UAAU;IACV,sBAAsB;IACtB,UAAU;IACV,iBAAiB,CAAC;IAClB,CAAA;GAEE"}
|
|
@@ -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 // Use laterality with default to \"right\"\n const effectiveLaterality = laterality ?? \"right\";\n // Get physical attributes for the archetype\n const physicalAttributes = useMemo(\n () => getArchetypePhysicalAttributes(archetype),\n [archetype],\n );\n\n // Create skeletal rig with scaled dimensions based on archetype\n const rig = useMemo<SkeletalRig>(\n () => createScaledHumanoidRig(physicalAttributes),\n [physicalAttributes],\n );\n\n // ========================================\n // ANIMATION HOOKS - Modular animation system\n // ========================================\n\n // Base skeletal animation (idle, walk, attack, etc.)\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 // Hand pose transitions for both hands\n const { leftHandState, rightHandState, updateHandAnimations } =\n useHandPoseTransitions({\n currentAnimation,\n attackAnimation,\n isBlocking,\n });\n\n // NOTE: Guard pose overlay removed - stance animations built with MartialArtsAnimationBuilder\n // already include proper guard positions via transitionToStanceGuard()\n // 가드 포즈 오버레이 제거 - MartialArtsAnimationBuilder로 빌드된 자세 애니메이션에\n // transitionToStanceGuard()를 통한 적절한 가드 위치가 이미 포함되어 있음\n\n // Balance animations (sway, stumble, lean based on balance state)\n const { swayPosition, helplessRotation, updateBalanceAnimations } =\n useBalanceAnimations({\n balance,\n });\n\n // Muscle activation system\n const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stamina,\n });\n\n // Get archetype-specific skin tone\n const skinTone = useMemo(() => getArchetypeSkinTone(archetype), [archetype]);\n\n // Body color - use skin tone for normal, override for special states\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 // Stance color\n const stanceColor = useMemo(() => getStanceColor(stance), [stance]);\n const trigramSymbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n\n // Track recent combat events for expression calculation\n const [justHit, setJustHit] = useState(false);\n const [justLanded, setJustLanded] = useState(false);\n const lastHealthRef = useRef(health);\n\n // Detect hit events (health decreased)\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 // Detect successful attacks (currentAnimation changed to attack)\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 // Calculate facial expression from combat state (or use provided one)\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 // Facial damage state\n const calculatedFacialDamage = useMemo(() => {\n return facialDamage ?? createDefaultFacialDamage();\n }, [facialDamage]);\n\n // Opponent position for eye tracking\n const opponentPos = useMemo(() => {\n if (opponentPosition) {\n return new THREE.Vector3(...opponentPosition);\n }\n // Default: opponent in front\n return new THREE.Vector3(facing === \"right\" ? 5 : -5, 2, 0);\n }, [opponentPosition, facing]);\n\n // ========================================\n // ANIMATION FRAME LOOP - Modular hook-based system\n // ========================================\n\n // Frame counter for periodic updates\n const frameCounter = useRef(0);\n\n // Animation loop using useFrame (60fps)\n useFrame((_state, delta) => {\n // Update body facing to track opponent (if enabled)\n // ONLY track opponent when NOT moving (walk animations handle their own direction)\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 // Check if facing should be locked during committed animations\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 // Lock facing at start of committed action (attack/defend/step/turn)\n updatedFacing = lockFacing(bodyFacing);\n } else if (!shouldLock && bodyFacing.isLocked) {\n // Unlock facing after committed action completes\n updatedFacing = unlockFacing(bodyFacing);\n }\n\n // Update facing direction (handles rotation speed, head tracking, turns)\n if (!updatedFacing.isLocked) {\n updatedFacing = updateFacingTowardOpponent(\n updatedFacing,\n playerPos,\n opponentPos,\n delta,\n Date.now(),\n );\n }\n\n // Notify parent if facing changed\n if (updatedFacing !== bodyFacing) {\n onBodyFacingUpdate(updatedFacing);\n }\n }\n\n // ========================================\n // HOOK-BASED ANIMATION UPDATES (60fps)\n // ========================================\n\n // Update frame counter for periodic state sync\n frameCounter.current = (frameCounter.current + 1) % 10;\n\n // 1. Base skeletal animation (idle, walk, attack, etc.)\n // Stance-specific guard positions are built into the MartialArtsAnimationBuilder animations\n // 자세별 가드 위치가 MartialArtsAnimationBuilder 애니메이션에 포함됨\n updateRigAnimation(rig, delta);\n\n // 2. Hand pose transitions\n updateHandAnimations(delta);\n\n // 3. Balance animations (sway, stumble, lean)\n updateBalanceAnimations(delta, frameCounter.current);\n\n // 5. Muscle activation states\n updateMuscleActivations(delta, frameCounter.current);\n\n // Apply head rotation toward opponent (if body facing tracking is enabled)\n // Note: Torso rotation is now handled by guard pose overlay for proper stance positioning\n // Only the head tracks the opponent independently for natural looking\n if (bodyFacing) {\n // Apply head rotation to head bone (includes independent offset)\n // Head can track ±45° independently from torso for natural looking\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 // Use diagonal rotation override if set, otherwise use prop rotation\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,IAAM,kBAAkB,WAA2B;AAWjD,QAAO;EATL,MAAM,cAAc;EACpB,KAAK,cAAc;EACnB,IAAI,cAAc;EAClB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,cAAc;EACnB,KAAK,cAAc;EAEd,CAAa,WAAW,cAAc;;;;;;;;;AAU/C,IAAM,oBAAoB,WAA2B;AAWnD,QAAO;EATL,MAAM;EACN,KAAK;EACL,IAAI;EACJ,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EAEA,CAAQ,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6B5B,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;CAEJ,MAAM,sBAAsB,cAAc;CAE1C,MAAM,qBAAqB,cACnB,+BAA+B,UAAU,EAC/C,CAAC,UAAU,CACZ;CAGD,MAAM,MAAM,cACJ,wBAAwB,mBAAmB,EACjD,CAAC,mBAAmB,CACrB;CAOD,MAAM,EAAE,oBAAoB,sBAAsB,qBAAqB;EACrE;EACA;EACA;EACA;EACA,YAAY;EACZ;EACD,CAAC;CAGF,MAAM,EAAE,eAAe,gBAAgB,yBACrC,uBAAuB;EACrB;EACA;EACA;EACD,CAAC;CAQJ,MAAM,EAAE,cAAc,kBAAkB,4BACtC,qBAAqB,EACnB,SACD,CAAC;CAGJ,MAAM,EAAE,cAAc,4BAA4B,oBAAoB;EACpE;EACA;EACA;EACA;EACD,CAAC;CAGF,MAAM,WAAW,cAAc,qBAAqB,UAAU,EAAE,CAAC,UAAU,CAAC;CAG5E,MAAM,YAAY,cAAc;AAC9B,MAAI,UAAW,QAAO,cAAc;AACpC,MAAI,SAAS,YAAY,GAAK,QAAO,cAAc;AACnD,MAAI,KAAK,MAAM,GAAK,QAAO,cAAc;AACzC,SAAO;IACN;EAAC;EAAW;EAAQ;EAAW;EAAI;EAAS,CAAC;CAGhD,MAAM,cAAc,cAAc,eAAe,OAAO,EAAE,CAAC,OAAO,CAAC;CACnE,MAAM,gBAAgB,cAAc,iBAAiB,OAAO,EAAE,CAAC,OAAO,CAAC;CAGvE,MAAM,CAAC,SAAS,cAAc,SAAS,MAAM;CAC7C,MAAM,CAAC,YAAY,iBAAiB,SAAS,MAAM;CACnD,MAAM,gBAAgB,OAAO,OAAO;AAGpC,iBAAgB;EACd,IAAI;AAEJ,MAAI,SAAS,cAAc,SAAS;AAClC,cAAW,KAAK;AAChB,eAAY,iBAAiB,WAAW,MAAM,EAAE,IAAK;;AAGvD,gBAAc,UAAU;AAExB,eAAa;AACX,OAAI,cAAc,KAAA,EAChB,cAAa,UAAU;;IAG1B,CAAC,OAAO,CAAC;AAGZ,iBAAgB;EACd,IAAI;AAEJ,MAAI,qBAAqB,UAAU;AACjC,iBAAc,KAAK;AACnB,eAAY,iBAAiB,cAAc,MAAM,EAAE,IAAI;;AAGzD,eAAa;AACX,OAAI,cAAc,KAAA,EAChB,cAAa,UAAU;;IAG1B,CAAC,iBAAiB,CAAC;CAGtB,MAAM,uBAAuB,cAAc;AACzC,MAAI,CAAC,wBACH,QAAO,iBAAiB;AAG1B,MAAI,iBACF,QAAO;AAGT,SAAO,6BACL,QACA,WACA,SACA,MACA,eACA,SACA,WACD;IACA;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;CAGF,MAAM,yBAAyB,cAAc;AAC3C,SAAO,gBAAgB,2BAA2B;IACjD,CAAC,aAAa,CAAC;CAGlB,MAAM,cAAc,cAAc;AAChC,MAAI,iBACF,QAAO,IAAI,MAAM,QAAQ,GAAG,iBAAiB;AAG/C,SAAO,IAAI,MAAM,QAAQ,WAAW,UAAU,IAAI,IAAI,GAAG,EAAE;IAC1D,CAAC,kBAAkB,OAAO,CAAC;CAO9B,MAAM,eAAe,OAAO,EAAE;AAG9B,WAAU,QAAQ,UAAU;EAG1B,MAAM,qBACJ,qBAAqB,UACpB,OAAO,qBAAqB,YAC3B,iBAAiB,WAAW,QAAQ;AAExC,MACE,cACA,oBACA,sBACA,CAAC,oBACD;GACA,MAAM,YAAY;IAAE,GAAG,SAAS;IAAI,GAAG,SAAS;IAAI;GACpD,MAAM,cAAc;IAAE,GAAG,iBAAiB;IAAI,GAAG,iBAAiB;IAAI;GAGtE,MAAM,kBACJ,OAAO,qBAAqB,YAC5B,iBAAiB,WAAW,QAAQ;GACtC,MAAM,kBACJ,OAAO,qBAAqB,YAC5B,iBAAiB,WAAW,QAAQ;GAEtC,MAAM,aACJ,qBAAqB,YACrB,qBAAqB,YACrB,mBACA;GAEF,IAAI,gBAAgB;AAEpB,OAAI,cAAc,CAAC,WAAW,SAE5B,iBAAgB,WAAW,WAAW;YAC7B,CAAC,cAAc,WAAW,SAEnC,iBAAgB,aAAa,WAAW;AAI1C,OAAI,CAAC,cAAc,SACjB,iBAAgB,2BACd,eACA,WACA,aACA,OACA,KAAK,KAAK,CACX;AAIH,OAAI,kBAAkB,WACpB,oBAAmB,cAAc;;AASrC,eAAa,WAAW,aAAa,UAAU,KAAK;AAKpD,qBAAmB,KAAK,MAAM;AAG9B,uBAAqB,MAAM;AAG3B,0BAAwB,OAAO,aAAa,QAAQ;AAGpD,0BAAwB,OAAO,aAAa,QAAQ;AAKpD,MAAI,YAAY;GAGd,MAAM,OAAO,IAAI,MAAM,IAAI,OAAO;AAClC,OAAI,MAAM;IACR,MAAM,eAAe,oBAAoB,WAAW;AACpD,SAAK,SAAS,IAAI;;;GAGtB;AAKF,QACE,qBAAC,SAAD;EACY;EACV,UAAU;GAAC;GALW,qBAAqB;GAKV;GAAE;EACnC,OAAO;GAAC,WAAW,SAAS,CAAC,QAAQ;GAAO;GAAO;GAAM;EACzD,MAAM,qBAAqB;YAJ7B,CAOE,qBAAC,SAAD;GAAO,UAAU;GAAc,UAAU;IAAC;IAAkB;IAAG;IAAE;aAAjE;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;MAC/B;KACa;KACd,aAAa,UAAU;KACZ;KACX,CAAA;IAMD,cACC,qBAAC,QAAD;KAAM,UAAU;MAAC;MAAG;MAAK;MAAI;eAA7B,CACE,oBAAC,kBAAD,EAAgB,MAAM,CAAC,IAAK,GAAG,EAAI,CAAA,EACnC,oBAAC,qBAAD;MACE,OAAO,cAAc;MACrB,aAAA;MACA,SAAS;MACT,MAAM;MACN,CAAA,CACG;;IAIR,gBACC,qBAAC,QAAD;KAAM,UAAU;MAAC;MAAG;MAAK;MAAE;eAA3B,CACE,oBAAC,iBAAD,EAAe,MAAM;MAAC;MAAK;MAAM;MAAG;MAAG,EAAI,CAAA,EAC3C,oBAAC,qBAAD,EAAmB,OAAO,cAAc,eAAiB,CAAA,CACpD;;IAIR,eAAe,QACd,oBAAC,MAAD;KACE,UAAU;MAAC;MAAG;MAAK;MAAE;KACrB,QAAA;KACA,gBAAgB,WAAW,KAAK;KAChC,SAAS;KACT,OAAO;MAAE,eAAe;MAAQ,YAAY;MAAQ;eAEpD,qBAAC,OAAD;MACE,OAAO;OACL,YAAY,YAAY;OACxB,WAAW;OACX,OAAO;OACP,YAAY;OACb;gBANH;OASE,oBAAC,OAAD;QACE,OAAO;SACL,UAAU,WAAW,SAAS;SAC9B,YAAY;SACZ,cAAc;SACf;QACD,eAAY;kBAEX,KAAK;QACF,CAAA;OAGL,uBACC,oBAAC,OAAD;QACE,OAAO;SACL,UAAU,WAAW,SAAS;SAC9B,OAAO,WAAW,YAAY;SAC/B;QACD,eAAY;kBAEX;QACG,CAAA;QAIN,cAAc,aAAa,iBAC3B,oBAAC,OAAD;QACE,OAAO;SACL,UAAU,WAAW,SAAS;SAC9B,YAAY;SACZ,OAAO;SACP,WAAW;SACZ;QACD,eAAY;kBAEX,aAAa,OAAO,YAAY,OAAO;QACpC,CAAA;OAEJ;;KACD,CAAA;IAIR,eACC,oBAAC,uBAAD;KACU;KACG;KACF;KACL;KACK;KACM;KACT;KACK;KACD;KACV,CAAA;IAEE;MAAC,IAEH"}
|
|
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 // Use laterality with default to \"right\"\n const effectiveLaterality = laterality ?? \"right\";\n // Get physical attributes for the archetype\n const physicalAttributes = useMemo(\n () => getArchetypePhysicalAttributes(archetype),\n [archetype],\n );\n\n // Create skeletal rig with scaled dimensions based on archetype\n const rig = useMemo<SkeletalRig>(\n () => createScaledHumanoidRig(physicalAttributes),\n [physicalAttributes],\n );\n\n // ========================================\n // ANIMATION HOOKS - Modular animation system\n // ========================================\n\n // Base skeletal animation (idle, walk, attack, etc.)\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 // Hand pose transitions for both hands\n const { leftHandState, rightHandState, updateHandAnimations } =\n useHandPoseTransitions({\n currentAnimation,\n attackAnimation,\n isBlocking,\n });\n\n // NOTE: Guard pose overlay removed - stance animations built with MartialArtsAnimationBuilder\n // already include proper guard positions via transitionToStanceGuard()\n // 가드 포즈 오버레이 제거 - MartialArtsAnimationBuilder로 빌드된 자세 애니메이션에\n // transitionToStanceGuard()를 통한 적절한 가드 위치가 이미 포함되어 있음\n\n // Balance animations (sway, stumble, lean based on balance state)\n const { swayPosition, helplessRotation, updateBalanceAnimations } =\n useBalanceAnimations({\n balance,\n });\n\n // Muscle activation system\n const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stamina,\n });\n\n // Get archetype-specific skin tone\n const skinTone = useMemo(() => getArchetypeSkinTone(archetype), [archetype]);\n\n // Body color - use skin tone for normal, override for special states\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 // Stance color\n const stanceColor = useMemo(() => getStanceColor(stance), [stance]);\n const trigramSymbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n\n // Track recent combat events for expression calculation\n const [justHit, setJustHit] = useState(false);\n const [justLanded, setJustLanded] = useState(false);\n const lastHealthRef = useRef(health);\n\n // Detect hit events (health decreased)\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 // Detect successful attacks (currentAnimation changed to attack)\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 // Calculate facial expression from combat state (or use provided one)\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 // Facial damage state\n const calculatedFacialDamage = useMemo(() => {\n return facialDamage ?? createDefaultFacialDamage();\n }, [facialDamage]);\n\n // Opponent position for eye tracking\n const opponentPos = useMemo(() => {\n if (opponentPosition) {\n return new THREE.Vector3(...opponentPosition);\n }\n // Default: opponent in front\n return new THREE.Vector3(facing === \"right\" ? 5 : -5, 2, 0);\n }, [opponentPosition, facing]);\n\n // ========================================\n // ANIMATION FRAME LOOP - Modular hook-based system\n // ========================================\n\n // Frame counter for periodic updates\n const frameCounter = useRef(0);\n\n // Animation loop using useFrame (60fps)\n useFrame((_state, delta) => {\n // Update body facing to track opponent (if enabled)\n // ONLY track opponent when NOT moving (walk animations handle their own direction)\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 // Check if facing should be locked during committed animations\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 // Lock facing at start of committed action (attack/defend/step/turn)\n updatedFacing = lockFacing(bodyFacing);\n } else if (!shouldLock && bodyFacing.isLocked) {\n // Unlock facing after committed action completes\n updatedFacing = unlockFacing(bodyFacing);\n }\n\n // Update facing direction (handles rotation speed, head tracking, turns)\n if (!updatedFacing.isLocked) {\n updatedFacing = updateFacingTowardOpponent(\n updatedFacing,\n playerPos,\n opponentPos,\n delta,\n Date.now(),\n );\n }\n\n // Notify parent if facing changed\n if (updatedFacing !== bodyFacing) {\n onBodyFacingUpdate(updatedFacing);\n }\n }\n\n // ========================================\n // HOOK-BASED ANIMATION UPDATES (60fps)\n // ========================================\n\n // Update frame counter for periodic state sync\n frameCounter.current = (frameCounter.current + 1) % 10;\n\n // 1. Base skeletal animation (idle, walk, attack, etc.)\n // Stance-specific guard positions are built into the MartialArtsAnimationBuilder animations\n // 자세별 가드 위치가 MartialArtsAnimationBuilder 애니메이션에 포함됨\n updateRigAnimation(rig, delta);\n\n // 2. Hand pose transitions\n updateHandAnimations(delta);\n\n // 3. Balance animations (sway, stumble, lean)\n updateBalanceAnimations(delta, frameCounter.current);\n\n // 5. Muscle activation states\n updateMuscleActivations(delta, frameCounter.current);\n\n // Apply head rotation toward opponent (if body facing tracking is enabled)\n // Note: Torso rotation is now handled by guard pose overlay for proper stance positioning\n // Only the head tracks the opponent independently for natural looking\n if (bodyFacing) {\n // Apply head rotation to head bone (includes independent offset)\n // Head can track ±45° independently from torso for natural looking\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 // Use diagonal rotation override if set, otherwise use prop rotation\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 +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 // Add to history\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n // Need enough samples before adjusting\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n // Check debounce time\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n // Calculate average FPS\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n // Determine if quality should change\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n // Downgrade quality\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 // Upgrade quality\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 // Initialize with appropriate starting quality\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n // Create system instance\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n // Create system with mobile-optimized thresholds\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 // FPS tracking - Initialize with 0, will be set on first frame\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 // Calculate FPS\n const now = performance.now();\n\n // Initialize on first frame\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 // Update every frame for smooth monitoring\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 // Add to history\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n // Need enough samples before adjusting\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n // Check debounce time\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n // Calculate average FPS\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n // Determine if quality should change\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n // Downgrade quality\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 // Upgrade quality\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 // Initialize with appropriate starting quality\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n // Create system instance\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n // Create system with mobile-optimized thresholds\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 // FPS tracking - Initialize with 0, will be set on first frame\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 // Calculate FPS\n const now = performance.now();\n\n // Initialize on first frame\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 // Update every frame for smooth monitoring\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AtmosphericParticles3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/AtmosphericParticles3D.tsx"],"sourcesContent":["/**\n * AtmosphericParticles3D - Three.js 3D atmospheric particle effects\n *\n * Renders rain/mist particles for environmental depth in the combat arena\n * Creates an immersive cyberpunk urban night atmosphere\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\n\n/**\n * Props for the AtmosphericParticles3D component.\n */\nexport interface AtmosphericParticles3DProps {\n /** Number of particles to render. Defaults to 500 */\n readonly count?: number;\n /** Scale factor for particle spread (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Particle fall speed. Defaults to 2 */\n readonly speed?: number;\n}\n\n/**\n * Generate particle positions using a deterministic pattern\n * This avoids Math.random() in render functions for React purity\n */\nfunction generateParticlePositions(\n count: number,\n spreadX: number,\n spreadY: number,\n spreadZ: number\n): Float32Array {\n const pos = new Float32Array(count * 3);\n\n // Use a simple pseudo-random pattern based on index\n // This creates deterministic but varied positions\n for (let i = 0; i < count; i++) {\n const t = i / count; // Normalized index [0, 1]\n\n // Create pseudo-random offsets using trigonometric functions\n const offsetX = Math.sin(t * 123.456) * Math.cos(t * 789.012);\n const offsetY = Math.sin(t * 234.567) * Math.cos(t * 890.123);\n const offsetZ = Math.sin(t * 345.678) * Math.cos(t * 901.234);\n\n pos[i * 3] = offsetX * spreadX * 0.5;\n pos[i * 3 + 1] = offsetY * spreadY * 0.5 + spreadY * 0.5; // Bias upward\n pos[i * 3 + 2] = offsetZ * spreadZ * 0.5;\n }\n\n return pos;\n}\n\n/**\n * AtmosphericParticles3D Component\n * Creates rain/mist particle effects for atmospheric depth\n *\n * Performance optimized with:\n * - BufferGeometry for efficient rendering\n * - Additive blending for transparent particles\n * - Configurable particle count for mobile optimization\n * - Deterministic position generation (no Math.random in render)\n */\nexport const AtmosphericParticles3D: React.FC<\n AtmosphericParticles3DProps\n> = ({ count = 500, scale = 1.0, speed = 2 }) => {\n const particlesRef = useRef<THREE.Points>(null);\n const [geometry] = useState(() => new THREE.BufferGeometry());\n\n // Scale-aware spread dimensions\n const spreadX = 40 * scale;\n const spreadY = 20;\n const spreadZ = 40 * scale;\n\n // Initialize particle positions once on mount\n useEffect(() => {\n const positions = generateParticlePositions(\n count,\n spreadX,\n spreadY,\n spreadZ\n );\n \n // Note: BufferAttribute doesn't have a dispose method in Three.js\n // The geometry cleanup on unmount will handle attribute cleanup\n geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n\n return () => {\n // Clean up geometry on unmount (this also cleans up attributes)\n geometry.dispose();\n };\n }, [count, spreadX, spreadY, spreadZ, geometry]);\n\n // Animate particles (rain/mist effect)\n // Performance note: Current implementation modifies geometry buffer every frame.\n // This is acceptable for up to ~500 particles but may impact performance beyond 1000.\n // For larger particle counts, consider using shader-based vertex displacement or instancing.\n useFrame((_state, delta) => {\n if (!particlesRef.current) return;\n\n const positions =\n particlesRef.current.geometry.attributes.position.array as Float32Array;\n for (let i = 0; i < count; i++) {\n positions[i * 3 + 1] -= delta * speed; // Fall down\n if (positions[i * 3 + 1] < 0) {\n positions[i * 3 + 1] = spreadY; // Reset to top\n }\n }\n particlesRef.current.geometry.attributes.position.needsUpdate = true;\n });\n\n return (\n <points ref={particlesRef} geometry={geometry}>\n <pointsMaterial\n size={0.05}\n color={0xffffff}\n transparent\n opacity={0.3}\n sizeAttenuation\n blending={THREE.AdditiveBlending}\n depthWrite={false}\n />\n </points>\n );\n};\n\nexport default AtmosphericParticles3D;\n"],"mappings":";;;;;;;;;;;;;;;AA2BA,SAAS,0BACP,OACA,SACA,SACA,SACc;CACd,MAAM,MAAM,IAAI,aAAa,QAAQ,EAAE;
|
|
1
|
+
{"version":3,"file":"AtmosphericParticles3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/AtmosphericParticles3D.tsx"],"sourcesContent":["/**\n * AtmosphericParticles3D - Three.js 3D atmospheric particle effects\n *\n * Renders rain/mist particles for environmental depth in the combat arena\n * Creates an immersive cyberpunk urban night atmosphere\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\n\n/**\n * Props for the AtmosphericParticles3D component.\n */\nexport interface AtmosphericParticles3DProps {\n /** Number of particles to render. Defaults to 500 */\n readonly count?: number;\n /** Scale factor for particle spread (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Particle fall speed. Defaults to 2 */\n readonly speed?: number;\n}\n\n/**\n * Generate particle positions using a deterministic pattern\n * This avoids Math.random() in render functions for React purity\n */\nfunction generateParticlePositions(\n count: number,\n spreadX: number,\n spreadY: number,\n spreadZ: number\n): Float32Array {\n const pos = new Float32Array(count * 3);\n\n // Use a simple pseudo-random pattern based on index\n // This creates deterministic but varied positions\n for (let i = 0; i < count; i++) {\n const t = i / count; // Normalized index [0, 1]\n\n // Create pseudo-random offsets using trigonometric functions\n const offsetX = Math.sin(t * 123.456) * Math.cos(t * 789.012);\n const offsetY = Math.sin(t * 234.567) * Math.cos(t * 890.123);\n const offsetZ = Math.sin(t * 345.678) * Math.cos(t * 901.234);\n\n pos[i * 3] = offsetX * spreadX * 0.5;\n pos[i * 3 + 1] = offsetY * spreadY * 0.5 + spreadY * 0.5; // Bias upward\n pos[i * 3 + 2] = offsetZ * spreadZ * 0.5;\n }\n\n return pos;\n}\n\n/**\n * AtmosphericParticles3D Component\n * Creates rain/mist particle effects for atmospheric depth\n *\n * Performance optimized with:\n * - BufferGeometry for efficient rendering\n * - Additive blending for transparent particles\n * - Configurable particle count for mobile optimization\n * - Deterministic position generation (no Math.random in render)\n */\nexport const AtmosphericParticles3D: React.FC<\n AtmosphericParticles3DProps\n> = ({ count = 500, scale = 1.0, speed = 2 }) => {\n const particlesRef = useRef<THREE.Points>(null);\n const [geometry] = useState(() => new THREE.BufferGeometry());\n\n // Scale-aware spread dimensions\n const spreadX = 40 * scale;\n const spreadY = 20;\n const spreadZ = 40 * scale;\n\n // Initialize particle positions once on mount\n useEffect(() => {\n const positions = generateParticlePositions(\n count,\n spreadX,\n spreadY,\n spreadZ\n );\n \n // Note: BufferAttribute doesn't have a dispose method in Three.js\n // The geometry cleanup on unmount will handle attribute cleanup\n geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n\n return () => {\n // Clean up geometry on unmount (this also cleans up attributes)\n geometry.dispose();\n };\n }, [count, spreadX, spreadY, spreadZ, geometry]);\n\n // Animate particles (rain/mist effect)\n // Performance note: Current implementation modifies geometry buffer every frame.\n // This is acceptable for up to ~500 particles but may impact performance beyond 1000.\n // For larger particle counts, consider using shader-based vertex displacement or instancing.\n useFrame((_state, delta) => {\n if (!particlesRef.current) return;\n\n const positions =\n particlesRef.current.geometry.attributes.position.array as Float32Array;\n for (let i = 0; i < count; i++) {\n positions[i * 3 + 1] -= delta * speed; // Fall down\n if (positions[i * 3 + 1] < 0) {\n positions[i * 3 + 1] = spreadY; // Reset to top\n }\n }\n particlesRef.current.geometry.attributes.position.needsUpdate = true;\n });\n\n return (\n <points ref={particlesRef} geometry={geometry}>\n <pointsMaterial\n size={0.05}\n color={0xffffff}\n transparent\n opacity={0.3}\n sizeAttenuation\n blending={THREE.AdditiveBlending}\n depthWrite={false}\n />\n </points>\n );\n};\n\nexport default AtmosphericParticles3D;\n"],"mappings":";;;;;;;;;;;;;;;AA2BA,SAAS,0BACP,OACA,SACA,SACA,SACc;CACd,MAAM,MAAM,IAAI,aAAa,QAAQ,EAAE;CAIvC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;EAC9B,MAAM,IAAI,IAAI;EAGd,MAAM,UAAU,KAAK,IAAI,IAAI,QAAQ,GAAG,KAAK,IAAI,IAAI,QAAQ;EAC7D,MAAM,UAAU,KAAK,IAAI,IAAI,QAAQ,GAAG,KAAK,IAAI,IAAI,QAAQ;EAC7D,MAAM,UAAU,KAAK,IAAI,IAAI,QAAQ,GAAG,KAAK,IAAI,IAAI,QAAQ;EAE7D,IAAI,IAAI,KAAK,UAAU,UAAU;EACjC,IAAI,IAAI,IAAI,KAAK,UAAU,UAAU,KAAM,UAAU;EACrD,IAAI,IAAI,IAAI,KAAK,UAAU,UAAU;;CAGvC,OAAO;;;;;;;;;;;;AAaT,IAAa,0BAER,EAAE,QAAQ,KAAK,QAAQ,GAAK,QAAQ,QAAQ;CAC/C,MAAM,eAAe,OAAqB,KAAK;CAC/C,MAAM,CAAC,YAAY,eAAe,IAAI,MAAM,gBAAgB,CAAC;CAG7D,MAAM,UAAU,KAAK;CACrB,MAAM,UAAU;CAChB,MAAM,UAAU,KAAK;CAGrB,gBAAgB;EACd,MAAM,YAAY,0BAChB,OACA,SACA,SACA,QACD;EAID,SAAS,aAAa,YAAY,IAAI,MAAM,gBAAgB,WAAW,EAAE,CAAC;EAE1E,aAAa;GAEX,SAAS,SAAS;;IAEnB;EAAC;EAAO;EAAS;EAAS;EAAS;EAAS,CAAC;CAMhD,UAAU,QAAQ,UAAU;EAC1B,IAAI,CAAC,aAAa,SAAS;EAE3B,MAAM,YACJ,aAAa,QAAQ,SAAS,WAAW,SAAS;EACpD,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,UAAU,IAAI,IAAI,MAAM,QAAQ;GAChC,IAAI,UAAU,IAAI,IAAI,KAAK,GACzB,UAAU,IAAI,IAAI,KAAK;;EAG3B,aAAa,QAAQ,SAAS,WAAW,SAAS,cAAc;GAChE;CAEF,OACE,oBAAC,UAAD;EAAQ,KAAK;EAAwB;YACnC,oBAAC,kBAAD;GACE,MAAM;GACN,OAAO;GACP,aAAA;GACA,SAAS;GACT,iBAAA;GACA,UAAU,MAAM;GAChB,YAAY;GACZ,CAAA;EACK,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BackgroundScene3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/BackgroundScene3D.tsx"],"sourcesContent":["/**\n * BackgroundScene3D - Shared cyberpunk Korean-themed 3D background scene\n *\n * @korean 배경씬3D - 공유 사이버펑크 한국 테마 3D 배경 씬\n *\n * Eliminates duplication across IntroScreen, ControlsScreen, and PhilosophyScreen\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Color theme variants for background scenes\n */\nexport type BackgroundTheme = \"intro\" | \"controls\" | \"philosophy\" | \"default\";\n\nexport interface BackgroundScene3DProps {\n /**\n * Color theme for the background\n * @default \"default\"\n */\n readonly theme?: BackgroundTheme;\n\n /**\n * Grid rotation speed (radians per frame)\n * @default 0.0005\n */\n readonly gridRotationSpeed?: number;\n\n /**\n * Grid position Y offset\n * @default -8\n */\n readonly gridPositionY?: number;\n\n /**\n * Grid size\n * @default 80\n */\n readonly gridSize?: number;\n\n /**\n * Grid divisions\n * @default 40\n */\n readonly gridDivisions?: number;\n\n /**\n * Fog near distance\n * @default 5\n */\n readonly fogNear?: number;\n\n /**\n * Fog far distance\n * @default 40\n */\n readonly fogFar?: number;\n\n /**\n * Ambient light intensity\n * @default 0.4\n */\n readonly ambientIntensity?: number;\n\n /**\n * Directional light intensity\n * @default 1\n */\n readonly directionalIntensity?: number;\n\n /**\n * Point light intensity\n * @default 0.5\n */\n readonly pointIntensity?: number;\n}\n\n/**\n * Theme color configurations\n */\nconst THEME_COLORS: Record<\n BackgroundTheme,\n {\n ambient: number;\n grid: number;\n point: number;\n ambientIntensity: number;\n directionalIntensity: number;\n pointIntensity: number;\n }\n> = {\n intro: {\n ambient: KOREAN_COLORS.PRIMARY_CYAN,\n grid: KOREAN_COLORS.PRIMARY_CYAN,\n point: KOREAN_COLORS.ACCENT_BLUE,\n ambientIntensity: 0.4,\n directionalIntensity: 1,\n pointIntensity: 0.5,\n },\n controls: {\n ambient: KOREAN_COLORS.PRIMARY_CYAN,\n grid: KOREAN_COLORS.PRIMARY_CYAN,\n point: KOREAN_COLORS.ACCENT_BLUE,\n ambientIntensity: 0.3,\n directionalIntensity: 0.8,\n pointIntensity: 0.4,\n },\n philosophy: {\n ambient: KOREAN_COLORS.ACCENT_GOLD,\n grid: KOREAN_COLORS.ACCENT_GOLD,\n point: KOREAN_COLORS.PRIMARY_CYAN,\n ambientIntensity: 0.3,\n directionalIntensity: 0.8,\n pointIntensity: 0.4,\n },\n default: {\n ambient: KOREAN_COLORS.PRIMARY_CYAN,\n grid: KOREAN_COLORS.PRIMARY_CYAN,\n point: KOREAN_COLORS.ACCENT_BLUE,\n ambientIntensity: 0.4,\n directionalIntensity: 1,\n pointIntensity: 0.5,\n },\n};\n\n/**\n * Shared 3D Background Scene Component\n *\n * Renders cyberpunk Korean-themed 3D background with rotating grid,\n * atmospheric lighting, and fog effects.\n *\n * @korean 사이버펑크 한국 테마의 3D 배경을 렌더링합니다.\n * 회전하는 그리드, 분위기 있는 조명, 안개 효과를 포함합니다.\n */\nexport const BackgroundScene3D: React.FC<BackgroundScene3DProps> = ({\n theme = \"default\",\n gridRotationSpeed = 0.0005,\n gridPositionY = -8,\n gridSize = 80,\n gridDivisions = 40,\n fogNear = 5,\n fogFar = 40,\n ambientIntensity,\n directionalIntensity,\n pointIntensity,\n}) => {\n const gridRef = useRef<THREE.GridHelper>(null);\n\n // Get theme colors\n const themeColors = THEME_COLORS[theme];\n\n // Use props or theme defaults\n const finalAmbientIntensity =\n ambientIntensity ?? themeColors.ambientIntensity;\n const finalDirectionalIntensity =\n directionalIntensity ?? themeColors.directionalIntensity;\n const finalPointIntensity = pointIntensity ?? themeColors.pointIntensity;\n\n // Animate grid using useFrame for proper sync with render loop\n useFrame(() => {\n if (gridRef.current) {\n gridRef.current.rotation.y += gridRotationSpeed;\n }\n });\n\n return (\n <>\n {/* Ambient lighting */}\n <ambientLight\n intensity={finalAmbientIntensity}\n color={themeColors.ambient}\n />\n\n {/* Directional lights for Korean aesthetic */}\n <directionalLight\n position={[10, 10, 5]}\n intensity={finalDirectionalIntensity}\n color={KOREAN_COLORS.ACCENT_GOLD}\n />\n <pointLight\n position={[-10, 5, -5]}\n intensity={finalPointIntensity}\n color={themeColors.point}\n />\n\n {/* Cyberpunk grid plane - positioned lower with fog to hide edges */}\n <gridHelper\n ref={gridRef}\n args={[\n gridSize,\n gridDivisions,\n themeColors.grid,\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n ]}\n position={[0, gridPositionY, 0]}\n rotation={[0, 0, 0]}\n />\n\n {/* Fog for depth - starts closer to hide grid edges */}\n <fog\n attach=\"fog\"\n args={[KOREAN_COLORS.UI_BACKGROUND_DARK, fogNear, fogFar]}\n />\n </>\n );\n};\n\nexport default BackgroundScene3D;\n"],"mappings":";;;;;;;;;;;;;;;AAmFA,IAAM,eAUF;CACF,OAAO;EACL,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACD,UAAU;EACR,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACD,YAAY;EACV,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACD,SAAS;EACP,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACF;;;;;;;;;;AAWD,IAAa,qBAAuD,EAClE,QAAQ,WACR,oBAAoB,MACpB,gBAAgB,IAChB,WAAW,IACX,gBAAgB,IAChB,UAAU,GACV,SAAS,IACT,kBACA,sBACA,qBACI;CACJ,MAAM,UAAU,OAAyB,KAAK;CAG9C,MAAM,cAAc,aAAa;CAGjC,MAAM,wBACJ,oBAAoB,YAAY;CAClC,MAAM,4BACJ,wBAAwB,YAAY;CACtC,MAAM,sBAAsB,kBAAkB,YAAY;
|
|
1
|
+
{"version":3,"file":"BackgroundScene3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/BackgroundScene3D.tsx"],"sourcesContent":["/**\n * BackgroundScene3D - Shared cyberpunk Korean-themed 3D background scene\n *\n * @korean 배경씬3D - 공유 사이버펑크 한국 테마 3D 배경 씬\n *\n * Eliminates duplication across IntroScreen, ControlsScreen, and PhilosophyScreen\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Color theme variants for background scenes\n */\nexport type BackgroundTheme = \"intro\" | \"controls\" | \"philosophy\" | \"default\";\n\nexport interface BackgroundScene3DProps {\n /**\n * Color theme for the background\n * @default \"default\"\n */\n readonly theme?: BackgroundTheme;\n\n /**\n * Grid rotation speed (radians per frame)\n * @default 0.0005\n */\n readonly gridRotationSpeed?: number;\n\n /**\n * Grid position Y offset\n * @default -8\n */\n readonly gridPositionY?: number;\n\n /**\n * Grid size\n * @default 80\n */\n readonly gridSize?: number;\n\n /**\n * Grid divisions\n * @default 40\n */\n readonly gridDivisions?: number;\n\n /**\n * Fog near distance\n * @default 5\n */\n readonly fogNear?: number;\n\n /**\n * Fog far distance\n * @default 40\n */\n readonly fogFar?: number;\n\n /**\n * Ambient light intensity\n * @default 0.4\n */\n readonly ambientIntensity?: number;\n\n /**\n * Directional light intensity\n * @default 1\n */\n readonly directionalIntensity?: number;\n\n /**\n * Point light intensity\n * @default 0.5\n */\n readonly pointIntensity?: number;\n}\n\n/**\n * Theme color configurations\n */\nconst THEME_COLORS: Record<\n BackgroundTheme,\n {\n ambient: number;\n grid: number;\n point: number;\n ambientIntensity: number;\n directionalIntensity: number;\n pointIntensity: number;\n }\n> = {\n intro: {\n ambient: KOREAN_COLORS.PRIMARY_CYAN,\n grid: KOREAN_COLORS.PRIMARY_CYAN,\n point: KOREAN_COLORS.ACCENT_BLUE,\n ambientIntensity: 0.4,\n directionalIntensity: 1,\n pointIntensity: 0.5,\n },\n controls: {\n ambient: KOREAN_COLORS.PRIMARY_CYAN,\n grid: KOREAN_COLORS.PRIMARY_CYAN,\n point: KOREAN_COLORS.ACCENT_BLUE,\n ambientIntensity: 0.3,\n directionalIntensity: 0.8,\n pointIntensity: 0.4,\n },\n philosophy: {\n ambient: KOREAN_COLORS.ACCENT_GOLD,\n grid: KOREAN_COLORS.ACCENT_GOLD,\n point: KOREAN_COLORS.PRIMARY_CYAN,\n ambientIntensity: 0.3,\n directionalIntensity: 0.8,\n pointIntensity: 0.4,\n },\n default: {\n ambient: KOREAN_COLORS.PRIMARY_CYAN,\n grid: KOREAN_COLORS.PRIMARY_CYAN,\n point: KOREAN_COLORS.ACCENT_BLUE,\n ambientIntensity: 0.4,\n directionalIntensity: 1,\n pointIntensity: 0.5,\n },\n};\n\n/**\n * Shared 3D Background Scene Component\n *\n * Renders cyberpunk Korean-themed 3D background with rotating grid,\n * atmospheric lighting, and fog effects.\n *\n * @korean 사이버펑크 한국 테마의 3D 배경을 렌더링합니다.\n * 회전하는 그리드, 분위기 있는 조명, 안개 효과를 포함합니다.\n */\nexport const BackgroundScene3D: React.FC<BackgroundScene3DProps> = ({\n theme = \"default\",\n gridRotationSpeed = 0.0005,\n gridPositionY = -8,\n gridSize = 80,\n gridDivisions = 40,\n fogNear = 5,\n fogFar = 40,\n ambientIntensity,\n directionalIntensity,\n pointIntensity,\n}) => {\n const gridRef = useRef<THREE.GridHelper>(null);\n\n // Get theme colors\n const themeColors = THEME_COLORS[theme];\n\n // Use props or theme defaults\n const finalAmbientIntensity =\n ambientIntensity ?? themeColors.ambientIntensity;\n const finalDirectionalIntensity =\n directionalIntensity ?? themeColors.directionalIntensity;\n const finalPointIntensity = pointIntensity ?? themeColors.pointIntensity;\n\n // Animate grid using useFrame for proper sync with render loop\n useFrame(() => {\n if (gridRef.current) {\n gridRef.current.rotation.y += gridRotationSpeed;\n }\n });\n\n return (\n <>\n {/* Ambient lighting */}\n <ambientLight\n intensity={finalAmbientIntensity}\n color={themeColors.ambient}\n />\n\n {/* Directional lights for Korean aesthetic */}\n <directionalLight\n position={[10, 10, 5]}\n intensity={finalDirectionalIntensity}\n color={KOREAN_COLORS.ACCENT_GOLD}\n />\n <pointLight\n position={[-10, 5, -5]}\n intensity={finalPointIntensity}\n color={themeColors.point}\n />\n\n {/* Cyberpunk grid plane - positioned lower with fog to hide edges */}\n <gridHelper\n ref={gridRef}\n args={[\n gridSize,\n gridDivisions,\n themeColors.grid,\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n ]}\n position={[0, gridPositionY, 0]}\n rotation={[0, 0, 0]}\n />\n\n {/* Fog for depth - starts closer to hide grid edges */}\n <fog\n attach=\"fog\"\n args={[KOREAN_COLORS.UI_BACKGROUND_DARK, fogNear, fogFar]}\n />\n </>\n );\n};\n\nexport default BackgroundScene3D;\n"],"mappings":";;;;;;;;;;;;;;;AAmFA,IAAM,eAUF;CACF,OAAO;EACL,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACD,UAAU;EACR,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACD,YAAY;EACV,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACD,SAAS;EACP,SAAS,cAAc;EACvB,MAAM,cAAc;EACpB,OAAO,cAAc;EACrB,kBAAkB;EAClB,sBAAsB;EACtB,gBAAgB;EACjB;CACF;;;;;;;;;;AAWD,IAAa,qBAAuD,EAClE,QAAQ,WACR,oBAAoB,MACpB,gBAAgB,IAChB,WAAW,IACX,gBAAgB,IAChB,UAAU,GACV,SAAS,IACT,kBACA,sBACA,qBACI;CACJ,MAAM,UAAU,OAAyB,KAAK;CAG9C,MAAM,cAAc,aAAa;CAGjC,MAAM,wBACJ,oBAAoB,YAAY;CAClC,MAAM,4BACJ,wBAAwB,YAAY;CACtC,MAAM,sBAAsB,kBAAkB,YAAY;CAG1D,eAAe;EACb,IAAI,QAAQ,SACV,QAAQ,QAAQ,SAAS,KAAK;GAEhC;CAEF,OACE,qBAAA,UAAA,EAAA,UAAA;EAEE,oBAAC,gBAAD;GACE,WAAW;GACX,OAAO,YAAY;GACnB,CAAA;EAGF,oBAAC,oBAAD;GACE,UAAU;IAAC;IAAI;IAAI;IAAE;GACrB,WAAW;GACX,OAAO,cAAc;GACrB,CAAA;EACF,oBAAC,cAAD;GACE,UAAU;IAAC;IAAK;IAAG;IAAG;GACtB,WAAW;GACX,OAAO,YAAY;GACnB,CAAA;EAGF,oBAAC,cAAD;GACE,KAAK;GACL,MAAM;IACJ;IACA;IACA,YAAY;IACZ,cAAc;IACf;GACD,UAAU;IAAC;IAAG;IAAe;IAAE;GAC/B,UAAU;IAAC;IAAG;IAAG;IAAE;GACnB,CAAA;EAGF,oBAAC,OAAD;GACE,QAAO;GACP,MAAM;IAAC,cAAc;IAAoB;IAAS;IAAO;GACzD,CAAA;EACD,EAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CombatArena3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/CombatArena3D.tsx"],"sourcesContent":["/**\n * CombatArena3D - Three.js 3D arena environment\n *\n * Renders the 3D combat arena with Korean dojang aesthetic\n * Includes floor, lighting, and atmospheric effects\n *\n * Enhanced features:\n * - Korean cyberpunk lighting with neon point lights (cyan, gold, blue)\n * - Reflective wet concrete floor with clearcoat and emissive glow\n * - Upgraded shadow quality (2048x2048 shadow maps)\n * - Atmospheric fog with Korean color gradient\n * - Korean signage with emissive neon glow (전투, 흑괘, 급소격)\n * - Optional atmospheric particles (rain/mist)\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../utils/threeObjectPool\";\n// Re-enabled after fixing KoreanSignage3D font prop issue\nimport AtmosphericParticles3D from \"./AtmosphericParticles3D\";\nimport KoreanSignage3D from \"./KoreanSignage3D\";\n\n/**\n * Props for the CombatArena3D component.\n * Configures the lighting and atmosphere of the 3D arena.\n */\nexport interface CombatArena3DProps {\n /** Lighting theme affecting ambiance and colors. Defaults to \"cyberpunk\" */\n readonly lighting?: \"cyberpunk\" | \"traditional\" | \"neutral\";\n /** Scale factor for arena size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Physical arena width in meters. Defaults to 10m (desktop standard) */\n readonly worldWidthMeters?: number;\n /** Physical arena depth in meters. Defaults to worldWidthMeters for square arena */\n readonly worldDepthMeters?: number;\n /** Enable atmospheric particles (rain/mist). Defaults to true on desktop, false on mobile */\n readonly enableParticles?: boolean;\n}\n\n// Floor scaling constant for extended arena boundaries\nconst FLOOR_SCALE_FACTOR = 1.5;\n\n// Shadow map size constants for performance optimization\n// Upgraded to 2048x2048 for crisp shadows on desktop\nconst SHADOW_MAP_SIZE_MOBILE: [number, number] = [1024, 1024]; // Upgraded from 512x512\nconst SHADOW_MAP_SIZE_DESKTOP: [number, number] = [2048, 2048]; // Upgraded from 1024x1024\n\n/**\n * CombatArena3D Component\n * Creates a Korean-themed 3D arena environment with cyberpunk aesthetic\n */\nexport const CombatArena3D: React.FC<CombatArena3DProps> = ({\n lighting = \"cyberpunk\",\n scale = 1.0,\n worldWidthMeters = 10, // Default desktop standard\n worldDepthMeters,\n enableParticles = scale >= 1.0, // Enable particles on desktop by default\n}) => {\n const gridRef = useRef<THREE.GridHelper>(null);\n\n // Animate grid rotation for cyberpunk effect\n useFrame(() => {\n if (gridRef.current) {\n gridRef.current.rotation.y += 0.0002;\n }\n });\n\n // Use physics-based world dimensions for 1:1 meter mapping\n // Floor extends 50% beyond arena bounds for visual buffer\n const effectiveDepth = worldDepthMeters ?? worldWidthMeters;\n const floorWidth = worldWidthMeters * FLOOR_SCALE_FACTOR;\n const floorDepth = effectiveDepth * FLOOR_SCALE_FACTOR;\n const gridSize = worldWidthMeters * FLOOR_SCALE_FACTOR;\n // Boundary markers at 80% of arena width, 40% of depth\n const markerDistance = worldWidthMeters * 0.8;\n const markerDepth = effectiveDepth * 0.4;\n\n // Memoized floor material with wet concrete aesthetic and reflections\n // Performance: Uses ThreeObjectPools for temporary Color objects to reduce GC pressure\n // Note: Empty dependency array is correct - KOREAN_COLORS is a const object\n // and doesn't need to be included in dependencies\n const floorMaterial = useMemo(\n () => {\n // Use pooled Color objects for temporary color creation\n const pooledBaseColor = ThreeObjectPools.color.acquire();\n const pooledEmissiveColor = ThreeObjectPools.color.acquire();\n \n try {\n pooledBaseColor.set(0x2a2a2a); // Dark concrete\n pooledEmissiveColor.set(KOREAN_COLORS.PRIMARY_CYAN);\n \n // Clone colors for material ownership (materials need their own color instances)\n const material = new THREE.MeshPhysicalMaterial({\n color: pooledBaseColor.clone(), // Material takes ownership of cloned color\n roughness: 0.3, // Wet/reflective surface\n metalness: 0.1,\n clearcoat: 0.3, // Wet sheen\n clearcoatRoughness: 0.4,\n envMapIntensity: 1.5, // Enhanced reflections from Environment preset\n // Subtle emissive for neon reflection glow\n emissive: pooledEmissiveColor.clone(), // Material takes ownership of cloned color\n emissiveIntensity: 0.05,\n });\n \n return material;\n } finally {\n // Release pooled colors back to pool after cloning\n ThreeObjectPools.color.release(pooledBaseColor);\n ThreeObjectPools.color.release(pooledEmissiveColor);\n }\n },\n [],\n );\n\n // Cleanup floor material on unmount\n useEffect(() => {\n return () => {\n floorMaterial.dispose();\n };\n }, [floorMaterial]);\n\n return (\n <group>\n {/* Lighting based on theme */}\n {lighting === \"cyberpunk\" && (\n <>\n {/* Base ambient light with Korean cyan tint */}\n <ambientLight intensity={0.5} color={KOREAN_COLORS.PRIMARY_CYAN} />\n\n {/* Primary directional light (moonlight) with upgraded shadows */}\n <directionalLight\n position={[15, 20, 10]}\n intensity={1.5}\n color={0xffffff}\n castShadow\n shadow-mapSize={\n scale < 1.0 ? SHADOW_MAP_SIZE_MOBILE : SHADOW_MAP_SIZE_DESKTOP\n }\n shadow-camera-far={50}\n shadow-camera-left={-20}\n shadow-camera-right={20}\n shadow-camera-top={20}\n shadow-camera-bottom={-20}\n shadow-bias={-0.0001}\n />\n\n {/* Korean neon accent lights */}\n {/* Cyan neon light (left side) */}\n <pointLight\n position={[-10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n />\n\n {/* Gold neon light (right side) */}\n <pointLight\n position={[10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.ACCENT_GOLD}\n />\n\n {/* Blue accent light (behind) */}\n <pointLight\n position={[0, 5, -15]}\n intensity={3}\n distance={30}\n decay={2}\n color={KOREAN_COLORS.ACCENT_BLUE}\n />\n </>\n )}\n\n {lighting === \"traditional\" && (\n <>\n <ambientLight intensity={0.6} color={0xffffee} />\n <directionalLight\n position={[5, 10, 5]}\n intensity={0.8}\n castShadow\n shadow-mapSize={[2048, 2048]}\n />\n </>\n )}\n\n {lighting === \"neutral\" && (\n <>\n <ambientLight intensity={0.5} />\n <directionalLight position={[10, 10, 5]} intensity={1} castShadow />\n </>\n )}\n\n {/* Arena floor - dojang mat with reflective wet concrete aesthetic */}\n {/* Floor dimensions use pre-calculated floorWidth/floorDepth which already include FLOOR_SCALE_FACTOR */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>\n <planeGeometry args={[floorWidth, floorDepth]} />\n {lighting === \"cyberpunk\" ? (\n <primitive object={floorMaterial} attach=\"material\" />\n ) : (\n <meshPhysicalMaterial\n color={KOREAN_COLORS.UI_BACKGROUND_MEDIUM}\n roughness={0.7}\n metalness={0.1}\n clearcoat={0}\n clearcoatRoughness={0.2}\n />\n )}\n </mesh>\n\n {/* Cyberpunk grid overlay (scale-aware) */}\n <gridHelper\n ref={gridRef}\n args={[\n gridSize,\n 20,\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n ]}\n position={[0, 0.01, 0]}\n />\n\n {/* Korean traditional boundary markers (scale-aware) */}\n {[\n [-markerDistance, 0, -markerDepth],\n [-markerDistance, 0, markerDepth],\n [markerDistance, 0, -markerDepth],\n [markerDistance, 0, markerDepth],\n ].map((pos, i) => (\n <mesh key={i} position={pos as [number, number, number]} castShadow>\n <cylinderGeometry args={[0.1 * scale, 0.15 * scale, 0.8, 8]} />\n <meshPhysicalMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n emissive={KOREAN_COLORS.ACCENT_GOLD}\n emissiveIntensity={0.5}\n metalness={0.8}\n roughness={0.2}\n clearcoat={1.0}\n />\n </mesh>\n ))}\n\n {/* Center marker - Yin Yang inspired (scale-aware) */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0]}>\n <ringGeometry args={[0.8 * scale, 1.0 * scale, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n\n {/* Korean Signage with neon glow */}\n {lighting === \"cyberpunk\" && <KoreanSignage3D scale={scale} />}\n\n {/* Atmospheric Particles (rain/mist) */}\n {lighting === \"cyberpunk\" && enableParticles && (\n <AtmosphericParticles3D\n count={scale >= 1.0 ? 500 : 250}\n scale={scale}\n speed={2}\n />\n )}\n </group>\n );\n};\n\nexport default CombatArena3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CombatArena3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/CombatArena3D.tsx"],"sourcesContent":["/**\n * CombatArena3D - Three.js 3D arena environment\n *\n * Renders the 3D combat arena with Korean dojang aesthetic\n * Includes floor, lighting, and atmospheric effects\n *\n * Enhanced features:\n * - Korean cyberpunk lighting with neon point lights (cyan, gold, blue)\n * - Reflective wet concrete floor with clearcoat and emissive glow\n * - Upgraded shadow quality (2048x2048 shadow maps)\n * - Atmospheric fog with Korean color gradient\n * - Korean signage with emissive neon glow (전투, 흑괘, 급소격)\n * - Optional atmospheric particles (rain/mist)\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../utils/threeObjectPool\";\n// Re-enabled after fixing KoreanSignage3D font prop issue\nimport AtmosphericParticles3D from \"./AtmosphericParticles3D\";\nimport KoreanSignage3D from \"./KoreanSignage3D\";\n\n/**\n * Props for the CombatArena3D component.\n * Configures the lighting and atmosphere of the 3D arena.\n */\nexport interface CombatArena3DProps {\n /** Lighting theme affecting ambiance and colors. Defaults to \"cyberpunk\" */\n readonly lighting?: \"cyberpunk\" | \"traditional\" | \"neutral\";\n /** Scale factor for arena size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Physical arena width in meters. Defaults to 10m (desktop standard) */\n readonly worldWidthMeters?: number;\n /** Physical arena depth in meters. Defaults to worldWidthMeters for square arena */\n readonly worldDepthMeters?: number;\n /** Enable atmospheric particles (rain/mist). Defaults to true on desktop, false on mobile */\n readonly enableParticles?: boolean;\n}\n\n// Floor scaling constant for extended arena boundaries\nconst FLOOR_SCALE_FACTOR = 1.5;\n\n// Shadow map size constants for performance optimization\n// Upgraded to 2048x2048 for crisp shadows on desktop\nconst SHADOW_MAP_SIZE_MOBILE: [number, number] = [1024, 1024]; // Upgraded from 512x512\nconst SHADOW_MAP_SIZE_DESKTOP: [number, number] = [2048, 2048]; // Upgraded from 1024x1024\n\n/**\n * CombatArena3D Component\n * Creates a Korean-themed 3D arena environment with cyberpunk aesthetic\n */\nexport const CombatArena3D: React.FC<CombatArena3DProps> = ({\n lighting = \"cyberpunk\",\n scale = 1.0,\n worldWidthMeters = 10, // Default desktop standard\n worldDepthMeters,\n enableParticles = scale >= 1.0, // Enable particles on desktop by default\n}) => {\n const gridRef = useRef<THREE.GridHelper>(null);\n\n // Animate grid rotation for cyberpunk effect\n useFrame(() => {\n if (gridRef.current) {\n gridRef.current.rotation.y += 0.0002;\n }\n });\n\n // Use physics-based world dimensions for 1:1 meter mapping\n // Floor extends 50% beyond arena bounds for visual buffer\n const effectiveDepth = worldDepthMeters ?? worldWidthMeters;\n const floorWidth = worldWidthMeters * FLOOR_SCALE_FACTOR;\n const floorDepth = effectiveDepth * FLOOR_SCALE_FACTOR;\n const gridSize = worldWidthMeters * FLOOR_SCALE_FACTOR;\n // Boundary markers at 80% of arena width, 40% of depth\n const markerDistance = worldWidthMeters * 0.8;\n const markerDepth = effectiveDepth * 0.4;\n\n // Memoized floor material with wet concrete aesthetic and reflections\n // Performance: Uses ThreeObjectPools for temporary Color objects to reduce GC pressure\n // Note: Empty dependency array is correct - KOREAN_COLORS is a const object\n // and doesn't need to be included in dependencies\n const floorMaterial = useMemo(\n () => {\n // Use pooled Color objects for temporary color creation\n const pooledBaseColor = ThreeObjectPools.color.acquire();\n const pooledEmissiveColor = ThreeObjectPools.color.acquire();\n \n try {\n pooledBaseColor.set(0x2a2a2a); // Dark concrete\n pooledEmissiveColor.set(KOREAN_COLORS.PRIMARY_CYAN);\n \n // Clone colors for material ownership (materials need their own color instances)\n const material = new THREE.MeshPhysicalMaterial({\n color: pooledBaseColor.clone(), // Material takes ownership of cloned color\n roughness: 0.3, // Wet/reflective surface\n metalness: 0.1,\n clearcoat: 0.3, // Wet sheen\n clearcoatRoughness: 0.4,\n envMapIntensity: 1.5, // Enhanced reflections from Environment preset\n // Subtle emissive for neon reflection glow\n emissive: pooledEmissiveColor.clone(), // Material takes ownership of cloned color\n emissiveIntensity: 0.05,\n });\n \n return material;\n } finally {\n // Release pooled colors back to pool after cloning\n ThreeObjectPools.color.release(pooledBaseColor);\n ThreeObjectPools.color.release(pooledEmissiveColor);\n }\n },\n [],\n );\n\n // Cleanup floor material on unmount\n useEffect(() => {\n return () => {\n floorMaterial.dispose();\n };\n }, [floorMaterial]);\n\n return (\n <group>\n {/* Lighting based on theme */}\n {lighting === \"cyberpunk\" && (\n <>\n {/* Base ambient light with Korean cyan tint */}\n <ambientLight intensity={0.5} color={KOREAN_COLORS.PRIMARY_CYAN} />\n\n {/* Primary directional light (moonlight) with upgraded shadows */}\n <directionalLight\n position={[15, 20, 10]}\n intensity={1.5}\n color={0xffffff}\n castShadow\n shadow-mapSize={\n scale < 1.0 ? SHADOW_MAP_SIZE_MOBILE : SHADOW_MAP_SIZE_DESKTOP\n }\n shadow-camera-far={50}\n shadow-camera-left={-20}\n shadow-camera-right={20}\n shadow-camera-top={20}\n shadow-camera-bottom={-20}\n shadow-bias={-0.0001}\n />\n\n {/* Korean neon accent lights */}\n {/* Cyan neon light (left side) */}\n <pointLight\n position={[-10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n />\n\n {/* Gold neon light (right side) */}\n <pointLight\n position={[10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.ACCENT_GOLD}\n />\n\n {/* Blue accent light (behind) */}\n <pointLight\n position={[0, 5, -15]}\n intensity={3}\n distance={30}\n decay={2}\n color={KOREAN_COLORS.ACCENT_BLUE}\n />\n </>\n )}\n\n {lighting === \"traditional\" && (\n <>\n <ambientLight intensity={0.6} color={0xffffee} />\n <directionalLight\n position={[5, 10, 5]}\n intensity={0.8}\n castShadow\n shadow-mapSize={[2048, 2048]}\n />\n </>\n )}\n\n {lighting === \"neutral\" && (\n <>\n <ambientLight intensity={0.5} />\n <directionalLight position={[10, 10, 5]} intensity={1} castShadow />\n </>\n )}\n\n {/* Arena floor - dojang mat with reflective wet concrete aesthetic */}\n {/* Floor dimensions use pre-calculated floorWidth/floorDepth which already include FLOOR_SCALE_FACTOR */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>\n <planeGeometry args={[floorWidth, floorDepth]} />\n {lighting === \"cyberpunk\" ? (\n <primitive object={floorMaterial} attach=\"material\" />\n ) : (\n <meshPhysicalMaterial\n color={KOREAN_COLORS.UI_BACKGROUND_MEDIUM}\n roughness={0.7}\n metalness={0.1}\n clearcoat={0}\n clearcoatRoughness={0.2}\n />\n )}\n </mesh>\n\n {/* Cyberpunk grid overlay (scale-aware) */}\n <gridHelper\n ref={gridRef}\n args={[\n gridSize,\n 20,\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n ]}\n position={[0, 0.01, 0]}\n />\n\n {/* Korean traditional boundary markers (scale-aware) */}\n {[\n [-markerDistance, 0, -markerDepth],\n [-markerDistance, 0, markerDepth],\n [markerDistance, 0, -markerDepth],\n [markerDistance, 0, markerDepth],\n ].map((pos, i) => (\n <mesh key={i} position={pos as [number, number, number]} castShadow>\n <cylinderGeometry args={[0.1 * scale, 0.15 * scale, 0.8, 8]} />\n <meshPhysicalMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n emissive={KOREAN_COLORS.ACCENT_GOLD}\n emissiveIntensity={0.5}\n metalness={0.8}\n roughness={0.2}\n clearcoat={1.0}\n />\n </mesh>\n ))}\n\n {/* Center marker - Yin Yang inspired (scale-aware) */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0]}>\n <ringGeometry args={[0.8 * scale, 1.0 * scale, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n\n {/* Korean Signage with neon glow */}\n {lighting === \"cyberpunk\" && <KoreanSignage3D scale={scale} />}\n\n {/* Atmospheric Particles (rain/mist) */}\n {lighting === \"cyberpunk\" && enableParticles && (\n <AtmosphericParticles3D\n count={scale >= 1.0 ? 500 : 250}\n scale={scale}\n speed={2}\n />\n )}\n </group>\n );\n};\n\nexport default CombatArena3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KoreanSignage3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/KoreanSignage3D.tsx"],"sourcesContent":["/**\n * KoreanSignage3D - Three.js 3D Korean text signage with emissive glow\n *\n * Renders Korean text signs with neon emissive effects positioned around the arena\n * Creates an immersive cyberpunk Korean martial arts atmosphere\n *\n * Note: Uses drei's Text component with default Inter font which supports Korean\n * The `font` prop requires a font FILE URL (.ttf/.woff), not a CSS font-family string\n */\n\nimport { Text } from \"@react-three/drei\";\nimport React, { Suspense, useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Props for the KoreanSignage3D component.\n */\nexport interface KoreanSignage3DProps {\n /** Scale factor for signage size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n}\n\n/**\n * KoreanSignage3D Component\n * Creates Korean text signs with neon emissive glow around the combat arena\n *\n * Signs:\n * - \"전투\" (Combat) - Left wall, gold accent\n * - \"흑괘\" (Black Trigram) - Right wall, cyan accent\n * - \"급소격\" (Vital Point Strike) - Back wall, red accent\n */\nexport const KoreanSignage3D: React.FC<KoreanSignage3DProps> = ({\n scale = 1.0,\n}) => {\n // Emissive material for glowing neon text\n // Using MeshBasicMaterial with toneMapped: false for bloom compatibility\n const goldNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.ACCENT_GOLD,\n toneMapped: false, // Prevent tone mapping for bloom effect\n }),\n [],\n );\n\n const cyanNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.PRIMARY_CYAN,\n toneMapped: false,\n }),\n [],\n );\n\n const redNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.KOREAN_RED,\n toneMapped: false,\n }),\n [],\n );\n\n // Scale-aware positioning and sizing\n const leftWallX = -12 * scale;\n const rightWallX = 12 * scale;\n const backWallZ = -14 * scale;\n const signHeight = 5 * scale;\n const fontSize = 1.5 * scale;\n const outlineWidth = 0.05 * scale;\n\n // Cleanup materials on unmount\n useEffect(() => {\n return () => {\n goldNeonMaterial.dispose();\n cyanNeonMaterial.dispose();\n redNeonMaterial.dispose();\n };\n }, [goldNeonMaterial, cyanNeonMaterial, redNeonMaterial]);\n\n return (\n <Suspense fallback={null}>\n <group>\n {/* \"전투\" (Combat) sign - left wall */}\n {/* Note: material prop takes precedence over color prop in Text component */}\n {/* Note: Omitting font prop uses default Inter font which supports Korean */}\n <Text\n position={[leftWallX, signHeight, 0]}\n rotation={[0, Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.PRIMARY_CYAN}\n outlineWidth={outlineWidth}\n material={goldNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 전투\n </Text>\n\n {/* \"흑괘\" (Black Trigram) sign - right wall */}\n <Text\n position={[rightWallX, signHeight, 0]}\n rotation={[0, -Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={cyanNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 흑괘\n </Text>\n\n {/* \"급소격\" (Vital Point Strike) sign - back wall */}\n <Text\n position={[0, signHeight, backWallZ]}\n rotation={[0, 0, 0]}\n fontSize={fontSize * 0.8} // Slightly smaller for back wall\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={redNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 급소격\n </Text>\n </group>\n </Suspense>\n );\n};\n\nexport default KoreanSignage3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,mBAAmD,EAC9D,QAAQ,QACJ;CAGJ,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;EACb,CAAC,EACJ,EAAE,CACH;CAED,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;EACb,CAAC,EACJ,EAAE,CACH;CAED,MAAM,kBAAkB,cAEpB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;EACb,CAAC,EACJ,EAAE,CACH;CAGD,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,KAAK;CACxB,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,IAAI;CACvB,MAAM,WAAW,MAAM;CACvB,MAAM,eAAe,MAAO;
|
|
1
|
+
{"version":3,"file":"KoreanSignage3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/KoreanSignage3D.tsx"],"sourcesContent":["/**\n * KoreanSignage3D - Three.js 3D Korean text signage with emissive glow\n *\n * Renders Korean text signs with neon emissive effects positioned around the arena\n * Creates an immersive cyberpunk Korean martial arts atmosphere\n *\n * Note: Uses drei's Text component with default Inter font which supports Korean\n * The `font` prop requires a font FILE URL (.ttf/.woff), not a CSS font-family string\n */\n\nimport { Text } from \"@react-three/drei\";\nimport React, { Suspense, useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Props for the KoreanSignage3D component.\n */\nexport interface KoreanSignage3DProps {\n /** Scale factor for signage size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n}\n\n/**\n * KoreanSignage3D Component\n * Creates Korean text signs with neon emissive glow around the combat arena\n *\n * Signs:\n * - \"전투\" (Combat) - Left wall, gold accent\n * - \"흑괘\" (Black Trigram) - Right wall, cyan accent\n * - \"급소격\" (Vital Point Strike) - Back wall, red accent\n */\nexport const KoreanSignage3D: React.FC<KoreanSignage3DProps> = ({\n scale = 1.0,\n}) => {\n // Emissive material for glowing neon text\n // Using MeshBasicMaterial with toneMapped: false for bloom compatibility\n const goldNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.ACCENT_GOLD,\n toneMapped: false, // Prevent tone mapping for bloom effect\n }),\n [],\n );\n\n const cyanNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.PRIMARY_CYAN,\n toneMapped: false,\n }),\n [],\n );\n\n const redNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.KOREAN_RED,\n toneMapped: false,\n }),\n [],\n );\n\n // Scale-aware positioning and sizing\n const leftWallX = -12 * scale;\n const rightWallX = 12 * scale;\n const backWallZ = -14 * scale;\n const signHeight = 5 * scale;\n const fontSize = 1.5 * scale;\n const outlineWidth = 0.05 * scale;\n\n // Cleanup materials on unmount\n useEffect(() => {\n return () => {\n goldNeonMaterial.dispose();\n cyanNeonMaterial.dispose();\n redNeonMaterial.dispose();\n };\n }, [goldNeonMaterial, cyanNeonMaterial, redNeonMaterial]);\n\n return (\n <Suspense fallback={null}>\n <group>\n {/* \"전투\" (Combat) sign - left wall */}\n {/* Note: material prop takes precedence over color prop in Text component */}\n {/* Note: Omitting font prop uses default Inter font which supports Korean */}\n <Text\n position={[leftWallX, signHeight, 0]}\n rotation={[0, Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.PRIMARY_CYAN}\n outlineWidth={outlineWidth}\n material={goldNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 전투\n </Text>\n\n {/* \"흑괘\" (Black Trigram) sign - right wall */}\n <Text\n position={[rightWallX, signHeight, 0]}\n rotation={[0, -Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={cyanNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 흑괘\n </Text>\n\n {/* \"급소격\" (Vital Point Strike) sign - back wall */}\n <Text\n position={[0, signHeight, backWallZ]}\n rotation={[0, 0, 0]}\n fontSize={fontSize * 0.8} // Slightly smaller for back wall\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={redNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 급소격\n </Text>\n </group>\n </Suspense>\n );\n};\n\nexport default KoreanSignage3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,mBAAmD,EAC9D,QAAQ,QACJ;CAGJ,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;EACb,CAAC,EACJ,EAAE,CACH;CAED,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;EACb,CAAC,EACJ,EAAE,CACH;CAED,MAAM,kBAAkB,cAEpB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;EACb,CAAC,EACJ,EAAE,CACH;CAGD,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,KAAK;CACxB,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,IAAI;CACvB,MAAM,WAAW,MAAM;CACvB,MAAM,eAAe,MAAO;CAG5B,gBAAgB;EACd,aAAa;GACX,iBAAiB,SAAS;GAC1B,iBAAiB,SAAS;GAC1B,gBAAgB,SAAS;;IAE1B;EAAC;EAAkB;EAAkB;EAAgB,CAAC;CAEzD,OACE,oBAAC,UAAD;EAAU,UAAU;YAClB,qBAAC,SAAD,EAAA,UAAA;GAIE,oBAAC,MAAD;IACE,UAAU;KAAC;KAAW;KAAY;KAAE;IACpC,UAAU;KAAC;KAAG,KAAK,KAAK;KAAG;KAAE;IACnB;IACV,cAAc,cAAc;IACd;IACd,UAAU;IACV,SAAQ;IACR,SAAQ;cACT;IAEM,CAAA;GAGP,oBAAC,MAAD;IACE,UAAU;KAAC;KAAY;KAAY;KAAE;IACrC,UAAU;KAAC;KAAG,CAAC,KAAK,KAAK;KAAG;KAAE;IACpB;IACV,cAAc,cAAc;IACd;IACd,UAAU;IACV,SAAQ;IACR,SAAQ;cACT;IAEM,CAAA;GAGP,oBAAC,MAAD;IACE,UAAU;KAAC;KAAG;KAAY;KAAU;IACpC,UAAU;KAAC;KAAG;KAAG;KAAE;IACnB,UAAU,WAAW;IACrB,cAAc,cAAc;IACd;IACd,UAAU;IACV,SAAQ;IACR,SAAQ;cACT;IAEM,CAAA;GACD,EAAA,CAAA;EACC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArchetypeCard.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ArchetypeCard.tsx"],"sourcesContent":["/**\n * ArchetypeCard - Three.js-compatible character archetype card\n * \n * Displays player archetype information with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { PlayerArchetype } from \"../../../../types/common\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { PLAYER_ARCHETYPES_DATA } from \"../../../../systems/types\";\n\n/**\n * Props for ArchetypeCard component\n */\nexport interface ArchetypeCardProps {\n readonly archetype: PlayerArchetype;\n readonly onSelect?: (archetype: PlayerArchetype) => void;\n readonly isSelected?: boolean;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly showStats?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ArchetypeCard Component\n * \n * A card component for displaying player archetypes with Korean martial arts styling.\n * Includes archetype name, description, and optional stats.\n * \n * @example\n * ```tsx\n * <ArchetypeCard\n * archetype={PlayerArchetype.MUSA}\n * onSelect={(archetype) => console.log(archetype)}\n * showStats\n * />\n * ```\n */\nexport const ArchetypeCard: React.FC<ArchetypeCardProps> = ({\n archetype,\n onSelect,\n isSelected = false,\n position = [0, 0, 0],\n width = 320,\n showStats = true,\n testId,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n\n const archetypeData = useMemo(\n () => PLAYER_ARCHETYPES_DATA[archetype],\n [archetype]\n );\n\n const handleClick = useCallback(() => {\n if (onSelect) {\n onSelect(archetype);\n }\n }, [onSelect, archetype]);\n\n const handleMouseEnter = useCallback(() => {\n setIsHovered(true);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n }, []);\n\n // Memoize card styles for performance\n const cardStyle = useMemo<React.CSSProperties>(() => {\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.4);\n let boxShadow = `0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.3)}`;\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)},\n 0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}\n `;\n } else if (isHovered) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.1);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.4)},\n 0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}\n `;\n }\n\n return {\n width: `${width}px`,\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"8px\",\n padding: \"16px\",\n cursor: onSelect ? \"pointer\" : \"default\",\n transition: \"all 0.3s ease\",\n boxShadow,\n transform: isHovered && onSelect ? \"translateY(-4px)\" : \"translateY(0)\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n }, [width, isSelected, isHovered, onSelect]);\n\n // Memoize header styles for performance\n const headerStyle = useMemo<React.CSSProperties>(\n () => ({\n marginBottom: \"12px\",\n paddingBottom: \"8px\",\n borderBottom: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}`,\n }),\n []\n );\n\n const titleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"20px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n marginBottom: \"4px\",\n }),\n []\n );\n\n const subtitleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n fontStyle: \"italic\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n opacity: 0.8,\n }),\n []\n );\n\n const descriptionStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n lineHeight: \"1.6\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n marginBottom: showStats ? \"12px\" : \"0\",\n }),\n [showStats]\n );\n\n const statsContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"grid\",\n gridTemplateColumns: \"repeat(2, 1fr)\",\n gap: \"8px\",\n marginTop: \"12px\",\n }),\n []\n );\n\n const statItemStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n []\n );\n\n const statLabelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n fontWeight: \"bold\",\n }),\n []\n );\n\n const statValueStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"16px\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontWeight: \"bold\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n style={cardStyle}\n data-testid={testId ?? `archetype-card-${archetype}`}\n >\n {/* Header */}\n <div style={headerStyle}>\n <div style={titleStyle}>{archetypeData.name.korean}</div>\n <div style={subtitleStyle}>{archetypeData.name.english}</div>\n </div>\n\n {/* Description */}\n <div style={descriptionStyle}>\n {archetypeData.description.korean}\n </div>\n\n {/* Stats */}\n {showStats && (\n <div style={statsContainerStyle}>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>공격 | Attack</div>\n <div style={statValueStyle}>{archetypeData.stats.attackPower}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>방어 | Defense</div>\n <div style={statValueStyle}>{archetypeData.stats.defense}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>속도 | Speed</div>\n <div style={statValueStyle}>{archetypeData.stats.speed}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>기력 | Ki</div>\n <div style={statValueStyle}>{archetypeData.baseKi}</div>\n </div>\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nArchetypeCard.displayName = \"ArchetypeCard\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAa,iBAA+C,EAC1D,WACA,UACA,aAAa,OACb,WAAW;CAAC;CAAG;CAAG;CAAE,EACpB,QAAQ,KACR,YAAY,MACZ,aACI;CACJ,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CAEjD,MAAM,gBAAgB,cACd,uBAAuB,YAC7B,CAAC,UAAU,CACZ;CAED,MAAM,cAAc,kBAAkB;AACpC,MAAI,SACF,UAAS,UAAU;IAEpB,CAAC,UAAU,UAAU,CAAC;CAEzB,MAAM,mBAAmB,kBAAkB;AACzC,eAAa,KAAK;IACjB,EAAE,CAAC;CAEN,MAAM,mBAAmB,kBAAkB;AACzC,eAAa,MAAM;IAClB,EAAE,CAAC;CAGN,MAAM,YAAY,cAAmC;EACnD,IAAI,aAAa,gBAAgB,cAAc,oBAAoB,IAAK;EACxE,IAAI,cAAc,gBAAgB,cAAc,aAAa,GAAI;EACjE,IAAI,YAAY,aAAa,gBAAgB,cAAc,aAAa,GAAI;AAE5E,MAAI,YAAY;AACd,gBAAa,gBAAgB,cAAc,cAAc,IAAK;AAC9D,iBAAc,gBAAgB,cAAc,cAAc,GAAI;AAC9D,eAAY;qBACG,gBAAgB,cAAc,aAAa,GAAI,CAAC;mBAClD,gBAAgB,cAAc,cAAc,GAAI,CAAC;;aAErD,WAAW;AACpB,gBAAa,gBAAgB,cAAc,aAAa,GAAI;AAC5D,iBAAc,gBAAgB,cAAc,aAAa,GAAI;AAC7D,eAAY;qBACG,gBAAgB,cAAc,aAAa,GAAI,CAAC;mBAClD,gBAAgB,cAAc,aAAa,GAAI,CAAC;;;AAI/D,SAAO;GACL,OAAO,GAAG,MAAM;GAChB;GACA,QAAQ,aAAa;GACrB,cAAc;GACd,SAAS;GACT,QAAQ,WAAW,YAAY;GAC/B,YAAY;GACZ;GACA,WAAW,aAAa,WAAW,qBAAqB;GACxD,YAAY;GACZ,kBAAkB;GACnB;IACA;EAAC;EAAO;EAAY;EAAW;EAAS,CAAC;CAG5C,MAAM,cAAc,eACX;EACL,cAAc;EACd,eAAe;EACf,cAAc,aAAa,gBAAgB,cAAc,aAAa,GAAI;EAC3E,GACD,EAAE,CACH;CAED,MAAM,aAAa,eACV;EACL,YAAY,YAAY;EACxB,UAAU;EACV,YAAY;EACZ,OAAO,gBAAgB,cAAc,YAAY;EACjD,YAAY,aAAa,gBAAgB,cAAc,aAAa,GAAI;EACxE,cAAc;EACf,GACD,EAAE,CACH;CAED,MAAM,gBAAgB,eACb;EACL,YAAY,YAAY;EACxB,UAAU;EACV,WAAW;EACX,OAAO,gBAAgB,cAAc,eAAe;EACpD,SAAS;EACV,GACD,EAAE,CACH;CAED,MAAM,mBAAmB,eAChB;EACL,YAAY,YAAY;EACxB,UAAU;EACV,YAAY;EACZ,OAAO,gBAAgB,cAAc,aAAa;EAClD,cAAc,YAAY,SAAS;EACpC,GACD,CAAC,UAAU,CACZ;CAED,MAAM,sBAAsB,eACnB;EACL,SAAS;EACT,qBAAqB;EACrB,KAAK;EACL,WAAW;EACZ,GACD,EAAE,CACH;CAED,MAAM,gBAAgB,eACb;EACL,SAAS;EACT,eAAe;EACf,KAAK;EACN,GACD,EAAE,CACH;CAED,MAAM,iBAAiB,eACd;EACL,YAAY,YAAY;EACxB,UAAU;EACV,OAAO,gBAAgB,cAAc,eAAe;EACpD,YAAY;EACb,GACD,EAAE,CACH;CAED,MAAM,iBAAiB,eACd;EACL,YAAY,YAAY;EACxB,UAAU;EACV,OAAO,gBAAgB,cAAc,aAAa;EAClD,YAAY;EACb,GACD,EAAE,CACH;AAED,QACE,oBAAC,MAAD;EAAgB;EAAU,QAAA;YACxB,qBAAC,OAAD;GACE,SAAS;GACT,cAAc;GACd,cAAc;GACd,OAAO;GACP,eAAa,UAAU,kBAAkB;aAL3C;IAQE,qBAAC,OAAD;KAAK,OAAO;eAAZ,CACE,oBAAC,OAAD;MAAK,OAAO;gBAAa,cAAc,KAAK;MAAa,CAAA,EACzD,oBAAC,OAAD;MAAK,OAAO;gBAAgB,cAAc,KAAK;MAAc,CAAA,CACzD;;IAGN,oBAAC,OAAD;KAAK,OAAO;eACT,cAAc,YAAY;KACvB,CAAA;IAGL,aACC,qBAAC,OAAD;KAAK,OAAO;eAAZ;MACE,qBAAC,OAAD;OAAK,OAAO;iBAAZ,CACE,oBAAC,OAAD;QAAK,OAAO;kBAAgB;QAAiB,CAAA,EAC7C,oBAAC,OAAD;QAAK,OAAO;kBAAiB,cAAc,MAAM;QAAkB,CAAA,CAC/D;;MACN,qBAAC,OAAD;OAAK,OAAO;iBAAZ,CACE,oBAAC,OAAD;QAAK,OAAO;kBAAgB;QAAkB,CAAA,EAC9C,oBAAC,OAAD;QAAK,OAAO;kBAAiB,cAAc,MAAM;QAAc,CAAA,CAC3D;;MACN,qBAAC,OAAD;OAAK,OAAO;iBAAZ,CACE,oBAAC,OAAD;QAAK,OAAO;kBAAgB;QAAgB,CAAA,EAC5C,oBAAC,OAAD;QAAK,OAAO;kBAAiB,cAAc,MAAM;QAAY,CAAA,CACzD;;MACN,qBAAC,OAAD;OAAK,OAAO;iBAAZ,CACE,oBAAC,OAAD;QAAK,OAAO;kBAAgB;QAAa,CAAA,EACzC,oBAAC,OAAD;QAAK,OAAO;kBAAiB,cAAc;QAAa,CAAA,CACpD;;MACF;;IAEJ;;EACD,CAAA;;AAIX,cAAc,cAAc"}
|
|
1
|
+
{"version":3,"file":"ArchetypeCard.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ArchetypeCard.tsx"],"sourcesContent":["/**\n * ArchetypeCard - Three.js-compatible character archetype card\n * \n * Displays player archetype information with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { PlayerArchetype } from \"../../../../types/common\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { PLAYER_ARCHETYPES_DATA } from \"../../../../systems/types\";\n\n/**\n * Props for ArchetypeCard component\n */\nexport interface ArchetypeCardProps {\n readonly archetype: PlayerArchetype;\n readonly onSelect?: (archetype: PlayerArchetype) => void;\n readonly isSelected?: boolean;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly showStats?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ArchetypeCard Component\n * \n * A card component for displaying player archetypes with Korean martial arts styling.\n * Includes archetype name, description, and optional stats.\n * \n * @example\n * ```tsx\n * <ArchetypeCard\n * archetype={PlayerArchetype.MUSA}\n * onSelect={(archetype) => console.log(archetype)}\n * showStats\n * />\n * ```\n */\nexport const ArchetypeCard: React.FC<ArchetypeCardProps> = ({\n archetype,\n onSelect,\n isSelected = false,\n position = [0, 0, 0],\n width = 320,\n showStats = true,\n testId,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n\n const archetypeData = useMemo(\n () => PLAYER_ARCHETYPES_DATA[archetype],\n [archetype]\n );\n\n const handleClick = useCallback(() => {\n if (onSelect) {\n onSelect(archetype);\n }\n }, [onSelect, archetype]);\n\n const handleMouseEnter = useCallback(() => {\n setIsHovered(true);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n }, []);\n\n // Memoize card styles for performance\n const cardStyle = useMemo<React.CSSProperties>(() => {\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.4);\n let boxShadow = `0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.3)}`;\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)},\n 0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}\n `;\n } else if (isHovered) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.1);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.4)},\n 0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}\n `;\n }\n\n return {\n width: `${width}px`,\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"8px\",\n padding: \"16px\",\n cursor: onSelect ? \"pointer\" : \"default\",\n transition: \"all 0.3s ease\",\n boxShadow,\n transform: isHovered && onSelect ? \"translateY(-4px)\" : \"translateY(0)\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n }, [width, isSelected, isHovered, onSelect]);\n\n // Memoize header styles for performance\n const headerStyle = useMemo<React.CSSProperties>(\n () => ({\n marginBottom: \"12px\",\n paddingBottom: \"8px\",\n borderBottom: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}`,\n }),\n []\n );\n\n const titleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"20px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n marginBottom: \"4px\",\n }),\n []\n );\n\n const subtitleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n fontStyle: \"italic\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n opacity: 0.8,\n }),\n []\n );\n\n const descriptionStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n lineHeight: \"1.6\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n marginBottom: showStats ? \"12px\" : \"0\",\n }),\n [showStats]\n );\n\n const statsContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"grid\",\n gridTemplateColumns: \"repeat(2, 1fr)\",\n gap: \"8px\",\n marginTop: \"12px\",\n }),\n []\n );\n\n const statItemStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n []\n );\n\n const statLabelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n fontWeight: \"bold\",\n }),\n []\n );\n\n const statValueStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"16px\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontWeight: \"bold\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n style={cardStyle}\n data-testid={testId ?? `archetype-card-${archetype}`}\n >\n {/* Header */}\n <div style={headerStyle}>\n <div style={titleStyle}>{archetypeData.name.korean}</div>\n <div style={subtitleStyle}>{archetypeData.name.english}</div>\n </div>\n\n {/* Description */}\n <div style={descriptionStyle}>\n {archetypeData.description.korean}\n </div>\n\n {/* Stats */}\n {showStats && (\n <div style={statsContainerStyle}>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>공격 | Attack</div>\n <div style={statValueStyle}>{archetypeData.stats.attackPower}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>방어 | Defense</div>\n <div style={statValueStyle}>{archetypeData.stats.defense}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>속도 | Speed</div>\n <div style={statValueStyle}>{archetypeData.stats.speed}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>기력 | Ki</div>\n <div style={statValueStyle}>{archetypeData.baseKi}</div>\n </div>\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nArchetypeCard.displayName = \"ArchetypeCard\";\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BodyPartHealthDisplay.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BodyPartHealthDisplay.tsx"],"sourcesContent":["/**\n * BodyPartHealthDisplay Component - Individual body part health visualization\n *\n * Displays health bars for all 8 body parts:\n * - Head (두부)\n * - Neck (경부)\n * - Torso Upper (상부 몸통)\n * - Torso Lower (하부 몸통)\n * - Left Arm (좌팔)\n * - Right Arm (우팔)\n * - Left Leg (좌다리)\n * - Right Leg (우다리)\n *\n * @module components/shared/three/ui/BodyPartHealthDisplay\n * @category Shared UI\n * @korean 신체부위체력표시\n */\n\nimport React, { useMemo } from \"react\";\nimport { BodyPart, BodyPartHealth } from \"../../../../systems/bodypart/types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\nexport interface BodyPartHealthDisplayProps {\n /** Body part health data */\n readonly bodyPartHealth: BodyPartHealth;\n /** Player identifier for test IDs */\n readonly playerId: string;\n /** Position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * Body part names in Korean and English\n */\nconst BODY_PART_NAMES: Record<BodyPart, { korean: string; english: string }> = {\n [BodyPart.HEAD]: { korean: \"두부\", english: \"Head\" },\n [BodyPart.NECK]: { korean: \"경부\", english: \"Neck\" },\n [BodyPart.TORSO_UPPER]: { korean: \"상부\", english: \"Upper\" },\n [BodyPart.TORSO_LOWER]: { korean: \"하부\", english: \"Lower\" },\n [BodyPart.ARM_LEFT]: { korean: \"좌팔\", english: \"L.Arm\" },\n [BodyPart.ARM_RIGHT]: { korean: \"우팔\", english: \"R.Arm\" },\n [BodyPart.LEG_LEFT]: { korean: \"좌다리\", english: \"L.Leg\" },\n [BodyPart.LEG_RIGHT]: { korean: \"우다리\", english: \"R.Leg\" },\n};\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (health: number): number => {\n if (health >= 80) return KOREAN_COLORS.HEALTH_FULL;\n if (health >= 60) return KOREAN_COLORS.ACCENT_GOLD;\n if (health >= 40) return KOREAN_COLORS.WARNING_ORANGE;\n if (health >= 20) return KOREAN_COLORS.PAIN_INDICATOR;\n return KOREAN_COLORS.NEGATIVE_RED_DARK;\n};\n\n/**\n * BodyPartHealthDisplay - Shows health bars for all 8 body parts\n *\n * @example\n * ```tsx\n * <BodyPartHealthDisplay\n * bodyPartHealth={player.bodyPartHealth}\n * playerId=\"player-1\"\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BodyPartHealthDisplay: React.FC<BodyPartHealthDisplayProps> = ({\n bodyPartHealth,\n playerId,\n position,\n isMobile,\n}) => {\n const isLeft = position === \"left\";\n\n // Responsive sizing\n const barWidth = isMobile ? 100 : 140;\n const barHeight = isMobile ? 8 : 10;\n const fontSize = isMobile ? 9 : 10;\n const gap = isMobile ? \"4px\" : \"5px\";\n\n // Group body parts for display\n const bodyPartGroups = useMemo(\n () => [\n {\n label: \"상체 | Upper\",\n parts: [\n BodyPart.HEAD,\n BodyPart.NECK,\n BodyPart.TORSO_UPPER,\n BodyPart.TORSO_LOWER,\n ],\n },\n { label: \"팔 | Arms\", parts: [BodyPart.ARM_LEFT, BodyPart.ARM_RIGHT] },\n { label: \"다리 | Legs\", parts: [BodyPart.LEG_LEFT, BodyPart.LEG_RIGHT] },\n ],\n [],\n );\n\n return (\n <div\n data-testid={`body-part-health-${playerId}`}\n style={{\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n borderRadius: \"8px\",\n padding: isMobile ? \"6px\" : \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)}`,\n boxShadow: `0 0 12px ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n pointerEvents: \"none\",\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n fontSize: isMobile ? \"9px\" : \"10px\",\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: isLeft ? \"left\" : \"right\",\n marginBottom: \"2px\",\n }}\n >\n 신체 | Body Parts\n </div>\n\n {/* Body part groups */}\n {bodyPartGroups.map((group) => (\n <div\n key={group.label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"3px\",\n }}\n >\n {/* Group label - optional, can be hidden for compact display */}\n <div\n style={{\n fontSize: `${fontSize - 1}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 0.8),\n textAlign: isLeft ? \"left\" : \"right\",\n marginTop: group.label === bodyPartGroups[0].label ? \"0\" : \"4px\",\n }}\n >\n {group.label}\n </div>\n\n {/* Body parts in group */}\n {group.parts.map((part) => {\n const health = bodyPartHealth[part];\n const names = BODY_PART_NAMES[part];\n const healthColor = getHealthColor(health);\n const shouldPulse = health < 20;\n\n return (\n <div\n key={part}\n data-testid={`body-part-${playerId}-${part}`}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Body part name and value */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(healthColor, 1),\n }}\n >\n <span style={{ fontWeight: health < 40 ? \"bold\" : \"normal\" }}>\n {isMobile\n ? names.korean\n : `${names.korean} | ${names.english}`}\n </span>\n <span style={{ fontSize: `${fontSize - 1}px` }}>\n {Math.round(health)}%\n </span>\n </div>\n\n {/* Health bar */}\n <div\n style={{\n width: `${barWidth}px`,\n height: `${barHeight}px`,\n backgroundColor: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 1,\n ),\n borderRadius: \"2px\",\n overflow: \"hidden\",\n animation: shouldPulse\n ? \"healthPulse 0.8s infinite\"\n : \"none\",\n }}\n >\n <div\n style={{\n width: `${health}%`,\n height: \"100%\",\n backgroundColor: hexToRgbaString(healthColor, 1),\n transition:\n \"width 0.3s ease-in-out, background-color 0.3s ease-in-out\",\n boxShadow: `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`,\n }}\n />\n </div>\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport default BodyPartHealthDisplay;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BodyPartHealthDisplay.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BodyPartHealthDisplay.tsx"],"sourcesContent":["/**\n * BodyPartHealthDisplay Component - Individual body part health visualization\n *\n * Displays health bars for all 8 body parts:\n * - Head (두부)\n * - Neck (경부)\n * - Torso Upper (상부 몸통)\n * - Torso Lower (하부 몸통)\n * - Left Arm (좌팔)\n * - Right Arm (우팔)\n * - Left Leg (좌다리)\n * - Right Leg (우다리)\n *\n * @module components/shared/three/ui/BodyPartHealthDisplay\n * @category Shared UI\n * @korean 신체부위체력표시\n */\n\nimport React, { useMemo } from \"react\";\nimport { BodyPart, BodyPartHealth } from \"../../../../systems/bodypart/types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\nexport interface BodyPartHealthDisplayProps {\n /** Body part health data */\n readonly bodyPartHealth: BodyPartHealth;\n /** Player identifier for test IDs */\n readonly playerId: string;\n /** Position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * Body part names in Korean and English\n */\nconst BODY_PART_NAMES: Record<BodyPart, { korean: string; english: string }> = {\n [BodyPart.HEAD]: { korean: \"두부\", english: \"Head\" },\n [BodyPart.NECK]: { korean: \"경부\", english: \"Neck\" },\n [BodyPart.TORSO_UPPER]: { korean: \"상부\", english: \"Upper\" },\n [BodyPart.TORSO_LOWER]: { korean: \"하부\", english: \"Lower\" },\n [BodyPart.ARM_LEFT]: { korean: \"좌팔\", english: \"L.Arm\" },\n [BodyPart.ARM_RIGHT]: { korean: \"우팔\", english: \"R.Arm\" },\n [BodyPart.LEG_LEFT]: { korean: \"좌다리\", english: \"L.Leg\" },\n [BodyPart.LEG_RIGHT]: { korean: \"우다리\", english: \"R.Leg\" },\n};\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (health: number): number => {\n if (health >= 80) return KOREAN_COLORS.HEALTH_FULL;\n if (health >= 60) return KOREAN_COLORS.ACCENT_GOLD;\n if (health >= 40) return KOREAN_COLORS.WARNING_ORANGE;\n if (health >= 20) return KOREAN_COLORS.PAIN_INDICATOR;\n return KOREAN_COLORS.NEGATIVE_RED_DARK;\n};\n\n/**\n * BodyPartHealthDisplay - Shows health bars for all 8 body parts\n *\n * @example\n * ```tsx\n * <BodyPartHealthDisplay\n * bodyPartHealth={player.bodyPartHealth}\n * playerId=\"player-1\"\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BodyPartHealthDisplay: React.FC<BodyPartHealthDisplayProps> = ({\n bodyPartHealth,\n playerId,\n position,\n isMobile,\n}) => {\n const isLeft = position === \"left\";\n\n // Responsive sizing\n const barWidth = isMobile ? 100 : 140;\n const barHeight = isMobile ? 8 : 10;\n const fontSize = isMobile ? 9 : 10;\n const gap = isMobile ? \"4px\" : \"5px\";\n\n // Group body parts for display\n const bodyPartGroups = useMemo(\n () => [\n {\n label: \"상체 | Upper\",\n parts: [\n BodyPart.HEAD,\n BodyPart.NECK,\n BodyPart.TORSO_UPPER,\n BodyPart.TORSO_LOWER,\n ],\n },\n { label: \"팔 | Arms\", parts: [BodyPart.ARM_LEFT, BodyPart.ARM_RIGHT] },\n { label: \"다리 | Legs\", parts: [BodyPart.LEG_LEFT, BodyPart.LEG_RIGHT] },\n ],\n [],\n );\n\n return (\n <div\n data-testid={`body-part-health-${playerId}`}\n style={{\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n borderRadius: \"8px\",\n padding: isMobile ? \"6px\" : \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)}`,\n boxShadow: `0 0 12px ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n pointerEvents: \"none\",\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n fontSize: isMobile ? \"9px\" : \"10px\",\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: isLeft ? \"left\" : \"right\",\n marginBottom: \"2px\",\n }}\n >\n 신체 | Body Parts\n </div>\n\n {/* Body part groups */}\n {bodyPartGroups.map((group) => (\n <div\n key={group.label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"3px\",\n }}\n >\n {/* Group label - optional, can be hidden for compact display */}\n <div\n style={{\n fontSize: `${fontSize - 1}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 0.8),\n textAlign: isLeft ? \"left\" : \"right\",\n marginTop: group.label === bodyPartGroups[0].label ? \"0\" : \"4px\",\n }}\n >\n {group.label}\n </div>\n\n {/* Body parts in group */}\n {group.parts.map((part) => {\n const health = bodyPartHealth[part];\n const names = BODY_PART_NAMES[part];\n const healthColor = getHealthColor(health);\n const shouldPulse = health < 20;\n\n return (\n <div\n key={part}\n data-testid={`body-part-${playerId}-${part}`}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Body part name and value */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(healthColor, 1),\n }}\n >\n <span style={{ fontWeight: health < 40 ? \"bold\" : \"normal\" }}>\n {isMobile\n ? names.korean\n : `${names.korean} | ${names.english}`}\n </span>\n <span style={{ fontSize: `${fontSize - 1}px` }}>\n {Math.round(health)}%\n </span>\n </div>\n\n {/* Health bar */}\n <div\n style={{\n width: `${barWidth}px`,\n height: `${barHeight}px`,\n backgroundColor: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 1,\n ),\n borderRadius: \"2px\",\n overflow: \"hidden\",\n animation: shouldPulse\n ? \"healthPulse 0.8s infinite\"\n : \"none\",\n }}\n >\n <div\n style={{\n width: `${health}%`,\n height: \"100%\",\n backgroundColor: hexToRgbaString(healthColor, 1),\n transition:\n \"width 0.3s ease-in-out, background-color 0.3s ease-in-out\",\n boxShadow: `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`,\n }}\n />\n </div>\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport default BodyPartHealthDisplay;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BreathingIndicator2.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BreathingIndicator.tsx"],"sourcesContent":["/**\n * BreathingIndicator Component - Visual feedback for breathing disruption status\n * \n * **Korean**: 호흡곤란 표시기\n * \n * Displays breathing difficulty with:\n * - Color-coded lungs icon (🫁)\n * - Bilingual label (Korean | English)\n * - Time remaining until recovery\n * - Pulsing animation based on severity\n */\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport {\n BreathingDisruptionSystem,\n createBreathingIndicator,\n} from \"../../../../systems/breathing\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./BreathingIndicator.css\";\n\nexport interface BreathingIndicatorProps {\n /** Player state to check for breathing disruption */\n readonly player: PlayerState;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n}\n\n/**\n * BreathingIndicator - Shows breathing disruption status with Korean-English labels\n */\nexport const BreathingIndicator: React.FC<BreathingIndicatorProps> = ({\n player,\n isMobile = false,\n}) => {\n // Use state to trigger re-renders for timer updates\n const [currentTime, setCurrentTime] = useState(() => Date.now());\n\n // Update current time every 100ms to keep the timer accurate\n useEffect(() => {\n const interval = setInterval(() => {\n setCurrentTime(Date.now());\n }, 100);\n\n return () => clearInterval(interval);\n }, []);\n\n // Get current breathing disruption state\n const breathingState = useMemo(() => {\n const level = BreathingDisruptionSystem.getCurrentLevel(player);\n const activeEffect = BreathingDisruptionSystem.getActiveEffect(player);\n const timeRemaining = activeEffect\n ? Math.max(0, activeEffect.endTime - currentTime)\n : 0;\n const isRecovering = BreathingDisruptionSystem.canRecover(player);\n\n return createBreathingIndicator(level, timeRemaining, isRecovering);\n }, [player, currentTime]);\n\n // Don't render if no breathing disruption\n if (!breathingState.visible) {\n return null;\n }\n\n // Responsive sizing\n const iconSize = isMobile ? 24 : 32;\n const fontSize = isMobile ? 10 : 12;\n const padding = isMobile ? \"4px 8px\" : \"6px 12px\";\n\n // Format time remaining\n const secondsRemaining = Math.ceil(breathingState.timeRemaining / 1000);\n\n return (\n <div\n data-testid=\"breathing-indicator\"\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: isMobile ? \"6px\" : \"8px\",\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(breathingState.color, breathingState.opacity)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(breathingState.color, 0.5)}`,\n animation: \"breathing-pulse 1s ease-in-out infinite\",\n pointerEvents: \"none\",\n }}\n >\n {/* Lungs icon */}\n <div\n data-testid=\"breathing-icon\"\n style={{\n fontSize: `${iconSize}px`,\n lineHeight: 1,\n transform: `scale(${breathingState.scale})`,\n filter: `drop-shadow(0 0 6px ${hexToRgbaString(breathingState.color, 0.6)})`,\n }}\n >\n {breathingState.icon}\n </div>\n\n {/* Label and time */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Bilingual label */}\n <div\n data-testid=\"breathing-label\"\n style={{\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n color: hexToRgbaString(breathingState.color, 1),\n textShadow: `0 0 4px ${hexToRgbaString(breathingState.color, 0.8)}`,\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.label.korean} | {breathingState.label.english}\n </div>\n\n {/* Time remaining */}\n <div\n data-testid=\"breathing-timer\"\n style={{\n fontSize: `${fontSize - 2}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: breathingState.isRecovering\n ? hexToRgbaString(KOREAN_COLORS.POSITIVE_GREEN, 0.8)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.6),\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.isRecovering ? \"회복중 | Recovering\" : `${secondsRemaining}s`}\n </div>\n </div>\n </div>\n );\n};\n\nBreathingIndicator.displayName = \"BreathingIndicator\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,sBAAyD,EACpE,QACA,WAAW,YACP;CAEJ,MAAM,CAAC,aAAa,kBAAkB,eAAe,KAAK,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"BreathingIndicator2.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BreathingIndicator.tsx"],"sourcesContent":["/**\n * BreathingIndicator Component - Visual feedback for breathing disruption status\n * \n * **Korean**: 호흡곤란 표시기\n * \n * Displays breathing difficulty with:\n * - Color-coded lungs icon (🫁)\n * - Bilingual label (Korean | English)\n * - Time remaining until recovery\n * - Pulsing animation based on severity\n */\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport {\n BreathingDisruptionSystem,\n createBreathingIndicator,\n} from \"../../../../systems/breathing\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./BreathingIndicator.css\";\n\nexport interface BreathingIndicatorProps {\n /** Player state to check for breathing disruption */\n readonly player: PlayerState;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n}\n\n/**\n * BreathingIndicator - Shows breathing disruption status with Korean-English labels\n */\nexport const BreathingIndicator: React.FC<BreathingIndicatorProps> = ({\n player,\n isMobile = false,\n}) => {\n // Use state to trigger re-renders for timer updates\n const [currentTime, setCurrentTime] = useState(() => Date.now());\n\n // Update current time every 100ms to keep the timer accurate\n useEffect(() => {\n const interval = setInterval(() => {\n setCurrentTime(Date.now());\n }, 100);\n\n return () => clearInterval(interval);\n }, []);\n\n // Get current breathing disruption state\n const breathingState = useMemo(() => {\n const level = BreathingDisruptionSystem.getCurrentLevel(player);\n const activeEffect = BreathingDisruptionSystem.getActiveEffect(player);\n const timeRemaining = activeEffect\n ? Math.max(0, activeEffect.endTime - currentTime)\n : 0;\n const isRecovering = BreathingDisruptionSystem.canRecover(player);\n\n return createBreathingIndicator(level, timeRemaining, isRecovering);\n }, [player, currentTime]);\n\n // Don't render if no breathing disruption\n if (!breathingState.visible) {\n return null;\n }\n\n // Responsive sizing\n const iconSize = isMobile ? 24 : 32;\n const fontSize = isMobile ? 10 : 12;\n const padding = isMobile ? \"4px 8px\" : \"6px 12px\";\n\n // Format time remaining\n const secondsRemaining = Math.ceil(breathingState.timeRemaining / 1000);\n\n return (\n <div\n data-testid=\"breathing-indicator\"\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: isMobile ? \"6px\" : \"8px\",\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(breathingState.color, breathingState.opacity)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(breathingState.color, 0.5)}`,\n animation: \"breathing-pulse 1s ease-in-out infinite\",\n pointerEvents: \"none\",\n }}\n >\n {/* Lungs icon */}\n <div\n data-testid=\"breathing-icon\"\n style={{\n fontSize: `${iconSize}px`,\n lineHeight: 1,\n transform: `scale(${breathingState.scale})`,\n filter: `drop-shadow(0 0 6px ${hexToRgbaString(breathingState.color, 0.6)})`,\n }}\n >\n {breathingState.icon}\n </div>\n\n {/* Label and time */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Bilingual label */}\n <div\n data-testid=\"breathing-label\"\n style={{\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n color: hexToRgbaString(breathingState.color, 1),\n textShadow: `0 0 4px ${hexToRgbaString(breathingState.color, 0.8)}`,\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.label.korean} | {breathingState.label.english}\n </div>\n\n {/* Time remaining */}\n <div\n data-testid=\"breathing-timer\"\n style={{\n fontSize: `${fontSize - 2}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: breathingState.isRecovering\n ? hexToRgbaString(KOREAN_COLORS.POSITIVE_GREEN, 0.8)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.6),\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.isRecovering ? \"회복중 | Recovering\" : `${secondsRemaining}s`}\n </div>\n </div>\n </div>\n );\n};\n\nBreathingIndicator.displayName = \"BreathingIndicator\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,sBAAyD,EACpE,QACA,WAAW,YACP;CAEJ,MAAM,CAAC,aAAa,kBAAkB,eAAe,KAAK,KAAK,CAAC;CAGhE,gBAAgB;EACd,MAAM,WAAW,kBAAkB;GACjC,eAAe,KAAK,KAAK,CAAC;KACzB,IAAI;EAEP,aAAa,cAAc,SAAS;IACnC,EAAE,CAAC;CAGN,MAAM,iBAAiB,cAAc;EACnC,MAAM,QAAQ,0BAA0B,gBAAgB,OAAO;EAC/D,MAAM,eAAe,0BAA0B,gBAAgB,OAAO;EAMtE,OAAO,yBAAyB,OALV,eAClB,KAAK,IAAI,GAAG,aAAa,UAAU,YAAY,GAC/C,GACiB,0BAA0B,WAAW,OAEJ,CAAa;IAClE,CAAC,QAAQ,YAAY,CAAC;CAGzB,IAAI,CAAC,eAAe,SAClB,OAAO;CAIT,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,YAAY;CAGvC,MAAM,mBAAmB,KAAK,KAAK,eAAe,gBAAgB,IAAK;CAEvE,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,SAAS;GACT,YAAY;GACZ,KAAK,WAAW,QAAQ;GACxB;GACA,iBAAiB,gBAAgB,cAAc,OAAO,GAAI;GAC1D,cAAc;GACd,QAAQ,aAAa,gBAAgB,eAAe,OAAO,eAAe,QAAQ;GAClF,WAAW,YAAY,gBAAgB,eAAe,OAAO,GAAI;GACjE,WAAW;GACX,eAAe;GAChB;YAbH,CAgBI,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,UAAU,GAAG,SAAS;IACtB,YAAY;IACZ,WAAW,SAAS,eAAe,MAAM;IACzC,QAAQ,uBAAuB,gBAAgB,eAAe,OAAO,GAAI,CAAC;IAC3E;aAEA,eAAe;GACZ,CAAA,EAGN,qBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,eAAe;IACf,KAAK;IACN;aALH,CAQE,qBAAC,OAAD;IACE,eAAY;IACZ,OAAO;KACL,UAAU,GAAG,SAAS;KACtB,YAAY,YAAY;KACxB,YAAY;KACZ,OAAO,gBAAgB,eAAe,OAAO,EAAE;KAC/C,YAAY,WAAW,gBAAgB,eAAe,OAAO,GAAI;KACjE,YAAY;KACb;cATH;KAWG,eAAe,MAAM;KAAO;KAAI,eAAe,MAAM;KAClD;OAGN,oBAAC,OAAD;IACE,eAAY;IACZ,OAAO;KACL,UAAU,GAAG,WAAW,EAAE;KAC1B,YAAY,YAAY;KACxB,OAAO,eAAe,eAClB,gBAAgB,cAAc,gBAAgB,GAAI,GAClD,gBAAgB,cAAc,cAAc,GAAI;KACpD,YAAY;KACb;cAEA,eAAe,eAAe,qBAAqB,GAAG,iBAAiB;IACpE,CAAA,CACF;KACF;;;AAIZ,mBAAmB,cAAc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CombatReadinessBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/CombatReadinessBar.tsx"],"sourcesContent":["/**\n * CombatReadinessBar Component - 10-bar segmented combat readiness display\n * \n * Displays comprehensive combat readiness with:\n * - 10 segmented bars representing 10% each\n * - Color transitions: Green (>80%), Yellow (60-79%), Orange (40-59%), Red (20-39%), Dark Red (<20%)\n * - Smooth 0.3s transition animations\n * - Korean/English bilingual labels\n * - Numeric percentage display\n * - Responsive sizing for mobile/tablet/desktop\n * - Real-time calculation from body health, pain, consciousness, and balance\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { \n calculateCombatReadiness, \n getCombatReadinessColor, \n getCombatReadinessLabel,\n getCombatReadinessBars\n} from \"../../../../utils/combatReadiness\";\nimport type { PlayerState } from \"../../../../systems/player\";\n\nexport interface CombatReadinessBarProps {\n /** Player state containing all combat factors */\n readonly player: PlayerState;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * CombatReadinessBar - 10-bar segmented combat readiness display with Korean theming\n * \n * Calculates and displays overall combat readiness from multiple factors:\n * - Body part health (40% weight)\n * - Pain level (20% weight)\n * - Consciousness (20% weight)\n * - Balance state (20% weight)\n * \n * @example\n * ```tsx\n * <CombatReadinessBar \n * player={playerState} \n * playerId=\"player-1\"\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReadinessBar: React.FC<CombatReadinessBarProps> = ({\n player,\n playerId,\n isMobile,\n}) => {\n // Calculate combat readiness percentage\n const readiness = useMemo(\n () => calculateCombatReadiness(player),\n [player]\n );\n\n const segments = 10;\n const filledSegments = getCombatReadinessBars(readiness, segments);\n const readinessColor = getCombatReadinessColor(readiness);\n const readinessLabel = getCombatReadinessLabel(readiness);\n const shouldPulse = readiness < 20;\n\n // Responsive sizing\n const barWidth = isMobile ? 180 : 250;\n const barHeight = isMobile ? 16 : 20;\n const fontSize = isMobile ? 11 : 13;\n const padding = isMobile ? \"8px 12px\" : \"12px 16px\";\n\n // Status text based on readiness level\n const statusText = `${readiness}% ${readinessLabel.korean}`;\n\n return (\n <div\n data-testid={`combat-readiness-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"전투 준비도 | Combat Readiness\"\n aria-valuenow={readiness}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuetext={`${readiness}% Combat Readiness - ${readinessLabel.english}`}\n style={{\n width: `${barWidth}px`,\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }}\n >\n {/* Label and numeric/status display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n }}\n >\n <span>전투 준비도 | Combat Readiness</span>\n <span data-testid={`combat-readiness-value-${playerId}`}>\n {statusText}\n </span>\n </div>\n\n {/* 10-segment combat readiness bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`combat-readiness-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(readinessColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.3s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(readinessColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n\n {/* Breakdown tooltip (hover) - optional enhancement */}\n <div\n data-testid={`combat-readiness-breakdown-${playerId}`}\n style={{\n display: \"none\", // Hidden by default, can be shown on hover\n fontSize: `${fontSize - 2}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n marginTop: \"4px\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {/* Detailed breakdown for debugging/advanced players */}\n <div>Body: {player.bodyPartHealth ? \"tracked\" : \"aggregate\"}</div>\n <div>Pain: {Math.round(player.pain)}%</div>\n <div>Consciousness: {Math.round(player.consciousness)}%</div>\n <div>Balance: {Math.round(player.balance)}%</div>\n </div>\n </div>\n );\n};\n\nexport default CombatReadinessBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,sBAAyD,EACpE,QACA,UACA,eACI;CAEJ,MAAM,YAAY,cACV,yBAAyB,OAAO,EACtC,CAAC,OAAO,CACT;CAED,MAAM,WAAW;CACjB,MAAM,iBAAiB,uBAAuB,WAAW,SAAS;CAClE,MAAM,iBAAiB,wBAAwB,UAAU;CACzD,MAAM,iBAAiB,wBAAwB,UAAU;CACzD,MAAM,cAAc,YAAY;CAGhC,MAAM,WAAW,WAAW,MAAM;CAClC,MAAM,YAAY,WAAW,KAAK;CAClC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,aAAa;CAGxC,MAAM,aAAa,GAAG,UAAU,IAAI,eAAe;
|
|
1
|
+
{"version":3,"file":"CombatReadinessBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/CombatReadinessBar.tsx"],"sourcesContent":["/**\n * CombatReadinessBar Component - 10-bar segmented combat readiness display\n * \n * Displays comprehensive combat readiness with:\n * - 10 segmented bars representing 10% each\n * - Color transitions: Green (>80%), Yellow (60-79%), Orange (40-59%), Red (20-39%), Dark Red (<20%)\n * - Smooth 0.3s transition animations\n * - Korean/English bilingual labels\n * - Numeric percentage display\n * - Responsive sizing for mobile/tablet/desktop\n * - Real-time calculation from body health, pain, consciousness, and balance\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { \n calculateCombatReadiness, \n getCombatReadinessColor, \n getCombatReadinessLabel,\n getCombatReadinessBars\n} from \"../../../../utils/combatReadiness\";\nimport type { PlayerState } from \"../../../../systems/player\";\n\nexport interface CombatReadinessBarProps {\n /** Player state containing all combat factors */\n readonly player: PlayerState;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * CombatReadinessBar - 10-bar segmented combat readiness display with Korean theming\n * \n * Calculates and displays overall combat readiness from multiple factors:\n * - Body part health (40% weight)\n * - Pain level (20% weight)\n * - Consciousness (20% weight)\n * - Balance state (20% weight)\n * \n * @example\n * ```tsx\n * <CombatReadinessBar \n * player={playerState} \n * playerId=\"player-1\"\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReadinessBar: React.FC<CombatReadinessBarProps> = ({\n player,\n playerId,\n isMobile,\n}) => {\n // Calculate combat readiness percentage\n const readiness = useMemo(\n () => calculateCombatReadiness(player),\n [player]\n );\n\n const segments = 10;\n const filledSegments = getCombatReadinessBars(readiness, segments);\n const readinessColor = getCombatReadinessColor(readiness);\n const readinessLabel = getCombatReadinessLabel(readiness);\n const shouldPulse = readiness < 20;\n\n // Responsive sizing\n const barWidth = isMobile ? 180 : 250;\n const barHeight = isMobile ? 16 : 20;\n const fontSize = isMobile ? 11 : 13;\n const padding = isMobile ? \"8px 12px\" : \"12px 16px\";\n\n // Status text based on readiness level\n const statusText = `${readiness}% ${readinessLabel.korean}`;\n\n return (\n <div\n data-testid={`combat-readiness-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"전투 준비도 | Combat Readiness\"\n aria-valuenow={readiness}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuetext={`${readiness}% Combat Readiness - ${readinessLabel.english}`}\n style={{\n width: `${barWidth}px`,\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }}\n >\n {/* Label and numeric/status display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n }}\n >\n <span>전투 준비도 | Combat Readiness</span>\n <span data-testid={`combat-readiness-value-${playerId}`}>\n {statusText}\n </span>\n </div>\n\n {/* 10-segment combat readiness bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`combat-readiness-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(readinessColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.3s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(readinessColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n\n {/* Breakdown tooltip (hover) - optional enhancement */}\n <div\n data-testid={`combat-readiness-breakdown-${playerId}`}\n style={{\n display: \"none\", // Hidden by default, can be shown on hover\n fontSize: `${fontSize - 2}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n marginTop: \"4px\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {/* Detailed breakdown for debugging/advanced players */}\n <div>Body: {player.bodyPartHealth ? \"tracked\" : \"aggregate\"}</div>\n <div>Pain: {Math.round(player.pain)}%</div>\n <div>Consciousness: {Math.round(player.consciousness)}%</div>\n <div>Balance: {Math.round(player.balance)}%</div>\n </div>\n </div>\n );\n};\n\nexport default CombatReadinessBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,sBAAyD,EACpE,QACA,UACA,eACI;CAEJ,MAAM,YAAY,cACV,yBAAyB,OAAO,EACtC,CAAC,OAAO,CACT;CAED,MAAM,WAAW;CACjB,MAAM,iBAAiB,uBAAuB,WAAW,SAAS;CAClE,MAAM,iBAAiB,wBAAwB,UAAU;CACzD,MAAM,iBAAiB,wBAAwB,UAAU;CACzD,MAAM,cAAc,YAAY;CAGhC,MAAM,WAAW,WAAW,MAAM;CAClC,MAAM,YAAY,WAAW,KAAK;CAClC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,aAAa;CAGxC,MAAM,aAAa,GAAG,UAAU,IAAI,eAAe;CAEnD,OACE,qBAAC,OAAD;EACE,eAAa,wBAAwB;EACrC,MAAK;EACL,cAAW;EACX,iBAAe;EACf,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,UAAU,uBAAuB,eAAe;EACnE,OAAO;GACL,OAAO,GAAG,SAAS;GACnB;GACA,iBAAiB,gBAAgB,cAAc,oBAAoB,EAAE;GACrE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAE;GACnE,WAAW,YAAY,gBAAgB,cAAc,cAAc,GAAI;GACxE;YAfH;GAkBE,qBAAC,OAAD;IACE,OAAO;KACL,UAAU,GAAG,SAAS;KACtB,OAAO,gBAAgB,cAAc,cAAc,EAAE;KACrD,YAAY,YAAY;KACxB,cAAc;KACd,SAAS;KACT,gBAAgB;KAChB,YAAY;KACb;cATH,CAWE,oBAAC,QAAD,EAAA,UAAM,6BAAgC,CAAA,EACtC,oBAAC,QAAD;KAAM,eAAa,0BAA0B;eAC1C;KACI,CAAA,CACH;;GAGN,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,KAAK;KACL,QAAQ,GAAG,UAAU;KACrB,WAAW,cAAc,8BAA8B;KACxD;cAEA,MAAM,KAAK,EAAE,QAAQ,UAAU,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;KAEE,eAAa,4BAA4B,SAAS,GAAG;KACrD,OAAO;MACL,MAAM;MACN,iBACE,QAAQ,iBACJ,gBAAgB,gBAAgB,EAAE,GAClC,gBAAgB,cAAc,sBAAsB,EAAE;MAC5D,cAAc;MACd,YAAY;MACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,gBAAgB,GAAI,KAC/C;MACP;KACD,EAfK,MAeL,CACF;IACE,CAAA;GAGN,qBAAC,OAAD;IACE,eAAa,8BAA8B;IAC3C,OAAO;KACL,SAAS;KACT,UAAU,GAAG,WAAW,EAAE;KAC1B,OAAO,gBAAgB,cAAc,gBAAgB,EAAE;KACvD,WAAW;KACX,YAAY,YAAY;KACzB;cARH;KAWE,qBAAC,OAAD,EAAA,UAAA,CAAK,UAAO,OAAO,iBAAiB,YAAY,YAAkB,EAAA,CAAA;KAClE,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAO,KAAK,MAAM,OAAO,KAAK;MAAC;MAAO,EAAA,CAAA;KAC3C,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAgB,KAAK,MAAM,OAAO,cAAc;MAAC;MAAO,EAAA,CAAA;KAC7D,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAU,KAAK,MAAM,OAAO,QAAQ;MAAC;MAAO,EAAA,CAAA;KAC7C;;GACF"}
|