blacktrigram 0.7.39 → 0.7.41
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 +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCombatActions.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatActions.ts"],"sourcesContent":["/**\n * useCombatActions Hook - Combat Action Handlers\n *\n * Custom hook for managing combat action handlers.\n * Consolidates player attack, defend, technique, and AI action logic.\n *\n * Performance:\n * - Memoized callbacks to prevent recreation\n * - Centralized action logic for better maintainability\n * - Reduces main component complexity\n *\n * @param config Combat action configuration\n * @returns Combat action handlers\n *\n * @example\n * ```typescript\n * const {\n * handleAttack,\n * handleDefend,\n * handleTechniqueExecute,\n * handleStanceSwitch,\n * handleAIAttack,\n * handleAIDefend,\n * handleAITechnique,\n * moveAIPlayer\n * } = useCombatActions({\n * validPlayers,\n * playerPositions,\n * combatState,\n * combatActions,\n * combatSystem,\n * onPlayerUpdate,\n * addCombatMessage,\n * addHitEffect,\n * arenaBounds\n * });\n * ```\n */\n\nimport { PlayerState } from \"@/systems\";\nimport {\n AnimationType,\n getAnimationHitTiming,\n type AnimationState,\n} from \"@/systems/animation\";\nimport { movementPenaltySystem } from \"@/systems/bodypart\";\nimport { clampToArenaBounds, type PhysicsArenaBounds } from \"@/types/PhysicsTypes\";\nimport {\n checkForFall,\n getFallTypeName,\n} from \"@/systems/combat/FallIntegration\";\nimport type { CombatResult } from \"@/systems/combat/types\";\nimport { CombatSystem } from \"@/systems/CombatSystem\";\nimport { HitEffectType } from \"@/systems/effects\";\nimport { KnockbackPhysics } from \"@/systems/physics\";\nimport { StanceManager } from \"@/systems/trigram\";\nimport { KoreanTechniquesSystem } from \"@/systems/trigram/KoreanTechniques\";\nimport { getVitalPointById } from \"@/systems/vitalpoint/KoreanVitalPoints\";\nimport { KoreanTechnique } from \"@/systems/vitalpoint/types\";\nimport { Position, Technique, TrigramStance, BodyRegion } from \"@/types\";\nimport { Injury, InjuryType } from \"@/types/injury\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { AttackIntensity } from \"./useCombatAudio\";\nimport { CombatActions, CombatScreenState } from \"./useCombatState\";\n\n/**\n * Hit position variation range for randomizing strike heights\n * Produces ±0.2 absolute units (±10% of 2.0 character height)\n */\nconst HIT_Y_VARIATION_RANGE = 0.4;\n\nexport const SCREEN_SHAKE_FRAME_INTERVAL_MS = 50;\nexport const SCREEN_SHAKE_FRAME_COUNT = 5;\n\n/**\n * Clears every timeout in a tracked set and empties the set afterwards.\n *\n * @param timeoutIds Timeout identifiers to clear and remove\n */\nfunction clearTimeoutSet(timeoutIds: Set<ReturnType<typeof setTimeout>>): void {\n timeoutIds.forEach((timeoutId) => {\n clearTimeout(timeoutId);\n });\n timeoutIds.clear();\n}\n\n/**\n * Calculate randomized hit position based on defender position\n * Adds vertical variation to simulate different strike heights\n *\n * @param defenderPos - Position of the defender being struck\n * @returns Hit position with randomized Y coordinate\n */\nfunction calculateHitPosition(defenderPos: Position): { x: number; y: number } {\n const hitYVariation = (Math.random() - 0.5) * HIT_Y_VARIATION_RANGE; // ±0.2 units\n return {\n x: defenderPos.x,\n y: Math.max(0.3, Math.min(1.8, defenderPos.y + hitYVariation)),\n };\n}\n\n/**\n * Determine injury type from combat result and technique damage type\n * 전투 결과와 기술 피해 유형으로 부상 유형 결정\n *\n * @param result - Combat result with damage information\n * @param technique - Technique that was executed\n * @returns InjuryType to create\n */\nfunction determineInjuryType(\n result: CombatResult,\n technique: KoreanTechnique,\n): InjuryType {\n // Slashing damage creates cuts (damageType is a string, not enum)\n if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n // Heavy damage creates severe bruising\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n // Medium damage creates moderate bruising\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\n // Light damage creates light bruising\n return InjuryType.BRUISE;\n}\n\n/**\n * Map body region to 3D position offset on character model\n * 신체 부위를 캐릭터 모델의 3D 위치 오프셋으로 매핑\n *\n * @param region - Body region that was hit\n * @returns Position offset [x, y, z] relative to character center\n */\nfunction getBodyRegionPosition(region: BodyRegion): [number, number, number] {\n // Map body regions to approximate positions on character model\n // Character is ~2 units tall, centered at [0, 0, 0]\n switch (region) {\n case BodyRegion.HEAD:\n return [0, 1.6, 0];\n case BodyRegion.NECK:\n return [0, 1.3, 0];\n case BodyRegion.TORSO:\n case BodyRegion.CORE:\n return [0, 0.8, 0];\n case BodyRegion.LEFT_ARM:\n return [-0.4, 1.0, 0];\n case BodyRegion.RIGHT_ARM:\n return [0.4, 1.0, 0];\n case BodyRegion.LEFT_LEG:\n return [-0.2, 0.2, 0];\n case BodyRegion.RIGHT_LEG:\n return [0.2, 0.2, 0];\n default:\n return [0, 0.8, 0]; // Default to torso\n }\n}\n\n/**\n * Create injury from combat damage result\n * 전투 피해 결과로부터 부상 생성\n *\n * @param result - Combat result with damage details\n * @param technique - Technique that caused the damage\n * @param defenderHealth - Current defender health after damage (0-100 scale)\n * @param targetPlayerIndex - Index of the player who was hit (0 or 1)\n * @returns Injury object for visualization\n */\nfunction createInjuryFromDamage(\n result: CombatResult,\n technique: KoreanTechnique,\n defenderHealth: number,\n targetPlayerIndex: number,\n): Injury {\n // Determine body region - use torso as default if not specified\n const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n // Determine injury type based on damage and technique\n let injuryType = determineInjuryType(result, technique);\n\n // Promote to fracture when health is critically low and damage is severe\n // to align with TraumaOverlay3D fracture behavior\n const isLowHealth = defenderHealth <= 30; // 30% health threshold\n const isSevereDamage = result.damage >= 25; // Severe damage threshold\n if (isLowHealth && isSevereDamage && injuryType !== InjuryType.FRACTURE) {\n injuryType = InjuryType.FRACTURE;\n }\n\n // Calculate severity (0.0 to 1.0) based on damage\n // Normalized so that a ~30-damage hit is treated as near-max severity\n const severity = Math.min(1.0, result.damage / 30);\n\n // Get position on character model for this body region\n const basePosition = getBodyRegionPosition(bodyRegion);\n\n // Add small random offset for variety\n const randomOffset: [number, number, number] = [\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n ];\n\n const position: [number, number, number] = [\n basePosition[0] + randomOffset[0],\n basePosition[1] + randomOffset[1],\n basePosition[2] + randomOffset[2],\n ];\n\n return {\n id: `injury_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,\n region: bodyRegion,\n type: injuryType,\n position,\n severity,\n hitCount: 1,\n timestamp: Date.now(),\n playerId: targetPlayerIndex === 0 ? \"player\" : \"enemy\",\n };\n}\n\n/**\n * Apply knockback displacement to defender position\n *\n * **Korean**: 밀침 적용 (Apply Knockback)\n *\n * Updates the defender's position based on knockback physics calculation.\n * The knockback displacement is applied in the direction of the attack vector\n * (attacker → defender), respecting arena boundaries.\n *\n * **Physics-First Architecture**: Both positions and knockback displacement\n * are in meters. Arena bounds are centered at origin (0, 0) with extent\n * ±worldWidthMeters/2 in X and ±worldDepthMeters/2 in Z.\n *\n * @param result - Combat result containing knockback data (in meters)\n * @param defenderPos - Current defender position (in meters)\n * @param arenaBounds - Arena boundary limits with meter dimensions\n * @returns Updated defender position after knockback (in meters)\n *\n * @example\n * ```typescript\n * // 10m × 7.5m arena, player at x=2m knocked back 2.5m to right\n * const newPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 2, y: 0 }, // Current position: 2m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 4.5, y: 0 } (2m + 2.5m = 4.5m, within ±5m boundary)\n *\n * // Same knockback but would exceed boundary\n * const clampedPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 4, y: 0 }, // Current position: 4m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 5, y: 0 } (4m + 2.5m = 6.5m, clamped to +5m boundary)\n * ```\n */\nfunction applyKnockbackDisplacement(\n result: CombatResult,\n defenderPos: Position,\n arenaBounds: PhysicsArenaBounds,\n): Position {\n if (!result.knockback) {\n return defenderPos;\n }\n\n // Apply knockback displacement (both in meters)\n // Note: knockback.displacement.z maps to position.y in 2D arena\n const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\n // Clamp to arena boundaries using shared physics helper\n return clampToArenaBounds(newPos, arenaBounds);\n}\n\nexport interface UseCombatActionsConfig {\n readonly validPlayers: readonly [PlayerState, PlayerState];\n readonly playerPositions: readonly [Position, Position];\n readonly combatState: CombatScreenState;\n readonly combatActions: CombatActions;\n readonly combatSystem: CombatSystem;\n readonly onPlayerUpdate: (\n playerIndex: number,\n updates: Partial<PlayerState>,\n ) => void;\n readonly onPlayerPositionUpdate?: (\n playerIndex: number,\n position: Position,\n ) => void;\n readonly onLateralityUpdate?: (\n playerIndex: number,\n laterality: \"left\" | \"right\",\n ) => void;\n readonly onInjuryCreated?: (\n injury: Injury,\n targetPlayerIndex: number,\n ) => void;\n readonly addCombatMessage: (korean: string, english: string) => void;\n readonly addHitEffect: (\n type: HitEffectType,\n position: Position,\n intensity?: number,\n ) => void;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly combatAudio?: {\n readonly playAttackSound: (intensity?: AttackIntensity) => Promise<void>;\n readonly playHitSound: (damage: number) => Promise<void>;\n readonly playBoneImpactSound: (options: {\n region?: import(\"../../../../audio/types\").AudioBodyRegion;\n intensity?: import(\"../../../../audio/types\").ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly playBlockSound: (guardBroken?: boolean) => Promise<void>;\n readonly playDodgeSound: () => Promise<void>;\n readonly playStanceChangeSound: () => Promise<void>;\n readonly playSpecialTechniqueSound: () => Promise<void>;\n };\n readonly playerAnimations?: {\n readonly player1: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n readonly player2: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n };\n}\n\nexport interface UseCombatActionsReturn {\n readonly handleAttack: (technique?: Technique) => void;\n readonly handleDefend: () => void;\n readonly handleTechniqueExecute: () => void;\n readonly handleStanceSwitch: (stance: TrigramStance) => void;\n readonly handleStanceSideSwitch: (playerIndex: 0 | 1) => void;\n readonly handleAIAttack: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly handleAIDefend: () => void;\n readonly handleAITechnique: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly moveAIPlayer: (targetPos: Position) => void;\n}\n\n/**\n * Helper function to convert Technique to KoreanTechnique format\n * @param technique - The technique to convert\n * @param stance - Current player stance\n * @returns KoreanTechnique compatible with CombatSystem\n */\nfunction convertTechniqueToKorean(\n technique: Technique,\n stance: TrigramStance,\n): KoreanTechnique {\n return {\n id: technique.id,\n name: {\n korean: technique.name.korean,\n english: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n },\n koreanName: technique.name.korean,\n englishName: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n description: {\n korean: technique.description.korean,\n english: technique.description.english,\n },\n stance: technique.requiredStance ?? stance,\n type: \"attack\",\n damageType: technique.damageType,\n damage: (technique.damage.min + technique.damage.max) / 2, // Use average damage\n kiCost: technique.kiCost,\n staminaCost: technique.staminaCost,\n accuracy: 0.85, // Default accuracy\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.9,\n },\n executionTime: technique.animationDuration ?? 400,\n recoveryTime: 300,\n critChance: technique.criticalChance ?? 0.1,\n critMultiplier: 1.5,\n effects: [],\n };\n}\n\n/**\n * Custom hook for combat action handlers\n */\nexport function useCombatActions(\n config: UseCombatActionsConfig,\n): UseCombatActionsReturn {\n const {\n validPlayers,\n playerPositions,\n combatState,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onLateralityUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n arenaBounds,\n combatAudio,\n } = config;\n\n // Refs to track knockback recovery timeouts for cleanup\n const player1KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const player2KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const screenShakeTimeoutsRef = useRef<Set<ReturnType<typeof setTimeout>>>(\n new Set(),\n );\n\n // Cleanup timeouts on unmount\n useEffect(() => {\n const screenShakeTimeouts = screenShakeTimeoutsRef.current;\n return () => {\n clearTimeoutSet(screenShakeTimeouts);\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n };\n }, []);\n\n // Player attack handler\n const handleAttack = useCallback(\n (technique?: Technique) => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n\n const player = validPlayers[0];\n const currentStance = player.currentStance;\n const archetype = player.archetype;\n\n // Use provided technique or select from stance techniques\n let attackTechnique: KoreanTechnique;\n\n if (technique) {\n // Convert selected technique to KoreanTechnique format\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\n // Get techniques for current stance and archetype\n const availableTechniques =\n KoreanTechniquesSystem.getAllAvailableTechniques(\n currentStance,\n archetype,\n );\n\n if (availableTechniques.length === 0) {\n console.warn(\n `No techniques found for stance: ${currentStance}, archetype: ${archetype}`,\n );\n addCombatMessage(\"기술 없음\", \"No techniques available\");\n return;\n }\n\n // Select primary technique (first in list)\n const selectedTechnique = availableTechniques[0];\n\n // Check if player has sufficient resources\n if (\n !KoreanTechniquesSystem.canExecuteTechnique(player, selectedTechnique)\n ) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n attackTechnique = selectedTechnique;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play attack sound based on technique damage/intensity\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for hit detection.\n // For synchronous hit detection, we use the animation's peak time (when limb\n // is fully extended) rather than t=0 (attack start). This ensures the hit\n // window check passes when the attack would visually connect.\n // 동기식 타격 판정: 애니메이션 피크 타임 사용 (팔/다리 완전 신전 시점)\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15; // Default to typical punch peak\n const animationContext = {\n animationType,\n currentTime: peakTime, // Use peak time for synchronous hit detection\n };\n\n // Use combat system for proper calculation with animation context\n const result = combatSystem.resolveAttack(\n validPlayers[0],\n validPlayers[1],\n attackTechnique,\n undefined,\n animationContext,\n );\n\n const effectType = result.hit\n ? result.isCritical\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[0], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound with body region and damage context\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n // Use bone impact audio instead of generic hit sound\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[1].health - result.damage,\n vitalPoint: result.isCritical, // Critical hits are often vital points\n hitPosition,\n });\n\n // Combo tracking: reset combo if too much time passed\n const now = Date.now();\n const timeSinceLastHit = now - combatState.lastHitTime;\n const newCombo =\n timeSinceLastHit < 2000 ? combatState.comboCount + 1 : 1;\n combatActions.setComboCount(newCombo);\n combatActions.setLastHitTime(now);\n\n // Apply damage through combat system\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(\n result,\n validPlayers[0],\n validPlayers[1],\n );\n\n onPlayerUpdate(0, updatedAttacker);\n onPlayerUpdate(1, updatedDefender);\n\n // Create injury for trauma visualization\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 1, // Player 2 (enemy) was hit\n );\n onInjuryCreated(injury, 1);\n }\n\n // Apply knockback displacement (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[1],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(1, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(knockbackName.korean, knockbackName.english);\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 2\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player2KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(1, { isStunned: false });\n player2KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if defender should fall after taking damage\n if (config.playerAnimations?.player2) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player2.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Display technique name in combat log\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n\n if (result.isCritical) {\n addCombatMessage(\n `치명타! ${techniqueNameKorean}`,\n `Critical Hit! ${techniqueNameEnglish}`,\n );\n } else if (newCombo > 2) {\n addCombatMessage(\n `${newCombo} 연속! ${techniqueNameKorean}`,\n `${newCombo} Combo! ${techniqueNameEnglish}`,\n );\n } else {\n addCombatMessage(\n `${techniqueNameKorean} 성공!`,\n `${techniqueNameEnglish} Hit!`,\n );\n }\n } else {\n combatActions.resetCombo();\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n addCombatMessage(\n `${techniqueNameKorean} 빗나감`,\n `${techniqueNameEnglish} Missed`,\n );\n }\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 500);\n },\n [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.comboCount,\n combatState.lastHitTime,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n config,\n ],\n );\n\n // Player defend handler\n const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(0, { isBlocking: true });\n addCombatMessage(\"방어 자세\", \"Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[0], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(0, { isBlocking: false });\n }, 1000);\n }, [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n // Player technique handler\n const handleTechniqueExecute = useCallback(() => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n if (validPlayers[0].ki < 10 || validPlayers[0].stamina < 15) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\n // Screen shake effect for impact\n clearTimeoutSet(screenShakeTimeoutsRef.current);\n const shakeIntensity = 8;\n const shakeFrames = [\n { x: shakeIntensity, y: -shakeIntensity * 0.5 },\n { x: -shakeIntensity * 0.7, y: shakeIntensity * 0.8 },\n { x: shakeIntensity * 0.5, y: shakeIntensity * 0.3 },\n { x: -shakeIntensity * 0.3, y: -shakeIntensity * 0.6 },\n { x: 0, y: 0 },\n ].slice(0, SCREEN_SHAKE_FRAME_COUNT);\n\n shakeFrames.forEach((shake, index) => {\n const timeoutId = setTimeout(\n () => {\n // Completed timers are removed so cleanup only tracks pending callbacks.\n screenShakeTimeoutsRef.current.delete(timeoutId);\n combatActions.setScreenShake(shake);\n },\n index * SCREEN_SHAKE_FRAME_INTERVAL_MS,\n );\n screenShakeTimeoutsRef.current.add(timeoutId);\n });\n\n const distance = Math.sqrt(\n Math.pow(playerPositions[0].x - playerPositions[1].x, 2) +\n Math.pow(playerPositions[0].y - playerPositions[1].y, 2),\n );\n\n if (distance < 150) {\n // Play bone impact sound for special technique hit\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n combatAudio?.playBoneImpactSound({\n damage: 25,\n remainingHealth: validPlayers[1].health - 25,\n vitalPoint: false,\n hitPosition,\n });\n\n onPlayerUpdate(1, {\n health: Math.max(0, validPlayers[1].health - 25),\n hitsTaken: validPlayers[1].hitsTaken + 1,\n });\n addCombatMessage(\"특수 기술 성공!\", \"Special Technique Hit!\");\n } else {\n addCombatMessage(\"기술 실패\", \"Technique Failed\");\n }\n\n // Consume resources\n onPlayerUpdate(0, {\n ki: Math.max(0, validPlayers[0].ki - 10),\n stamina: Math.max(0, validPlayers[0].stamina - 15),\n });\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 800);\n }, [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatActions,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n ]);\n\n // Player stance switch handler\n const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play stance change sound\n combatAudio?.playStanceChangeSound();\n\n onPlayerUpdate(0, { currentStance: stance });\n addCombatMessage(`자세 변경: ${stance}`, `Stance Change: ${stance}`);\n addHitEffect(HitEffectType.STATUS_EFFECT, playerPositions[0], 0.6);\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ],\n );\n\n /**\n * Handle stance side switch (left/right)\n * @korean 자세측면전환처리\n */\n // Reuse StanceManager instance for stance side switches\n const stanceManagerRef = useRef<StanceManager>(new StanceManager());\n\n const handleStanceSideSwitch = useCallback(\n (playerIndex: 0 | 1) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n const player = validPlayers[playerIndex];\n // Get current laterality from combat state\n const currentLaterality = combatState.playerLaterality[playerIndex];\n\n const result = stanceManagerRef.current.switchStanceSide(\n player,\n currentLaterality,\n );\n\n if (result.success && result.laterality) {\n // Update player state with new stamina\n onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n // Update laterality in combat state via callback\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n // Audio feedback\n combatAudio?.playStanceChangeSound?.();\n\n // Visual feedback\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n // Visual effect\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\n // Feedback for failed switch\n if (result.message?.includes(\"stamina\")) {\n addCombatMessage(\"체력 부족\", \"Insufficient Stamina\");\n } else if (result.message?.includes(\"cooldown\")) {\n addCombatMessage(\"대기 중\", \"On Cooldown\");\n }\n }\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.playerLaterality,\n validPlayers,\n onPlayerUpdate,\n onLateralityUpdate,\n combatAudio,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n ],\n );\n\n /**\n * Helper function to create AI technique objects\n * Reduces code duplication between basic attacks and special techniques\n */\n const createAITechnique = useCallback(\n (type: \"basic\" | \"special\", aiPlayer: PlayerState) => {\n if (type === \"basic\") {\n return {\n id: \"ai_basic_attack\",\n name: {\n korean: \"AI 기본공격\",\n english: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n },\n koreanName: \"AI 기본공격\",\n englishName: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n description: { korean: \"AI 기본 공격\", english: \"AI basic attack\" },\n stance: aiPlayer.currentStance,\n type: \"attack\" as const,\n damageType: \"physical\" as const,\n damage: 15,\n kiCost: 5,\n staminaCost: 8,\n accuracy: 0.8,\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.95,\n },\n executionTime: 400,\n recoveryTime: 300,\n critChance: 0.1,\n critMultiplier: 1.5,\n effects: [],\n animationType: AnimationType.JAB, // Default animation for basic attack\n };\n } else {\n return {\n id: \"ai_special_technique\",\n name: {\n korean: \"AI 특수기술\",\n english: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n },\n koreanName: \"AI 특수기술\",\n englishName: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n description: {\n korean: \"AI 특수 기술\",\n english: \"AI special technique\",\n },\n stance: aiPlayer.currentStance,\n type: \"technique\" as const,\n damageType: \"physical\" as const,\n damage: 25,\n kiCost: 10,\n staminaCost: 15,\n accuracy: 0.85,\n reachConfig: {\n bodyPart: \"leg\" as const,\n techniqueType: \"kick\" as const,\n baseExtension: 1.1,\n },\n executionTime: 600,\n recoveryTime: 800,\n critChance: 0.15,\n critMultiplier: 1.8,\n effects: [],\n animationType: AnimationType.SPINNING_HOOK, // Default animation for special technique\n };\n }\n },\n [],\n );\n\n /**\n * Helper function to determine hit effect type based on combat result\n * Reduces duplication between attack and technique handlers\n */\n const getHitEffectType = useCallback(\n (result: { hit: boolean; isCritical?: boolean }): HitEffectType => {\n if (!result.hit) return HitEffectType.MISS;\n return result.isCritical ? HitEffectType.CRITICAL_HIT : HitEffectType.HIT;\n },\n [],\n );\n\n /**\n * AI attack handler with technique and vital point targeting\n *\n * @param technique - Optional Korean martial arts technique to execute. If not provided, creates a basic attack.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAIAttack = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create basic attack technique\n const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\n // Play attack sound based on technique damage/intensity (consistent with player)\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for AI hit detection (same as player)\n // 동기식 타격 판정: AI도 피크 타임 사용\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15;\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n attackTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = getHitEffectType(result);\n addHitEffect(effectType, playerPositions[1], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI hits on player\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical,\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI attacks (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI ${knockbackName.korean}`,\n `AI ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 급소 타격! ${vpName}`,\n `AI Vital Point Hit! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else if (result.isCritical) {\n addCombatMessage(\"AI 치명타!\", \"AI Critical Hit!\");\n } else {\n addCombatMessage(\"AI 공격 성공!\", \"AI Attack Hit!\");\n }\n } else {\n // Consume resources on miss for consistency with technique behavior\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - attackTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - attackTechnique.staminaCost),\n });\n addCombatMessage(\"AI 공격 빗나감\", \"AI Attack Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n createAITechnique,\n getHitEffectType,\n config,\n ],\n );\n\n // AI defend handler\n const handleAIDefend = useCallback(() => {\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(1, { isBlocking: true });\n addCombatMessage(\"AI 방어 자세\", \"AI Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[1], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(1, { isBlocking: false });\n }, 1000);\n }, [\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n /**\n * AI technique handler with technique and vital point targeting\n *\n * @param technique - Optional special Korean martial arts technique to execute. If not provided, creates a special technique.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAITechnique = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create special technique\n const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\n // Check if AI has sufficient resources for the technique\n if (\n aiPlayer.ki < specialTechnique.kiCost ||\n aiPlayer.stamina < specialTechnique.staminaCost\n ) {\n handleAIAttack(undefined, targetVitalPoint); // Fallback to basic attack with same targeting\n return;\n }\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n // Calculate animation timing context for AI technique hit detection\n // 동기식 타격 판정: AI 특수 기술도 피크 타임 사용\n const animationType =\n specialTechnique.animationType ?? AnimationType.SPINNING_HOOK;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.25; // Special techniques often have longer peak times\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n specialTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = result.hit\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[1], result.hit ? 1.5 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI technique hits\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical === true || !!targetVitalPoint, // Special techniques often target vital points\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI technique hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n specialTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI technique\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI special techniques (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI 특수 ${knockbackName.korean}`,\n `AI Special ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI technique\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 특수 급소 기술! ${vpName}`,\n `AI Special Vital Point Technique! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else {\n addCombatMessage(\"AI 특수 기술!\", \"AI Special Technique!\");\n }\n } else {\n // Consume resources on miss (technique was attempted)\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - specialTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - specialTechnique.staminaCost),\n });\n addCombatMessage(\"AI 기술 빗나감\", \"AI Technique Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n handleAIAttack,\n combatAudio,\n createAITechnique,\n config,\n ],\n );\n\n // AI movement handler with injury-based movement penalties\n // **UPDATED**: Now scale-aware for consistent movement and distance calculations\n // **FIX**: Positions are in METERS, not pixels - use meters-based speed\n const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n // Movement speed calibrated for physics-first system (all in METERS)\n // Combat closing speed: ~2.5 m/s (fast tactical approach, not slow walking)\n // Real fights are over in 4-5 seconds - AI must close distance quickly\n // AI decision loop frequency (defined in useAICombat.ts)\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n // Calculation: 2.5 m/s / 20 calls/s = 0.125 meters per call\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\n // Calculate movement direction vector (in meters)\n const dx = targetPos.x - currentPos.x;\n const dy = targetPos.y - currentPos.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // Apply movement penalties from leg injuries if body part health exists\n let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n // Normalize movement direction\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n // Calculate modified speed with all penalties applied\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\n // Physics-first: positions are in METERS, so distance is in meters\n // Stop moving when within 0.05 meters (5cm) of target - close enough for melee range\n const MIN_MOVEMENT_THRESHOLD_METERS = 0.05;\n\n if (distance > MIN_MOVEMENT_THRESHOLD_METERS) {\n const newPos = {\n x: currentPos.x + (dx / distance) * finalSpeed,\n y: currentPos.y + (dy / distance) * finalSpeed,\n };\n\n // Keep AI within arena bounds (positions in meters, centered at origin)\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n newPos.x = Math.max(-halfWidth, Math.min(halfWidth, newPos.x));\n newPos.y = Math.max(-halfDepth, Math.min(halfDepth, newPos.y));\n\n // Update position through parent - this should trigger playerPositions state update in parent\n onPlayerUpdate(1, { position: newPos });\n }\n },\n [playerPositions, validPlayers, arenaBounds, onPlayerUpdate],\n );\n\n return {\n handleAttack,\n handleDefend,\n handleTechniqueExecute,\n handleStanceSwitch,\n handleStanceSideSwitch,\n handleAIAttack,\n handleAIDefend,\n handleAITechnique,\n moveAIPlayer,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useCombatActions.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatActions.ts"],"sourcesContent":["/**\n * useCombatActions Hook - Combat Action Handlers\n *\n * Custom hook for managing combat action handlers.\n * Consolidates player attack, defend, technique, and AI action logic.\n *\n * Performance:\n * - Memoized callbacks to prevent recreation\n * - Centralized action logic for better maintainability\n * - Reduces main component complexity\n *\n * @param config Combat action configuration\n * @returns Combat action handlers\n *\n * @example\n * ```typescript\n * const {\n * handleAttack,\n * handleDefend,\n * handleTechniqueExecute,\n * handleStanceSwitch,\n * handleAIAttack,\n * handleAIDefend,\n * handleAITechnique,\n * moveAIPlayer\n * } = useCombatActions({\n * validPlayers,\n * playerPositions,\n * combatState,\n * combatActions,\n * combatSystem,\n * onPlayerUpdate,\n * addCombatMessage,\n * addHitEffect,\n * arenaBounds\n * });\n * ```\n */\n\nimport { PlayerState } from \"@/systems\";\nimport {\n AnimationType,\n getAnimationHitTiming,\n type AnimationState,\n} from \"@/systems/animation\";\nimport { movementPenaltySystem } from \"@/systems/bodypart\";\nimport { clampToArenaBounds, type PhysicsArenaBounds } from \"@/types/PhysicsTypes\";\nimport {\n checkForFall,\n getFallTypeName,\n} from \"@/systems/combat/FallIntegration\";\nimport type { CombatResult } from \"@/systems/combat/types\";\nimport { CombatSystem } from \"@/systems/CombatSystem\";\nimport { HitEffectType } from \"@/systems/effects\";\nimport { KnockbackPhysics } from \"@/systems/physics\";\nimport { StanceManager } from \"@/systems/trigram\";\nimport { KoreanTechniquesSystem } from \"@/systems/trigram/KoreanTechniques\";\nimport { getVitalPointById } from \"@/systems/vitalpoint/KoreanVitalPoints\";\nimport { KoreanTechnique } from \"@/systems/vitalpoint/types\";\nimport { Position, Technique, TrigramStance, BodyRegion } from \"@/types\";\nimport { Injury, InjuryType } from \"@/types/injury\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { AttackIntensity } from \"./useCombatAudio\";\nimport { CombatActions, CombatScreenState } from \"./useCombatState\";\n\n/**\n * Hit position variation range for randomizing strike heights\n * Produces ±0.2 absolute units (±10% of 2.0 character height)\n */\nconst HIT_Y_VARIATION_RANGE = 0.4;\n\nexport const SCREEN_SHAKE_FRAME_INTERVAL_MS = 50;\nexport const SCREEN_SHAKE_FRAME_COUNT = 5;\n\n/**\n * Clears every timeout in a tracked set and empties the set afterwards.\n *\n * @param timeoutIds Timeout identifiers to clear and remove\n */\nfunction clearTimeoutSet(timeoutIds: Set<ReturnType<typeof setTimeout>>): void {\n timeoutIds.forEach((timeoutId) => {\n clearTimeout(timeoutId);\n });\n timeoutIds.clear();\n}\n\n/**\n * Calculate randomized hit position based on defender position\n * Adds vertical variation to simulate different strike heights\n *\n * @param defenderPos - Position of the defender being struck\n * @returns Hit position with randomized Y coordinate\n */\nfunction calculateHitPosition(defenderPos: Position): { x: number; y: number } {\n const hitYVariation = (Math.random() - 0.5) * HIT_Y_VARIATION_RANGE; // ±0.2 units\n return {\n x: defenderPos.x,\n y: Math.max(0.3, Math.min(1.8, defenderPos.y + hitYVariation)),\n };\n}\n\n/**\n * Determine injury type from combat result and technique damage type\n * 전투 결과와 기술 피해 유형으로 부상 유형 결정\n *\n * @param result - Combat result with damage information\n * @param technique - Technique that was executed\n * @returns InjuryType to create\n */\nfunction determineInjuryType(\n result: CombatResult,\n technique: KoreanTechnique,\n): InjuryType {\n // Slashing damage creates cuts (damageType is a string, not enum)\n if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n // Heavy damage creates severe bruising\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n // Medium damage creates moderate bruising\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\n // Light damage creates light bruising\n return InjuryType.BRUISE;\n}\n\n/**\n * Map body region to 3D position offset on character model\n * 신체 부위를 캐릭터 모델의 3D 위치 오프셋으로 매핑\n *\n * @param region - Body region that was hit\n * @returns Position offset [x, y, z] relative to character center\n */\nfunction getBodyRegionPosition(region: BodyRegion): [number, number, number] {\n // Map body regions to approximate positions on character model\n // Character is ~2 units tall, centered at [0, 0, 0]\n switch (region) {\n case BodyRegion.HEAD:\n return [0, 1.6, 0];\n case BodyRegion.NECK:\n return [0, 1.3, 0];\n case BodyRegion.TORSO:\n case BodyRegion.CORE:\n return [0, 0.8, 0];\n case BodyRegion.LEFT_ARM:\n return [-0.4, 1.0, 0];\n case BodyRegion.RIGHT_ARM:\n return [0.4, 1.0, 0];\n case BodyRegion.LEFT_LEG:\n return [-0.2, 0.2, 0];\n case BodyRegion.RIGHT_LEG:\n return [0.2, 0.2, 0];\n default:\n return [0, 0.8, 0]; // Default to torso\n }\n}\n\n/**\n * Create injury from combat damage result\n * 전투 피해 결과로부터 부상 생성\n *\n * @param result - Combat result with damage details\n * @param technique - Technique that caused the damage\n * @param defenderHealth - Current defender health after damage (0-100 scale)\n * @param targetPlayerIndex - Index of the player who was hit (0 or 1)\n * @returns Injury object for visualization\n */\nfunction createInjuryFromDamage(\n result: CombatResult,\n technique: KoreanTechnique,\n defenderHealth: number,\n targetPlayerIndex: number,\n): Injury {\n // Determine body region - use torso as default if not specified\n const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n // Determine injury type based on damage and technique\n let injuryType = determineInjuryType(result, technique);\n\n // Promote to fracture when health is critically low and damage is severe\n // to align with TraumaOverlay3D fracture behavior\n const isLowHealth = defenderHealth <= 30; // 30% health threshold\n const isSevereDamage = result.damage >= 25; // Severe damage threshold\n if (isLowHealth && isSevereDamage && injuryType !== InjuryType.FRACTURE) {\n injuryType = InjuryType.FRACTURE;\n }\n\n // Calculate severity (0.0 to 1.0) based on damage\n // Normalized so that a ~30-damage hit is treated as near-max severity\n const severity = Math.min(1.0, result.damage / 30);\n\n // Get position on character model for this body region\n const basePosition = getBodyRegionPosition(bodyRegion);\n\n // Add small random offset for variety\n const randomOffset: [number, number, number] = [\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n (Math.random() - 0.5) * 0.1,\n ];\n\n const position: [number, number, number] = [\n basePosition[0] + randomOffset[0],\n basePosition[1] + randomOffset[1],\n basePosition[2] + randomOffset[2],\n ];\n\n return {\n id: `injury_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,\n region: bodyRegion,\n type: injuryType,\n position,\n severity,\n hitCount: 1,\n timestamp: Date.now(),\n playerId: targetPlayerIndex === 0 ? \"player\" : \"enemy\",\n };\n}\n\n/**\n * Apply knockback displacement to defender position\n *\n * **Korean**: 밀침 적용 (Apply Knockback)\n *\n * Updates the defender's position based on knockback physics calculation.\n * The knockback displacement is applied in the direction of the attack vector\n * (attacker → defender), respecting arena boundaries.\n *\n * **Physics-First Architecture**: Both positions and knockback displacement\n * are in meters. Arena bounds are centered at origin (0, 0) with extent\n * ±worldWidthMeters/2 in X and ±worldDepthMeters/2 in Z.\n *\n * @param result - Combat result containing knockback data (in meters)\n * @param defenderPos - Current defender position (in meters)\n * @param arenaBounds - Arena boundary limits with meter dimensions\n * @returns Updated defender position after knockback (in meters)\n *\n * @example\n * ```typescript\n * // 10m × 7.5m arena, player at x=2m knocked back 2.5m to right\n * const newPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 2, y: 0 }, // Current position: 2m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 4.5, y: 0 } (2m + 2.5m = 4.5m, within ±5m boundary)\n *\n * // Same knockback but would exceed boundary\n * const clampedPosition = applyKnockbackDisplacement(\n * result, // knockback.displacement.x = 2.5m\n * { x: 4, y: 0 }, // Current position: 4m from center\n * { worldWidthMeters: 10, worldDepthMeters: 7.5, ... }\n * );\n * // Returns: { x: 5, y: 0 } (4m + 2.5m = 6.5m, clamped to +5m boundary)\n * ```\n */\nfunction applyKnockbackDisplacement(\n result: CombatResult,\n defenderPos: Position,\n arenaBounds: PhysicsArenaBounds,\n): Position {\n if (!result.knockback) {\n return defenderPos;\n }\n\n // Apply knockback displacement (both in meters)\n // Note: knockback.displacement.z maps to position.y in 2D arena\n const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\n // Clamp to arena boundaries using shared physics helper\n return clampToArenaBounds(newPos, arenaBounds);\n}\n\nexport interface UseCombatActionsConfig {\n readonly validPlayers: readonly [PlayerState, PlayerState];\n readonly playerPositions: readonly [Position, Position];\n readonly combatState: CombatScreenState;\n readonly combatActions: CombatActions;\n readonly combatSystem: CombatSystem;\n readonly onPlayerUpdate: (\n playerIndex: number,\n updates: Partial<PlayerState>,\n ) => void;\n readonly onPlayerPositionUpdate?: (\n playerIndex: number,\n position: Position,\n ) => void;\n readonly onLateralityUpdate?: (\n playerIndex: number,\n laterality: \"left\" | \"right\",\n ) => void;\n readonly onInjuryCreated?: (\n injury: Injury,\n targetPlayerIndex: number,\n ) => void;\n readonly addCombatMessage: (korean: string, english: string) => void;\n readonly addHitEffect: (\n type: HitEffectType,\n position: Position,\n intensity?: number,\n ) => void;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly combatAudio?: {\n readonly playAttackSound: (intensity?: AttackIntensity) => Promise<void>;\n readonly playHitSound: (damage: number) => Promise<void>;\n readonly playBoneImpactSound: (options: {\n region?: import(\"../../../../audio/types\").AudioBodyRegion;\n intensity?: import(\"../../../../audio/types\").ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly playBlockSound: (guardBroken?: boolean) => Promise<void>;\n readonly playDodgeSound: () => Promise<void>;\n readonly playStanceChangeSound: () => Promise<void>;\n readonly playSpecialTechniqueSound: () => Promise<void>;\n };\n readonly playerAnimations?: {\n readonly player1: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n readonly player2: {\n readonly transitionTo: (state: AnimationState) => boolean;\n };\n };\n}\n\nexport interface UseCombatActionsReturn {\n readonly handleAttack: (technique?: Technique) => void;\n readonly handleDefend: () => void;\n readonly handleTechniqueExecute: () => void;\n readonly handleStanceSwitch: (stance: TrigramStance) => void;\n readonly handleStanceSideSwitch: (playerIndex: 0 | 1) => void;\n readonly handleAIAttack: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly handleAIDefend: () => void;\n readonly handleAITechnique: (\n technique?: KoreanTechnique,\n targetVitalPoint?: string,\n ) => void;\n readonly moveAIPlayer: (targetPos: Position) => void;\n}\n\n/**\n * Helper function to convert Technique to KoreanTechnique format\n * @param technique - The technique to convert\n * @param stance - Current player stance\n * @returns KoreanTechnique compatible with CombatSystem\n */\nfunction convertTechniqueToKorean(\n technique: Technique,\n stance: TrigramStance,\n): KoreanTechnique {\n return {\n id: technique.id,\n name: {\n korean: technique.name.korean,\n english: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n },\n koreanName: technique.name.korean,\n englishName: technique.name.english,\n romanized: technique.name.romanized ?? \"\",\n description: {\n korean: technique.description.korean,\n english: technique.description.english,\n },\n stance: technique.requiredStance ?? stance,\n type: \"attack\",\n damageType: technique.damageType,\n damage: (technique.damage.min + technique.damage.max) / 2, // Use average damage\n kiCost: technique.kiCost,\n staminaCost: technique.staminaCost,\n accuracy: 0.85, // Default accuracy\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.9,\n },\n executionTime: technique.animationDuration ?? 400,\n recoveryTime: 300,\n critChance: technique.criticalChance ?? 0.1,\n critMultiplier: 1.5,\n effects: [],\n };\n}\n\n/**\n * Custom hook for combat action handlers\n */\nexport function useCombatActions(\n config: UseCombatActionsConfig,\n): UseCombatActionsReturn {\n const {\n validPlayers,\n playerPositions,\n combatState,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onLateralityUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n arenaBounds,\n combatAudio,\n } = config;\n\n // Refs to track knockback recovery timeouts for cleanup\n const player1KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const player2KnockbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const screenShakeTimeoutsRef = useRef<Set<ReturnType<typeof setTimeout>>>(\n new Set(),\n );\n\n // Cleanup timeouts on unmount\n useEffect(() => {\n const screenShakeTimeouts = screenShakeTimeoutsRef.current;\n return () => {\n clearTimeoutSet(screenShakeTimeouts);\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n };\n }, []);\n\n // Player attack handler\n const handleAttack = useCallback(\n (technique?: Technique) => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n\n const player = validPlayers[0];\n const currentStance = player.currentStance;\n const archetype = player.archetype;\n\n // Use provided technique or select from stance techniques\n let attackTechnique: KoreanTechnique;\n\n if (technique) {\n // Convert selected technique to KoreanTechnique format\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\n // Get techniques for current stance and archetype\n const availableTechniques =\n KoreanTechniquesSystem.getAllAvailableTechniques(\n currentStance,\n archetype,\n );\n\n if (availableTechniques.length === 0) {\n console.warn(\n `No techniques found for stance: ${currentStance}, archetype: ${archetype}`,\n );\n addCombatMessage(\"기술 없음\", \"No techniques available\");\n return;\n }\n\n // Select primary technique (first in list)\n const selectedTechnique = availableTechniques[0];\n\n // Check if player has sufficient resources\n if (\n !KoreanTechniquesSystem.canExecuteTechnique(player, selectedTechnique)\n ) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n attackTechnique = selectedTechnique;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play attack sound based on technique damage/intensity\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for hit detection.\n // For synchronous hit detection, we use the animation's peak time (when limb\n // is fully extended) rather than t=0 (attack start). This ensures the hit\n // window check passes when the attack would visually connect.\n // 동기식 타격 판정: 애니메이션 피크 타임 사용 (팔/다리 완전 신전 시점)\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15; // Default to typical punch peak\n const animationContext = {\n animationType,\n currentTime: peakTime, // Use peak time for synchronous hit detection\n };\n\n // Use combat system for proper calculation with animation context\n const result = combatSystem.resolveAttack(\n validPlayers[0],\n validPlayers[1],\n attackTechnique,\n undefined,\n animationContext,\n );\n\n const effectType = result.hit\n ? result.isCritical\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[0], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound with body region and damage context\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n // Use bone impact audio instead of generic hit sound\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[1].health - result.damage,\n vitalPoint: result.isCritical, // Critical hits are often vital points\n hitPosition,\n });\n\n // Combo tracking: reset combo if too much time passed\n const now = Date.now();\n const timeSinceLastHit = now - combatState.lastHitTime;\n const newCombo =\n timeSinceLastHit < 2000 ? combatState.comboCount + 1 : 1;\n combatActions.setComboCount(newCombo);\n combatActions.setLastHitTime(now);\n\n // Apply damage through combat system\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(\n result,\n validPlayers[0],\n validPlayers[1],\n );\n\n onPlayerUpdate(0, updatedAttacker);\n onPlayerUpdate(1, updatedDefender);\n\n // Create injury for trauma visualization\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 1, // Player 2 (enemy) was hit\n );\n onInjuryCreated(injury, 1);\n }\n\n // Apply knockback displacement (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[1],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(1, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(knockbackName.korean, knockbackName.english);\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 2\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player2KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(1, { isStunned: false });\n player2KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if defender should fall after taking damage\n if (config.playerAnimations?.player2) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player2.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Display technique name in combat log\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n\n if (result.isCritical) {\n addCombatMessage(\n `치명타! ${techniqueNameKorean}`,\n `Critical Hit! ${techniqueNameEnglish}`,\n );\n } else if (newCombo > 2) {\n addCombatMessage(\n `${newCombo} 연속! ${techniqueNameKorean}`,\n `${newCombo} Combo! ${techniqueNameEnglish}`,\n );\n } else {\n addCombatMessage(\n `${techniqueNameKorean} 성공!`,\n `${techniqueNameEnglish} Hit!`,\n );\n }\n } else {\n combatActions.resetCombo();\n const techniqueNameKorean =\n attackTechnique.koreanName ?? attackTechnique.name.korean;\n const techniqueNameEnglish =\n attackTechnique.englishName ?? attackTechnique.name.english;\n addCombatMessage(\n `${techniqueNameKorean} 빗나감`,\n `${techniqueNameEnglish} Missed`,\n );\n }\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 500);\n },\n [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.comboCount,\n combatState.lastHitTime,\n combatActions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n config,\n ],\n );\n\n // Player defend handler\n const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(0, { isBlocking: true });\n addCombatMessage(\"방어 자세\", \"Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[0], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(0, { isBlocking: false });\n }, 1000);\n }, [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n // Player technique handler\n const handleTechniqueExecute = useCallback(() => {\n if (\n combatState.isExecutingTechnique ||\n !combatState.roundStarted ||\n combatState.roundEnded\n )\n return;\n if (validPlayers[0].ki < 10 || validPlayers[0].stamina < 15) {\n addCombatMessage(\"기력/체력 부족\", \"Insufficient Ki/Stamina\");\n return;\n }\n\n combatActions.setExecutingTechnique(true);\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\n // Screen shake effect for impact\n clearTimeoutSet(screenShakeTimeoutsRef.current);\n const shakeIntensity = 8;\n const shakeFrames = [\n { x: shakeIntensity, y: -shakeIntensity * 0.5 },\n { x: -shakeIntensity * 0.7, y: shakeIntensity * 0.8 },\n { x: shakeIntensity * 0.5, y: shakeIntensity * 0.3 },\n { x: -shakeIntensity * 0.3, y: -shakeIntensity * 0.6 },\n { x: 0, y: 0 },\n ].slice(0, SCREEN_SHAKE_FRAME_COUNT);\n\n shakeFrames.forEach((shake, index) => {\n const timeoutId = setTimeout(\n () => {\n // Completed timers are removed so cleanup only tracks pending callbacks.\n screenShakeTimeoutsRef.current.delete(timeoutId);\n combatActions.setScreenShake(shake);\n },\n index * SCREEN_SHAKE_FRAME_INTERVAL_MS,\n );\n screenShakeTimeoutsRef.current.add(timeoutId);\n });\n\n const distance = Math.sqrt(\n Math.pow(playerPositions[0].x - playerPositions[1].x, 2) +\n Math.pow(playerPositions[0].y - playerPositions[1].y, 2),\n );\n\n if (distance < 150) {\n // Play bone impact sound for special technique hit\n const hitPosition = calculateHitPosition(playerPositions[1]);\n\n combatAudio?.playBoneImpactSound({\n damage: 25,\n remainingHealth: validPlayers[1].health - 25,\n vitalPoint: false,\n hitPosition,\n });\n\n onPlayerUpdate(1, {\n health: Math.max(0, validPlayers[1].health - 25),\n hitsTaken: validPlayers[1].hitsTaken + 1,\n });\n addCombatMessage(\"특수 기술 성공!\", \"Special Technique Hit!\");\n } else {\n addCombatMessage(\"기술 실패\", \"Technique Failed\");\n }\n\n // Consume resources\n onPlayerUpdate(0, {\n ki: Math.max(0, validPlayers[0].ki - 10),\n stamina: Math.max(0, validPlayers[0].stamina - 15),\n });\n\n setTimeout(() => combatActions.setExecutingTechnique(false), 800);\n }, [\n validPlayers,\n playerPositions,\n combatState.isExecutingTechnique,\n combatState.roundStarted,\n combatState.roundEnded,\n combatActions,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n ]);\n\n // Player stance switch handler\n const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n // Play stance change sound\n combatAudio?.playStanceChangeSound();\n\n onPlayerUpdate(0, { currentStance: stance });\n addCombatMessage(`자세 변경: ${stance}`, `Stance Change: ${stance}`);\n addHitEffect(HitEffectType.STATUS_EFFECT, playerPositions[0], 0.6);\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ],\n );\n\n /**\n * Handle stance side switch (left/right)\n * @korean 자세측면전환처리\n */\n // Reuse StanceManager instance for stance side switches\n const stanceManagerRef = useRef<StanceManager>(new StanceManager());\n\n const handleStanceSideSwitch = useCallback(\n (playerIndex: 0 | 1) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\n const player = validPlayers[playerIndex];\n // Get current laterality from combat state\n const currentLaterality = combatState.playerLaterality[playerIndex];\n\n const result = stanceManagerRef.current.switchStanceSide(\n player,\n currentLaterality,\n );\n\n if (result.success && result.laterality) {\n // Update player state with new stamina\n onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n // Update laterality in combat state via callback\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n // Audio feedback\n combatAudio?.playStanceChangeSound?.();\n\n // Visual feedback\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n // Visual effect\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\n // Feedback for failed switch\n if (result.message?.includes(\"stamina\")) {\n addCombatMessage(\"체력 부족\", \"Insufficient Stamina\");\n } else if (result.message?.includes(\"cooldown\")) {\n addCombatMessage(\"대기 중\", \"On Cooldown\");\n }\n }\n },\n [\n combatState.roundStarted,\n combatState.roundEnded,\n combatState.playerLaterality,\n validPlayers,\n onPlayerUpdate,\n onLateralityUpdate,\n combatAudio,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n ],\n );\n\n /**\n * Helper function to create AI technique objects\n * Reduces code duplication between basic attacks and special techniques\n */\n const createAITechnique = useCallback(\n (type: \"basic\" | \"special\", aiPlayer: PlayerState) => {\n if (type === \"basic\") {\n return {\n id: \"ai_basic_attack\",\n name: {\n korean: \"AI 기본공격\",\n english: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n },\n koreanName: \"AI 기본공격\",\n englishName: \"AI Basic Attack\",\n romanized: \"ai_gibon_gonggyeok\",\n description: { korean: \"AI 기본 공격\", english: \"AI basic attack\" },\n stance: aiPlayer.currentStance,\n type: \"attack\" as const,\n damageType: \"physical\" as const,\n damage: 15,\n kiCost: 5,\n staminaCost: 8,\n accuracy: 0.8,\n reachConfig: {\n bodyPart: \"arm\" as const,\n techniqueType: \"punch\" as const,\n baseExtension: 0.95,\n },\n executionTime: 400,\n recoveryTime: 300,\n critChance: 0.1,\n critMultiplier: 1.5,\n effects: [],\n animationType: AnimationType.JAB, // Default animation for basic attack\n };\n } else {\n return {\n id: \"ai_special_technique\",\n name: {\n korean: \"AI 특수기술\",\n english: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n },\n koreanName: \"AI 특수기술\",\n englishName: \"AI Special Technique\",\n romanized: \"ai_teuksu_gisul\",\n description: {\n korean: \"AI 특수 기술\",\n english: \"AI special technique\",\n },\n stance: aiPlayer.currentStance,\n type: \"technique\" as const,\n damageType: \"physical\" as const,\n damage: 25,\n kiCost: 10,\n staminaCost: 15,\n accuracy: 0.85,\n reachConfig: {\n bodyPart: \"leg\" as const,\n techniqueType: \"kick\" as const,\n baseExtension: 1.1,\n },\n executionTime: 600,\n recoveryTime: 800,\n critChance: 0.15,\n critMultiplier: 1.8,\n effects: [],\n animationType: AnimationType.SPINNING_HOOK, // Default animation for special technique\n };\n }\n },\n [],\n );\n\n /**\n * Helper function to determine hit effect type based on combat result\n * Reduces duplication between attack and technique handlers\n */\n const getHitEffectType = useCallback(\n (result: { hit: boolean; isCritical?: boolean }): HitEffectType => {\n if (!result.hit) return HitEffectType.MISS;\n return result.isCritical ? HitEffectType.CRITICAL_HIT : HitEffectType.HIT;\n },\n [],\n );\n\n /**\n * AI attack handler with technique and vital point targeting\n *\n * @param technique - Optional Korean martial arts technique to execute. If not provided, creates a basic attack.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAIAttack = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create basic attack technique\n const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\n // Play attack sound based on technique damage/intensity (consistent with player)\n const damage = attackTechnique.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n combatAudio?.playAttackSound(intensity);\n\n // Calculate animation timing context for AI hit detection (same as player)\n // 동기식 타격 판정: AI도 피크 타임 사용\n const animationType = attackTechnique.animationType ?? AnimationType.JAB;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.15;\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n attackTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = getHitEffectType(result);\n addHitEffect(effectType, playerPositions[1], result.hit ? 1 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI hits on player\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical,\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n attackTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI attacks (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI ${knockbackName.korean}`,\n `AI ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 급소 타격! ${vpName}`,\n `AI Vital Point Hit! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else if (result.isCritical) {\n addCombatMessage(\"AI 치명타!\", \"AI Critical Hit!\");\n } else {\n addCombatMessage(\"AI 공격 성공!\", \"AI Attack Hit!\");\n }\n } else {\n // Consume resources on miss for consistency with technique behavior\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - attackTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - attackTechnique.staminaCost),\n });\n addCombatMessage(\"AI 공격 빗나감\", \"AI Attack Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n combatAudio,\n createAITechnique,\n getHitEffectType,\n config,\n ],\n );\n\n // AI defend handler\n const handleAIDefend = useCallback(() => {\n // Play block sound\n combatAudio?.playBlockSound(false);\n\n onPlayerUpdate(1, { isBlocking: true });\n addCombatMessage(\"AI 방어 자세\", \"AI Defensive Stance\");\n addHitEffect(HitEffectType.BLOCK, playerPositions[1], 0.8);\n\n setTimeout(() => {\n onPlayerUpdate(1, { isBlocking: false });\n }, 1000);\n }, [\n onPlayerUpdate,\n addCombatMessage,\n addHitEffect,\n playerPositions,\n combatAudio,\n ]);\n\n /**\n * AI technique handler with technique and vital point targeting\n *\n * @param technique - Optional special Korean martial arts technique to execute. If not provided, creates a special technique.\n * @param targetVitalPoint - Optional vital point ID to target for increased damage effectiveness.\n */\n const handleAITechnique = useCallback(\n (technique?: KoreanTechnique, targetVitalPoint?: string) => {\n const aiPlayer = validPlayers[1];\n const targetPlayer = validPlayers[0];\n\n // Use provided technique or create special technique\n const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\n // Check if AI has sufficient resources for the technique\n if (\n aiPlayer.ki < specialTechnique.kiCost ||\n aiPlayer.stamina < specialTechnique.staminaCost\n ) {\n handleAIAttack(undefined, targetVitalPoint); // Fallback to basic attack with same targeting\n return;\n }\n\n // Play special technique sound\n combatAudio?.playSpecialTechniqueSound();\n\n // Calculate animation timing context for AI technique hit detection\n // 동기식 타격 판정: AI 특수 기술도 피크 타임 사용\n const animationType =\n specialTechnique.animationType ?? AnimationType.SPINNING_HOOK;\n const hitTiming = getAnimationHitTiming(animationType);\n const peakTime = hitTiming?.hitWindow.peakTime ?? 0.25; // Special techniques often have longer peak times\n const animationContext = {\n animationType,\n currentTime: peakTime,\n };\n\n // Use combat system for proper calculation with vital point targeting and animation context\n const result = combatSystem.resolveAttack(\n aiPlayer,\n targetPlayer,\n specialTechnique,\n targetVitalPoint, // Pass vital point ID for targeting\n animationContext, // Pass animation context for distance/reach check\n );\n\n const effectType = result.hit\n ? HitEffectType.CRITICAL_HIT\n : HitEffectType.MISS;\n\n addHitEffect(effectType, playerPositions[1], result.hit ? 1.5 : 0.5);\n\n if (result.hit) {\n // Play bone impact sound for AI technique hits\n const hitPosition = calculateHitPosition(playerPositions[0]);\n\n combatAudio?.playBoneImpactSound({\n damage: result.damage,\n remainingHealth: validPlayers[0].health - result.damage,\n vitalPoint: result.isCritical === true || !!targetVitalPoint, // Special techniques often target vital points\n hitPosition,\n });\n\n // Apply damage through combat system (deducts resources)\n const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\n // Create injury for trauma visualization (AI technique hit player)\n if (onInjuryCreated) {\n const injury = createInjuryFromDamage(\n result,\n specialTechnique,\n updatedDefender.health,\n 0, // Player 1 (player) was hit by AI technique\n );\n onInjuryCreated(injury, 0);\n }\n\n // Apply knockback displacement for AI special techniques (밀침 적용)\n if (result.knockback && config.onPlayerPositionUpdate) {\n const newDefenderPosition = applyKnockbackDisplacement(\n result,\n playerPositions[0],\n config.arenaBounds,\n );\n config.onPlayerPositionUpdate(0, newDefenderPosition);\n\n // Add combat message for significant knockback\n const knockbackDistance = Math.sqrt(\n result.knockback.displacement.x ** 2 +\n result.knockback.displacement.z ** 2,\n );\n if (knockbackDistance > 1.5) {\n const knockbackName = KnockbackPhysics.getKnockbackStateName(\n result.knockback.shouldFall,\n );\n addCombatMessage(\n `AI 특수 ${knockbackName.korean}`,\n `AI Special ${knockbackName.english}`,\n );\n }\n\n // Set stunned state for knockback duration (non-interruptible)\n if (result.knockback.duration > 0) {\n // Clear any existing timeout for player 1\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\n // Schedule recovery after knockback duration + recovery window\n player1KnockbackTimeoutRef.current = setTimeout(\n () => {\n onPlayerUpdate(0, { isStunned: false });\n player1KnockbackTimeoutRef.current = null;\n },\n (result.knockback.duration + result.knockback.recoveryWindow) *\n 1000,\n );\n }\n }\n\n // Check if player should fall after taking damage from AI technique\n if (config.playerAnimations?.player1) {\n const fallCheck = checkForFall(\n updatedDefender,\n combatSystem,\n undefined, // lastImpactAngle not tracked yet\n undefined, // attackAngle not tracked yet\n );\n\n if (\n fallCheck.shouldFall &&\n fallCheck.animationState &&\n fallCheck.fallType\n ) {\n config.playerAnimations.player1.transitionTo(\n fallCheck.animationState,\n );\n const fallName = getFallTypeName(fallCheck.fallType);\n addCombatMessage(`${fallName.korean}!`, `${fallName.english}!`);\n }\n }\n\n // Enhanced combat message with vital point info\n if (result.vitalPointHit && targetVitalPoint) {\n const vitalPoint = getVitalPointById(targetVitalPoint);\n const vpName = vitalPoint\n ? vitalPoint.names.korean\n : targetVitalPoint;\n addCombatMessage(\n `AI 특수 급소 기술! ${vpName}`,\n `AI Special Vital Point Technique! ${\n vitalPoint?.names.english ?? targetVitalPoint\n }`,\n );\n } else {\n addCombatMessage(\"AI 특수 기술!\", \"AI Special Technique!\");\n }\n } else {\n // Consume resources on miss (technique was attempted)\n onPlayerUpdate(1, {\n ki: Math.max(0, aiPlayer.ki - specialTechnique.kiCost),\n stamina: Math.max(0, aiPlayer.stamina - specialTechnique.staminaCost),\n });\n addCombatMessage(\"AI 기술 빗나감\", \"AI Technique Missed\");\n }\n },\n [\n validPlayers,\n playerPositions,\n combatSystem,\n onPlayerUpdate,\n onInjuryCreated,\n addCombatMessage,\n addHitEffect,\n handleAIAttack,\n combatAudio,\n createAITechnique,\n config,\n ],\n );\n\n // AI movement handler with injury-based movement penalties\n // **UPDATED**: Now scale-aware for consistent movement and distance calculations\n // **FIX**: Positions are in METERS, not pixels - use meters-based speed\n const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n // Movement speed calibrated for physics-first system (all in METERS)\n // Combat closing speed: ~2.5 m/s (fast tactical approach, not slow walking)\n // Real fights are over in 4-5 seconds - AI must close distance quickly\n // AI decision loop frequency (defined in useAICombat.ts)\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n // Calculation: 2.5 m/s / 20 calls/s = 0.125 meters per call\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\n // Calculate movement direction vector (in meters)\n const dx = targetPos.x - currentPos.x;\n const dy = targetPos.y - currentPos.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // Apply movement penalties from leg injuries if body part health exists\n let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n // Normalize movement direction\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n // Calculate modified speed with all penalties applied\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\n // Physics-first: positions are in METERS, so distance is in meters\n // Stop moving when within 0.05 meters (5cm) of target - close enough for melee range\n const MIN_MOVEMENT_THRESHOLD_METERS = 0.05;\n\n if (distance > MIN_MOVEMENT_THRESHOLD_METERS) {\n const newPos = {\n x: currentPos.x + (dx / distance) * finalSpeed,\n y: currentPos.y + (dy / distance) * finalSpeed,\n };\n\n // Keep AI within arena bounds (positions in meters, centered at origin)\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n newPos.x = Math.max(-halfWidth, Math.min(halfWidth, newPos.x));\n newPos.y = Math.max(-halfDepth, Math.min(halfDepth, newPos.y));\n\n // Update position through parent - this should trigger playerPositions state update in parent\n onPlayerUpdate(1, { position: newPos });\n }\n },\n [playerPositions, validPlayers, arenaBounds, onPlayerUpdate],\n );\n\n return {\n handleAttack,\n handleDefend,\n handleTechniqueExecute,\n handleStanceSwitch,\n handleStanceSideSwitch,\n handleAIAttack,\n handleAIDefend,\n handleAITechnique,\n moveAIPlayer,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCombatAttackMovement.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatAttackMovement.ts"],"sourcesContent":["/**\n * useCombatAttackMovement Hook - Track Attack Movement for Combat Characters\n *\n * Custom hook for managing attack movement physics during combat animations.\n * Supports two fighters with simultaneous attack movements and arena bounds.\n *\n * @korean 전투공격이동훅 - 전투 공격 이동 추적\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { AnimationType } from \"@/systems/animation\";\nimport { AttackMovementPhysics } from \"@/systems/physics\";\nimport { TrigramStance } from \"@/types/common\";\n\n/**\n * Calculate attack direction from attacker to defender\n * 공격자에서 방어자로의 공격 방향 계산\n *\n * @param attackerPos - Attacker position\n * @param defenderPos - Defender position\n * @returns Normalized direction vector\n */\nfunction calculateAttackDirection(\n attackerPos: [number, number, number],\n defenderPos: [number, number, number],\n): THREE.Vector3 {\n const direction = new THREE.Vector3(\n defenderPos[0] - attackerPos[0],\n 0, // Keep movement horizontal\n defenderPos[2] - attackerPos[2],\n );\n return direction.normalize();\n}\n\n/**\n * Configuration for combat attack movement\n */\nexport interface CombatAttackMovementConfig {\n /** Whether player 1 is currently attacking */\n readonly player1Attacking: boolean;\n /** Player 1 animation type */\n readonly player1AnimationType?: AnimationType;\n /** Player 1 current stance */\n readonly player1Stance: TrigramStance;\n /** Player 1 base position */\n readonly player1BasePosition: [number, number, number];\n /** Player 1 animation duration in seconds (default: 0.4) */\n readonly player1AnimationDuration?: number;\n\n /** Whether player 2 is currently attacking */\n readonly player2Attacking: boolean;\n /** Player 2 animation type */\n readonly player2AnimationType?: AnimationType;\n /** Player 2 current stance */\n readonly player2Stance: TrigramStance;\n /** Player 2 base position */\n readonly player2BasePosition: [number, number, number];\n /** Player 2 animation duration in seconds (default: 0.4) */\n readonly player2AnimationDuration?: number;\n\n /** Default animation duration in seconds for both players if individual values not specified (default: 0.4) */\n readonly animationDuration?: number;\n}\n\n/**\n * Return value from useCombatAttackMovement hook\n */\nexport interface CombatAttackMovementResult {\n /** Player 1 current position including attack movement */\n readonly player1Position: [number, number, number];\n /** Player 2 current position including attack movement */\n readonly player2Position: [number, number, number];\n /** Whether player 1 is in forward lunge phase */\n readonly player1IsLunging: boolean;\n /** Whether player 2 is in forward lunge phase */\n readonly player2IsLunging: boolean;\n}\n\n/**\n * useCombatAttackMovement hook\n *\n * Tracks attack movement physics for both fighters in combat.\n * Automatically handles lunge and recovery phases with smooth easing.\n * Calculates attack direction between fighters dynamically.\n *\n * @param config - Combat attack movement configuration\n * @returns Current positions and movement states for both fighters\n *\n * @example\n * ```typescript\n * const {\n * player1Position,\n * player2Position,\n * player1IsLunging\n * } = useCombatAttackMovement({\n * player1Attacking: isPlayer1Attacking,\n * player1AnimationType: AnimationType.ROUNDHOUSE_KICK,\n * player1Stance: player1.stance,\n * player1BasePosition: [5, 0, 0],\n * player2Attacking: false,\n * player2AnimationType: undefined,\n * player2Stance: player2.stance,\n * player2BasePosition: [-5, 0, 0],\n * });\n *\n * <Player3D position={player1Position} />\n * <Player3D position={player2Position} />\n * ```\n *\n * @korean 전투공격이동사용\n */\nexport function useCombatAttackMovement(\n config: CombatAttackMovementConfig,\n): CombatAttackMovementResult {\n const {\n player1Attacking,\n player1AnimationType,\n player1Stance,\n player1BasePosition,\n player1AnimationDuration,\n player2Attacking,\n player2AnimationType,\n player2Stance,\n player2BasePosition,\n player2AnimationDuration,\n animationDuration = 0.4,\n } = config;\n\n // Per-player animation durations (fallback to shared default)\n const p1Duration = player1AnimationDuration ?? animationDuration;\n const p2Duration = player2AnimationDuration ?? animationDuration;\n\n // Attack movement physics engines (separate instances for each player to avoid race conditions)\n const player1PhysicsRef = useRef(new AttackMovementPhysics());\n const player2PhysicsRef = useRef(new AttackMovementPhysics());\n\n // Reusable Vector3 objects to avoid GC pressure (60fps allocation)\n const player1BasePosVectorRef = useRef(new THREE.Vector3());\n const player2BasePosVectorRef = useRef(new THREE.Vector3());\n\n // Player 1 attack timing\n const player1AttackStartTimeRef = useRef<number | null>(null);\n const player1MovementResultRef = useRef<ReturnType<\n typeof player1PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n // Player 2 attack timing\n const player2AttackStartTimeRef = useRef<number | null>(null);\n const player2MovementResultRef = useRef<ReturnType<\n typeof player2PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n // Current positions\n const [player1Position, setPlayer1Position] =\n useState<[number, number, number]>(player1BasePosition);\n const [player2Position, setPlayer2Position] =\n useState<[number, number, number]>(player2BasePosition);\n\n // Movement states\n const [player1IsLunging, setPlayer1IsLunging] = useState(false);\n const [player2IsLunging, setPlayer2IsLunging] = useState(false);\n\n // Track previous attacking state for rising-edge detection\n const player1WasAttackingRef = useRef(false);\n const player2WasAttackingRef = useRef(false);\n\n // Player 1 attack movement effect\n useEffect(() => {\n const wasAttacking = player1WasAttackingRef.current;\n player1WasAttackingRef.current = player1Attacking;\n\n // Only initialize on transition from not-attacking → attacking (rising edge)\n if (player1Attacking && player1AnimationType && !wasAttacking) {\n // Start attack - calculate movement result\n player1AttackStartTimeRef.current = Date.now();\n\n const direction = calculateAttackDirection(\n player1BasePosition,\n player2BasePosition,\n );\n\n player1MovementResultRef.current =\n player1PhysicsRef.current.calculateAttackMovement({\n animationType: player1AnimationType,\n currentStance: player1Stance,\n direction,\n animationDuration: p1Duration,\n });\n\n setPlayer1IsLunging(true);\n\n // Setup animation frame loop\n let animationFrameId: number;\n\n const updatePosition = () => {\n const elapsed = Date.now() - (player1AttackStartTimeRef.current ?? 0);\n const movementResult = player1MovementResultRef.current;\n\n if (!movementResult) {\n setPlayer1Position(player1BasePosition);\n return;\n }\n\n const totalDuration = movementResult.totalDuration * 1000; // Convert to ms\n\n if (elapsed >= totalDuration) {\n // Attack complete - return to base position\n setPlayer1Position(player1BasePosition);\n setPlayer1IsLunging(false);\n player1AttackStartTimeRef.current = null;\n player1MovementResultRef.current = null;\n return;\n }\n\n // Calculate current position with attack movement\n const elapsedSeconds = elapsed / 1000;\n const basePos = player1BasePosVectorRef.current.set(\n ...player1BasePosition,\n );\n const recovering = elapsed >= movementResult.lungeDuration * 1000;\n\n const newPos = player1PhysicsRef.current.applyAttackMovement(\n basePos,\n movementResult,\n elapsedSeconds,\n recovering,\n );\n\n const newPosition: [number, number, number] = [\n newPos.x,\n newPos.y,\n newPos.z,\n ];\n\n setPlayer1Position(newPosition);\n\n // Update lunging state (first half is lunge, second half is recovery)\n const totalProgress = elapsedSeconds / movementResult.totalDuration;\n setPlayer1IsLunging(totalProgress < 0.5);\n\n animationFrameId = requestAnimationFrame(updatePosition);\n };\n\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n } else {\n // Not attacking - reset to base position\n setPlayer1Position(player1BasePosition);\n setPlayer1IsLunging(false);\n player1AttackStartTimeRef.current = null;\n player1MovementResultRef.current = null;\n }\n }, [\n player1Attacking,\n player1AnimationType,\n player1Stance,\n player1BasePosition,\n player2BasePosition,\n p1Duration,\n ]);\n\n // Player 2 attack movement effect (same logic as player 1)\n useEffect(() => {\n const wasAttacking = player2WasAttackingRef.current;\n player2WasAttackingRef.current = player2Attacking;\n\n // Only initialize on transition from not-attacking → attacking (rising edge)\n if (player2Attacking && player2AnimationType && !wasAttacking) {\n player2AttackStartTimeRef.current = Date.now();\n\n const direction = calculateAttackDirection(\n player2BasePosition,\n player1BasePosition,\n );\n\n player2MovementResultRef.current =\n player2PhysicsRef.current.calculateAttackMovement({\n animationType: player2AnimationType,\n currentStance: player2Stance,\n direction,\n animationDuration: p2Duration,\n });\n\n setPlayer2IsLunging(true);\n\n let animationFrameId: number;\n\n const updatePosition = () => {\n const elapsed = Date.now() - (player2AttackStartTimeRef.current ?? 0);\n const movementResult = player2MovementResultRef.current;\n\n if (!movementResult) {\n setPlayer2Position(player2BasePosition);\n return;\n }\n\n const totalDuration = movementResult.totalDuration * 1000;\n\n if (elapsed >= totalDuration) {\n setPlayer2Position(player2BasePosition);\n setPlayer2IsLunging(false);\n player2AttackStartTimeRef.current = null;\n player2MovementResultRef.current = null;\n return;\n }\n\n const elapsedSeconds = elapsed / 1000;\n const basePos = player2BasePosVectorRef.current.set(\n ...player2BasePosition,\n );\n const recovering = elapsed >= movementResult.lungeDuration * 1000;\n\n const newPos = player2PhysicsRef.current.applyAttackMovement(\n basePos,\n movementResult,\n elapsedSeconds,\n recovering,\n );\n\n const newPosition: [number, number, number] = [\n newPos.x,\n newPos.y,\n newPos.z,\n ];\n\n setPlayer2Position(newPosition);\n\n const totalProgress = elapsedSeconds / movementResult.totalDuration;\n setPlayer2IsLunging(totalProgress < 0.5);\n\n animationFrameId = requestAnimationFrame(updatePosition);\n };\n\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n } else {\n setPlayer2Position(player2BasePosition);\n setPlayer2IsLunging(false);\n player2AttackStartTimeRef.current = null;\n player2MovementResultRef.current = null;\n }\n }, [\n player2Attacking,\n player2AnimationType,\n player2Stance,\n player1BasePosition,\n player2BasePosition,\n p2Duration,\n ]);\n\n return {\n player1Position,\n player2Position,\n player1IsLunging,\n player2IsLunging,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useCombatAttackMovement.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatAttackMovement.ts"],"sourcesContent":["/**\n * useCombatAttackMovement Hook - Track Attack Movement for Combat Characters\n *\n * Custom hook for managing attack movement physics during combat animations.\n * Supports two fighters with simultaneous attack movements and arena bounds.\n *\n * @korean 전투공격이동훅 - 전투 공격 이동 추적\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { AnimationType } from \"@/systems/animation\";\nimport { AttackMovementPhysics } from \"@/systems/physics\";\nimport { TrigramStance } from \"@/types/common\";\n\n/**\n * Calculate attack direction from attacker to defender\n * 공격자에서 방어자로의 공격 방향 계산\n *\n * @param attackerPos - Attacker position\n * @param defenderPos - Defender position\n * @returns Normalized direction vector\n */\nfunction calculateAttackDirection(\n attackerPos: [number, number, number],\n defenderPos: [number, number, number],\n): THREE.Vector3 {\n const direction = new THREE.Vector3(\n defenderPos[0] - attackerPos[0],\n 0, // Keep movement horizontal\n defenderPos[2] - attackerPos[2],\n );\n return direction.normalize();\n}\n\n/**\n * Configuration for combat attack movement\n */\nexport interface CombatAttackMovementConfig {\n /** Whether player 1 is currently attacking */\n readonly player1Attacking: boolean;\n /** Player 1 animation type */\n readonly player1AnimationType?: AnimationType;\n /** Player 1 current stance */\n readonly player1Stance: TrigramStance;\n /** Player 1 base position */\n readonly player1BasePosition: [number, number, number];\n /** Player 1 animation duration in seconds (default: 0.4) */\n readonly player1AnimationDuration?: number;\n\n /** Whether player 2 is currently attacking */\n readonly player2Attacking: boolean;\n /** Player 2 animation type */\n readonly player2AnimationType?: AnimationType;\n /** Player 2 current stance */\n readonly player2Stance: TrigramStance;\n /** Player 2 base position */\n readonly player2BasePosition: [number, number, number];\n /** Player 2 animation duration in seconds (default: 0.4) */\n readonly player2AnimationDuration?: number;\n\n /** Default animation duration in seconds for both players if individual values not specified (default: 0.4) */\n readonly animationDuration?: number;\n}\n\n/**\n * Return value from useCombatAttackMovement hook\n */\nexport interface CombatAttackMovementResult {\n /** Player 1 current position including attack movement */\n readonly player1Position: [number, number, number];\n /** Player 2 current position including attack movement */\n readonly player2Position: [number, number, number];\n /** Whether player 1 is in forward lunge phase */\n readonly player1IsLunging: boolean;\n /** Whether player 2 is in forward lunge phase */\n readonly player2IsLunging: boolean;\n}\n\n/**\n * useCombatAttackMovement hook\n *\n * Tracks attack movement physics for both fighters in combat.\n * Automatically handles lunge and recovery phases with smooth easing.\n * Calculates attack direction between fighters dynamically.\n *\n * @param config - Combat attack movement configuration\n * @returns Current positions and movement states for both fighters\n *\n * @example\n * ```typescript\n * const {\n * player1Position,\n * player2Position,\n * player1IsLunging\n * } = useCombatAttackMovement({\n * player1Attacking: isPlayer1Attacking,\n * player1AnimationType: AnimationType.ROUNDHOUSE_KICK,\n * player1Stance: player1.stance,\n * player1BasePosition: [5, 0, 0],\n * player2Attacking: false,\n * player2AnimationType: undefined,\n * player2Stance: player2.stance,\n * player2BasePosition: [-5, 0, 0],\n * });\n *\n * <Player3D position={player1Position} />\n * <Player3D position={player2Position} />\n * ```\n *\n * @korean 전투공격이동사용\n */\nexport function useCombatAttackMovement(\n config: CombatAttackMovementConfig,\n): CombatAttackMovementResult {\n const {\n player1Attacking,\n player1AnimationType,\n player1Stance,\n player1BasePosition,\n player1AnimationDuration,\n player2Attacking,\n player2AnimationType,\n player2Stance,\n player2BasePosition,\n player2AnimationDuration,\n animationDuration = 0.4,\n } = config;\n\n // Per-player animation durations (fallback to shared default)\n const p1Duration = player1AnimationDuration ?? animationDuration;\n const p2Duration = player2AnimationDuration ?? animationDuration;\n\n // Attack movement physics engines (separate instances for each player to avoid race conditions)\n const player1PhysicsRef = useRef(new AttackMovementPhysics());\n const player2PhysicsRef = useRef(new AttackMovementPhysics());\n\n // Reusable Vector3 objects to avoid GC pressure (60fps allocation)\n const player1BasePosVectorRef = useRef(new THREE.Vector3());\n const player2BasePosVectorRef = useRef(new THREE.Vector3());\n\n // Player 1 attack timing\n const player1AttackStartTimeRef = useRef<number | null>(null);\n const player1MovementResultRef = useRef<ReturnType<\n typeof player1PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n // Player 2 attack timing\n const player2AttackStartTimeRef = useRef<number | null>(null);\n const player2MovementResultRef = useRef<ReturnType<\n typeof player2PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n // Current positions\n const [player1Position, setPlayer1Position] =\n useState<[number, number, number]>(player1BasePosition);\n const [player2Position, setPlayer2Position] =\n useState<[number, number, number]>(player2BasePosition);\n\n // Movement states\n const [player1IsLunging, setPlayer1IsLunging] = useState(false);\n const [player2IsLunging, setPlayer2IsLunging] = useState(false);\n\n // Track previous attacking state for rising-edge detection\n const player1WasAttackingRef = useRef(false);\n const player2WasAttackingRef = useRef(false);\n\n // Player 1 attack movement effect\n useEffect(() => {\n const wasAttacking = player1WasAttackingRef.current;\n player1WasAttackingRef.current = player1Attacking;\n\n // Only initialize on transition from not-attacking → attacking (rising edge)\n if (player1Attacking && player1AnimationType && !wasAttacking) {\n // Start attack - calculate movement result\n player1AttackStartTimeRef.current = Date.now();\n\n const direction = calculateAttackDirection(\n player1BasePosition,\n player2BasePosition,\n );\n\n player1MovementResultRef.current =\n player1PhysicsRef.current.calculateAttackMovement({\n animationType: player1AnimationType,\n currentStance: player1Stance,\n direction,\n animationDuration: p1Duration,\n });\n\n setPlayer1IsLunging(true);\n\n // Setup animation frame loop\n let animationFrameId: number;\n\n const updatePosition = () => {\n const elapsed = Date.now() - (player1AttackStartTimeRef.current ?? 0);\n const movementResult = player1MovementResultRef.current;\n\n if (!movementResult) {\n setPlayer1Position(player1BasePosition);\n return;\n }\n\n const totalDuration = movementResult.totalDuration * 1000; // Convert to ms\n\n if (elapsed >= totalDuration) {\n // Attack complete - return to base position\n setPlayer1Position(player1BasePosition);\n setPlayer1IsLunging(false);\n player1AttackStartTimeRef.current = null;\n player1MovementResultRef.current = null;\n return;\n }\n\n // Calculate current position with attack movement\n const elapsedSeconds = elapsed / 1000;\n const basePos = player1BasePosVectorRef.current.set(\n ...player1BasePosition,\n );\n const recovering = elapsed >= movementResult.lungeDuration * 1000;\n\n const newPos = player1PhysicsRef.current.applyAttackMovement(\n basePos,\n movementResult,\n elapsedSeconds,\n recovering,\n );\n\n const newPosition: [number, number, number] = [\n newPos.x,\n newPos.y,\n newPos.z,\n ];\n\n setPlayer1Position(newPosition);\n\n // Update lunging state (first half is lunge, second half is recovery)\n const totalProgress = elapsedSeconds / movementResult.totalDuration;\n setPlayer1IsLunging(totalProgress < 0.5);\n\n animationFrameId = requestAnimationFrame(updatePosition);\n };\n\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n } else {\n // Not attacking - reset to base position\n setPlayer1Position(player1BasePosition);\n setPlayer1IsLunging(false);\n player1AttackStartTimeRef.current = null;\n player1MovementResultRef.current = null;\n }\n }, [\n player1Attacking,\n player1AnimationType,\n player1Stance,\n player1BasePosition,\n player2BasePosition,\n p1Duration,\n ]);\n\n // Player 2 attack movement effect (same logic as player 1)\n useEffect(() => {\n const wasAttacking = player2WasAttackingRef.current;\n player2WasAttackingRef.current = player2Attacking;\n\n // Only initialize on transition from not-attacking → attacking (rising edge)\n if (player2Attacking && player2AnimationType && !wasAttacking) {\n player2AttackStartTimeRef.current = Date.now();\n\n const direction = calculateAttackDirection(\n player2BasePosition,\n player1BasePosition,\n );\n\n player2MovementResultRef.current =\n player2PhysicsRef.current.calculateAttackMovement({\n animationType: player2AnimationType,\n currentStance: player2Stance,\n direction,\n animationDuration: p2Duration,\n });\n\n setPlayer2IsLunging(true);\n\n let animationFrameId: number;\n\n const updatePosition = () => {\n const elapsed = Date.now() - (player2AttackStartTimeRef.current ?? 0);\n const movementResult = player2MovementResultRef.current;\n\n if (!movementResult) {\n setPlayer2Position(player2BasePosition);\n return;\n }\n\n const totalDuration = movementResult.totalDuration * 1000;\n\n if (elapsed >= totalDuration) {\n setPlayer2Position(player2BasePosition);\n setPlayer2IsLunging(false);\n player2AttackStartTimeRef.current = null;\n player2MovementResultRef.current = null;\n return;\n }\n\n const elapsedSeconds = elapsed / 1000;\n const basePos = player2BasePosVectorRef.current.set(\n ...player2BasePosition,\n );\n const recovering = elapsed >= movementResult.lungeDuration * 1000;\n\n const newPos = player2PhysicsRef.current.applyAttackMovement(\n basePos,\n movementResult,\n elapsedSeconds,\n recovering,\n );\n\n const newPosition: [number, number, number] = [\n newPos.x,\n newPos.y,\n newPos.z,\n ];\n\n setPlayer2Position(newPosition);\n\n const totalProgress = elapsedSeconds / movementResult.totalDuration;\n setPlayer2IsLunging(totalProgress < 0.5);\n\n animationFrameId = requestAnimationFrame(updatePosition);\n };\n\n animationFrameId = requestAnimationFrame(updatePosition);\n\n return () => {\n if (animationFrameId) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n } else {\n setPlayer2Position(player2BasePosition);\n setPlayer2IsLunging(false);\n player2AttackStartTimeRef.current = null;\n player2MovementResultRef.current = null;\n }\n }, [\n player2Attacking,\n player2AnimationType,\n player2Stance,\n player1BasePosition,\n player2BasePosition,\n p2Duration,\n ]);\n\n return {\n player1Position,\n player2Position,\n player1IsLunging,\n player2IsLunging,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCombatAudio.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatAudio.ts"],"sourcesContent":["/**\n * Combat Audio Hook for Black Trigram\n * Provides comprehensive audio feedback for combat actions including\n * bone impact sounds, fracture audio, and body-region-specific hit sounds\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport {\n calculateImpactIntensity,\n detectAudioBodyRegion,\n getBoneImpactSoundId,\n getImpactVolumeMultiplier,\n} from \"../../../../audio/BoneImpactAudioMap\";\nimport { AudioBodyRegion, ImpactIntensity } from \"../../../../audio/types\";\n\n/**\n * Attack intensity levels for sound selection\n */\nexport type AttackIntensity = \"light\" | \"medium\" | \"heavy\" | \"critical\";\n\n/**\n * Maximum number of simultaneous sounds to prevent audio chaos\n */\nconst MAX_SIMULTANEOUS_SOUNDS = 5;\n\n/**\n * Combat audio hook for playing attack, hit, block, dodge, and stance sounds\n * @returns Object with methods for playing various combat sounds\n */\nexport const useCombatAudio = () => {\n const audio = useAudio();\n const lastPlayTime = useRef<Record<string, number>>({});\n const activeSounds = useRef(new Set<string>());\n const timeoutIds = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());\n\n // Cleanup all timeouts on unmount\n useEffect(() => {\n // Copy ref value to local variable for cleanup\n const timeoutIdsSet = timeoutIds.current;\n return () => {\n timeoutIdsSet.forEach(clearTimeout);\n timeoutIdsSet.clear();\n };\n }, []);\n\n /**\n * Check if we can play a sound (rate limiting and simultaneous sound check)\n * @param soundType - Type of sound to check\n * @param minInterval - Minimum interval between plays in milliseconds\n * @returns True if sound can be played\n */\n const canPlaySound = useCallback(\n (soundType: string, minInterval = 50): boolean => {\n const now = Date.now();\n const lastTime = lastPlayTime.current[soundType] ?? 0;\n\n // Rate limiting check\n if (now - lastTime < minInterval) {\n return false;\n }\n\n // Check simultaneous sounds limit\n if (activeSounds.current.size >= MAX_SIMULTANEOUS_SOUNDS) {\n return false;\n }\n\n return true;\n },\n [],\n );\n\n /**\n * Register a sound as active and auto-remove after duration\n * @param soundId - ID of the sound to register\n * @param duration - Duration in milliseconds (default 500ms)\n */\n const registerActiveSound = useCallback((soundId: string, duration = 500) => {\n activeSounds.current.add(soundId);\n const timeoutId = setTimeout(() => {\n activeSounds.current.delete(soundId);\n timeoutIds.current.delete(timeoutId);\n }, duration);\n timeoutIds.current.add(timeoutId);\n }, []);\n\n /**\n * Get a random variant from a pool\n * @param base - Base sound ID\n * @param count - Number of variants\n * @returns Random variant ID\n */\n const getRandomVariant = useCallback(\n (base: string, count: number): string => {\n const variant = Math.floor(Math.random() * count) + 1;\n return `${base}_${variant}`;\n },\n [],\n );\n\n /**\n * Play attack sound based on intensity\n * @param intensity - Attack intensity (light, medium, heavy, critical)\n */\n const playAttackSound = useCallback(\n async (intensity: AttackIntensity = \"light\") => {\n const soundType = `attack_${intensity}`;\n\n if (!canPlaySound(soundType)) {\n return;\n }\n\n let soundId: string;\n\n switch (intensity) {\n case \"light\":\n // 8 variations of light punch sounds\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n break;\n case \"medium\":\n // 4 variations of medium punch sounds\n soundId = getRandomVariant(\"attack_punch_medium\", 4);\n break;\n case \"heavy\":\n soundId = \"attack_heavy\";\n break;\n case \"critical\":\n // 4 variations of critical attack sounds\n soundId = getRandomVariant(\"attack_critical\", 4);\n break;\n default:\n console.warn(\n `Unknown attack intensity: ${intensity}, defaulting to light`,\n );\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play attack sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play hit reaction sound based on damage amount\n * @param damage - Damage amount to determine hit intensity\n */\n const playHitSound = useCallback(\n async (damage: number) => {\n const soundType = \"hit\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n let soundId: string;\n\n // Determine hit intensity based on damage\n if (damage >= 40) {\n // Critical hit (4 variations)\n soundId = getRandomVariant(\"hit_critical\", 4);\n } else if (damage >= 25) {\n // Heavy hit (4 variations)\n soundId = getRandomVariant(\"hit_heavy\", 4);\n } else if (damage >= 10) {\n // Medium hit (4 variations)\n soundId = getRandomVariant(\"hit_medium\", 4);\n } else {\n // Light hit (4 variations)\n soundId = getRandomVariant(\"hit_light\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 500);\n } catch (error) {\n console.warn(`Failed to play hit sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play block/parry sound\n * @param guardBroken - Whether the guard was broken or successfully blocked\n */\n const playBlockSound = useCallback(\n async (guardBroken: boolean = false) => {\n const soundType = \"block\";\n\n if (!canPlaySound(soundType, 150)) {\n return;\n }\n\n let soundId: string;\n\n if (guardBroken) {\n // 4 variations of guard break sounds\n soundId = getRandomVariant(\"block_break\", 4);\n } else {\n // 4 variations of successful block sounds\n soundId = getRandomVariant(\"block_success\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play block sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play dodge sound\n */\n const playDodgeSound = useCallback(async () => {\n const soundType = \"dodge\";\n\n if (!canPlaySound(soundType, 200)) {\n return;\n }\n\n // 8 variations of dodge sounds\n const soundId = getRandomVariant(\"dodge\", 8);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 300);\n } catch (error) {\n console.warn(`Failed to play dodge sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play stance change sound\n */\n const playStanceChangeSound = useCallback(async () => {\n const soundType = \"stance\";\n\n if (!canPlaySound(soundType, 250)) {\n return;\n }\n\n // 4 variations of stance change sounds\n const soundId = getRandomVariant(\"stance_change\", 4);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play stance change sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play special technique sound (e.g., Geon special)\n */\n const playSpecialTechniqueSound = useCallback(async () => {\n const soundType = \"special\";\n\n if (!canPlaySound(soundType, 300)) {\n return;\n }\n\n // 4 variations of special Geon technique sounds\n const soundId = getRandomVariant(\"attack_special_geon\", 4);\n\n try {\n await audio.playSFX(soundId, 0.8);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 600);\n } catch (error) {\n console.warn(`Failed to play special technique sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play combat theme music with fade-in\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playCombatMusic = useCallback(\n async (fadeInDuration: number = 2000) => {\n try {\n await audio.fadeIn(\"combat_theme\", fadeInDuration);\n } catch (error) {\n console.warn(\"Failed to play combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Play archetype-specific music theme\n * @param archetype - Player archetype (musa, amsalja, hacker, jeongbo_yowon, jojik_pokryeokbae)\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playArchetypeMusic = useCallback(\n async (archetype: string, fadeInDuration: number = 2000) => {\n const archetypeMap: Record<string, string> = {\n musa: \"musa_warrior_theme\",\n amsalja: \"amsalja_shadow_theme\",\n hacker: \"hacker_cyber_theme\",\n jeongbo_yowon: \"jeongbo_intel_theme\",\n jojik_pokryeokbae: \"jojik_street_theme\",\n };\n\n const musicId = archetypeMap[archetype.toLowerCase()];\n\n if (!musicId) {\n console.warn(`Unknown archetype: ${archetype}, using combat theme`);\n await playCombatMusic(fadeInDuration);\n return;\n }\n\n try {\n await audio.fadeIn(musicId, fadeInDuration);\n } catch (error) {\n console.warn(`Failed to play archetype music: ${musicId}`, error);\n // Fallback to combat theme\n await playCombatMusic(fadeInDuration);\n }\n },\n [audio, playCombatMusic],\n );\n\n /**\n * Stop combat music with fade-out\n * @param fadeOutDuration - Fade-out duration in milliseconds\n */\n const stopCombatMusic = useCallback(\n async (fadeOutDuration: number = 2000) => {\n try {\n await audio.fadeOut(fadeOutDuration);\n } catch (error) {\n console.warn(\"Failed to stop combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Get number of currently active sounds\n * @returns Number of active sounds\n */\n const getActiveSoundCount = useCallback((): number => {\n return activeSounds.current.size;\n }, []);\n\n /**\n * Play bone impact sound with body region and intensity awareness\n * Implements realistic bone/flesh audio with fracture detection\n *\n * @param options - Bone impact event parameters\n * @param options.region - Body region struck (head, torso, arms, legs, soft_tissue)\n * @param options.intensity - Impact intensity (auto-calculated if omitted)\n * @param options.damage - Damage amount (for intensity calculation)\n * @param options.remainingHealth - Target's remaining health (for fracture detection)\n * @param options.vitalPoint - Whether strike hit a vital point\n * @param options.hitPosition - 3D position of strike (for auto region detection)\n *\n * @example\n * // Explicit region and intensity\n * playBoneImpactSound({ region: 'head', intensity: 'heavy' });\n *\n * @example\n * // Auto-calculate intensity from damage and health\n * playBoneImpactSound({\n * region: 'torso',\n * damage: 35,\n * remainingHealth: 25,\n * vitalPoint: false\n * });\n *\n * @example\n * // Auto-detect region from 3D hit position\n * playBoneImpactSound({\n * damage: 40,\n * remainingHealth: 60,\n * hitPosition: { x: 0.1, y: 1.8, z: 0 }\n * });\n */\n const playBoneImpactSound = useCallback(\n async (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => {\n const soundType = \"bone_impact\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n // Auto-detect region from hit position if not provided\n let region = options.region;\n if (!region && options.hitPosition) {\n region = detectAudioBodyRegion(options.hitPosition);\n }\n\n // Default to torso if region still undefined\n region ??= \"torso\";\n\n // Auto-calculate intensity if not provided\n let intensity = options.intensity;\n if (!intensity && options.damage !== undefined) {\n intensity = calculateImpactIntensity(\n options.damage,\n options.remainingHealth,\n options.vitalPoint,\n );\n }\n\n // Default to medium intensity if still undefined\n intensity ??= \"medium\";\n\n // Get appropriate sound ID with random variant\n const soundId = getBoneImpactSoundId(region, intensity, true);\n\n // Get volume multiplier based on intensity\n const volumeMultiplier = getImpactVolumeMultiplier(intensity);\n const finalVolume = Math.min(1.0, 0.8 * volumeMultiplier);\n\n try {\n await audio.playSFX(soundId, finalVolume);\n lastPlayTime.current[soundType] = Date.now();\n\n // Longer active duration for fracture sounds (more impactful)\n const duration = intensity === \"fracture\" ? 800 : 500;\n registerActiveSound(soundId, duration);\n } catch (error) {\n console.warn(\n `Failed to play bone impact sound: ${soundId} (region: ${region}, intensity: ${intensity})`,\n error,\n );\n }\n },\n [audio, canPlaySound, registerActiveSound],\n );\n\n return {\n playAttackSound,\n playHitSound,\n playBlockSound,\n playDodgeSound,\n playStanceChangeSound,\n playSpecialTechniqueSound,\n playCombatMusic,\n playArchetypeMusic,\n stopCombatMusic,\n getActiveSoundCount,\n playBoneImpactSound, // NEW: Body-region-specific bone/flesh impact sounds\n };\n};\n\nexport default useCombatAudio;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useCombatAudio.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatAudio.ts"],"sourcesContent":["/**\n * Combat Audio Hook for Black Trigram\n * Provides comprehensive audio feedback for combat actions including\n * bone impact sounds, fracture audio, and body-region-specific hit sounds\n */\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport {\n calculateImpactIntensity,\n detectAudioBodyRegion,\n getBoneImpactSoundId,\n getImpactVolumeMultiplier,\n} from \"../../../../audio/BoneImpactAudioMap\";\nimport { AudioBodyRegion, ImpactIntensity } from \"../../../../audio/types\";\n\n/**\n * Attack intensity levels for sound selection\n */\nexport type AttackIntensity = \"light\" | \"medium\" | \"heavy\" | \"critical\";\n\n/**\n * Maximum number of simultaneous sounds to prevent audio chaos\n */\nconst MAX_SIMULTANEOUS_SOUNDS = 5;\n\n/**\n * Combat audio hook for playing attack, hit, block, dodge, and stance sounds\n * @returns Object with methods for playing various combat sounds\n */\nexport const useCombatAudio = () => {\n const audio = useAudio();\n const lastPlayTime = useRef<Record<string, number>>({});\n const activeSounds = useRef(new Set<string>());\n const timeoutIds = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());\n\n // Cleanup all timeouts on unmount\n useEffect(() => {\n // Copy ref value to local variable for cleanup\n const timeoutIdsSet = timeoutIds.current;\n return () => {\n timeoutIdsSet.forEach(clearTimeout);\n timeoutIdsSet.clear();\n };\n }, []);\n\n /**\n * Check if we can play a sound (rate limiting and simultaneous sound check)\n * @param soundType - Type of sound to check\n * @param minInterval - Minimum interval between plays in milliseconds\n * @returns True if sound can be played\n */\n const canPlaySound = useCallback(\n (soundType: string, minInterval = 50): boolean => {\n const now = Date.now();\n const lastTime = lastPlayTime.current[soundType] ?? 0;\n\n // Rate limiting check\n if (now - lastTime < minInterval) {\n return false;\n }\n\n // Check simultaneous sounds limit\n if (activeSounds.current.size >= MAX_SIMULTANEOUS_SOUNDS) {\n return false;\n }\n\n return true;\n },\n [],\n );\n\n /**\n * Register a sound as active and auto-remove after duration\n * @param soundId - ID of the sound to register\n * @param duration - Duration in milliseconds (default 500ms)\n */\n const registerActiveSound = useCallback((soundId: string, duration = 500) => {\n activeSounds.current.add(soundId);\n const timeoutId = setTimeout(() => {\n activeSounds.current.delete(soundId);\n timeoutIds.current.delete(timeoutId);\n }, duration);\n timeoutIds.current.add(timeoutId);\n }, []);\n\n /**\n * Get a random variant from a pool\n * @param base - Base sound ID\n * @param count - Number of variants\n * @returns Random variant ID\n */\n const getRandomVariant = useCallback(\n (base: string, count: number): string => {\n const variant = Math.floor(Math.random() * count) + 1;\n return `${base}_${variant}`;\n },\n [],\n );\n\n /**\n * Play attack sound based on intensity\n * @param intensity - Attack intensity (light, medium, heavy, critical)\n */\n const playAttackSound = useCallback(\n async (intensity: AttackIntensity = \"light\") => {\n const soundType = `attack_${intensity}`;\n\n if (!canPlaySound(soundType)) {\n return;\n }\n\n let soundId: string;\n\n switch (intensity) {\n case \"light\":\n // 8 variations of light punch sounds\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n break;\n case \"medium\":\n // 4 variations of medium punch sounds\n soundId = getRandomVariant(\"attack_punch_medium\", 4);\n break;\n case \"heavy\":\n soundId = \"attack_heavy\";\n break;\n case \"critical\":\n // 4 variations of critical attack sounds\n soundId = getRandomVariant(\"attack_critical\", 4);\n break;\n default:\n console.warn(\n `Unknown attack intensity: ${intensity}, defaulting to light`,\n );\n soundId = getRandomVariant(\"attack_punch_light\", 8);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play attack sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play hit reaction sound based on damage amount\n * @param damage - Damage amount to determine hit intensity\n */\n const playHitSound = useCallback(\n async (damage: number) => {\n const soundType = \"hit\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n let soundId: string;\n\n // Determine hit intensity based on damage\n if (damage >= 40) {\n // Critical hit (4 variations)\n soundId = getRandomVariant(\"hit_critical\", 4);\n } else if (damage >= 25) {\n // Heavy hit (4 variations)\n soundId = getRandomVariant(\"hit_heavy\", 4);\n } else if (damage >= 10) {\n // Medium hit (4 variations)\n soundId = getRandomVariant(\"hit_medium\", 4);\n } else {\n // Light hit (4 variations)\n soundId = getRandomVariant(\"hit_light\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 500);\n } catch (error) {\n console.warn(`Failed to play hit sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play block/parry sound\n * @param guardBroken - Whether the guard was broken or successfully blocked\n */\n const playBlockSound = useCallback(\n async (guardBroken: boolean = false) => {\n const soundType = \"block\";\n\n if (!canPlaySound(soundType, 150)) {\n return;\n }\n\n let soundId: string;\n\n if (guardBroken) {\n // 4 variations of guard break sounds\n soundId = getRandomVariant(\"block_break\", 4);\n } else {\n // 4 variations of successful block sounds\n soundId = getRandomVariant(\"block_success\", 4);\n }\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play block sound: ${soundId}`, error);\n }\n },\n [audio, canPlaySound, getRandomVariant, registerActiveSound],\n );\n\n /**\n * Play dodge sound\n */\n const playDodgeSound = useCallback(async () => {\n const soundType = \"dodge\";\n\n if (!canPlaySound(soundType, 200)) {\n return;\n }\n\n // 8 variations of dodge sounds\n const soundId = getRandomVariant(\"dodge\", 8);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 300);\n } catch (error) {\n console.warn(`Failed to play dodge sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play stance change sound\n */\n const playStanceChangeSound = useCallback(async () => {\n const soundType = \"stance\";\n\n if (!canPlaySound(soundType, 250)) {\n return;\n }\n\n // 4 variations of stance change sounds\n const soundId = getRandomVariant(\"stance_change\", 4);\n\n try {\n await audio.playSFX(soundId);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 400);\n } catch (error) {\n console.warn(`Failed to play stance change sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play special technique sound (e.g., Geon special)\n */\n const playSpecialTechniqueSound = useCallback(async () => {\n const soundType = \"special\";\n\n if (!canPlaySound(soundType, 300)) {\n return;\n }\n\n // 4 variations of special Geon technique sounds\n const soundId = getRandomVariant(\"attack_special_geon\", 4);\n\n try {\n await audio.playSFX(soundId, 0.8);\n lastPlayTime.current[soundType] = Date.now();\n registerActiveSound(soundId, 600);\n } catch (error) {\n console.warn(`Failed to play special technique sound: ${soundId}`, error);\n }\n }, [audio, canPlaySound, getRandomVariant, registerActiveSound]);\n\n /**\n * Play combat theme music with fade-in\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playCombatMusic = useCallback(\n async (fadeInDuration: number = 2000) => {\n try {\n await audio.fadeIn(\"combat_theme\", fadeInDuration);\n } catch (error) {\n console.warn(\"Failed to play combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Play archetype-specific music theme\n * @param archetype - Player archetype (musa, amsalja, hacker, jeongbo_yowon, jojik_pokryeokbae)\n * @param fadeInDuration - Fade-in duration in milliseconds\n */\n const playArchetypeMusic = useCallback(\n async (archetype: string, fadeInDuration: number = 2000) => {\n const archetypeMap: Record<string, string> = {\n musa: \"musa_warrior_theme\",\n amsalja: \"amsalja_shadow_theme\",\n hacker: \"hacker_cyber_theme\",\n jeongbo_yowon: \"jeongbo_intel_theme\",\n jojik_pokryeokbae: \"jojik_street_theme\",\n };\n\n const musicId = archetypeMap[archetype.toLowerCase()];\n\n if (!musicId) {\n console.warn(`Unknown archetype: ${archetype}, using combat theme`);\n await playCombatMusic(fadeInDuration);\n return;\n }\n\n try {\n await audio.fadeIn(musicId, fadeInDuration);\n } catch (error) {\n console.warn(`Failed to play archetype music: ${musicId}`, error);\n // Fallback to combat theme\n await playCombatMusic(fadeInDuration);\n }\n },\n [audio, playCombatMusic],\n );\n\n /**\n * Stop combat music with fade-out\n * @param fadeOutDuration - Fade-out duration in milliseconds\n */\n const stopCombatMusic = useCallback(\n async (fadeOutDuration: number = 2000) => {\n try {\n await audio.fadeOut(fadeOutDuration);\n } catch (error) {\n console.warn(\"Failed to stop combat music\", error);\n }\n },\n [audio],\n );\n\n /**\n * Get number of currently active sounds\n * @returns Number of active sounds\n */\n const getActiveSoundCount = useCallback((): number => {\n return activeSounds.current.size;\n }, []);\n\n /**\n * Play bone impact sound with body region and intensity awareness\n * Implements realistic bone/flesh audio with fracture detection\n *\n * @param options - Bone impact event parameters\n * @param options.region - Body region struck (head, torso, arms, legs, soft_tissue)\n * @param options.intensity - Impact intensity (auto-calculated if omitted)\n * @param options.damage - Damage amount (for intensity calculation)\n * @param options.remainingHealth - Target's remaining health (for fracture detection)\n * @param options.vitalPoint - Whether strike hit a vital point\n * @param options.hitPosition - 3D position of strike (for auto region detection)\n *\n * @example\n * // Explicit region and intensity\n * playBoneImpactSound({ region: 'head', intensity: 'heavy' });\n *\n * @example\n * // Auto-calculate intensity from damage and health\n * playBoneImpactSound({\n * region: 'torso',\n * damage: 35,\n * remainingHealth: 25,\n * vitalPoint: false\n * });\n *\n * @example\n * // Auto-detect region from 3D hit position\n * playBoneImpactSound({\n * damage: 40,\n * remainingHealth: 60,\n * hitPosition: { x: 0.1, y: 1.8, z: 0 }\n * });\n */\n const playBoneImpactSound = useCallback(\n async (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => {\n const soundType = \"bone_impact\";\n\n if (!canPlaySound(soundType, 100)) {\n return;\n }\n\n // Auto-detect region from hit position if not provided\n let region = options.region;\n if (!region && options.hitPosition) {\n region = detectAudioBodyRegion(options.hitPosition);\n }\n\n // Default to torso if region still undefined\n region ??= \"torso\";\n\n // Auto-calculate intensity if not provided\n let intensity = options.intensity;\n if (!intensity && options.damage !== undefined) {\n intensity = calculateImpactIntensity(\n options.damage,\n options.remainingHealth,\n options.vitalPoint,\n );\n }\n\n // Default to medium intensity if still undefined\n intensity ??= \"medium\";\n\n // Get appropriate sound ID with random variant\n const soundId = getBoneImpactSoundId(region, intensity, true);\n\n // Get volume multiplier based on intensity\n const volumeMultiplier = getImpactVolumeMultiplier(intensity);\n const finalVolume = Math.min(1.0, 0.8 * volumeMultiplier);\n\n try {\n await audio.playSFX(soundId, finalVolume);\n lastPlayTime.current[soundType] = Date.now();\n\n // Longer active duration for fracture sounds (more impactful)\n const duration = intensity === \"fracture\" ? 800 : 500;\n registerActiveSound(soundId, duration);\n } catch (error) {\n console.warn(\n `Failed to play bone impact sound: ${soundId} (region: ${region}, intensity: ${intensity})`,\n error,\n );\n }\n },\n [audio, canPlaySound, registerActiveSound],\n );\n\n return {\n playAttackSound,\n playHitSound,\n playBlockSound,\n playDodgeSound,\n playStanceChangeSound,\n playSpecialTechniqueSound,\n playCombatMusic,\n playArchetypeMusic,\n stopCombatMusic,\n getActiveSoundCount,\n playBoneImpactSound, // NEW: Body-region-specific bone/flesh impact sounds\n };\n};\n\nexport default useCombatAudio;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCombatLayout.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatLayout.ts"],"sourcesContent":["/**\n * useCombatLayout Hook - Enhanced Responsive Combat Layout\n *\n * Custom hook for managing responsive combat screen layout calculations with\n * comprehensive support for all screen sizes from mobile to ultra-wide displays.\n *\n * Enhanced Features:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional scaling for consistent sizing across devices\n * - Optimized arena sizing for each device category\n * - Smooth transitions for resize operations\n * - 60fps performance maintained\n *\n * Uses robust device detection combining user-agent and screen size to ensure\n * mobile controls are shown on all mobile devices, including high-resolution phones.\n *\n * Performance:\n * - Reduces recalculations by checking only breakpoint changes, not exact dimensions\n * - Memoizes arena bounds to prevent cascading re-renders\n * - Targets <1ms execution time for layout calculations\n *\n * @param width - Screen width\n * @param height - Screen height\n *\n * @returns Layout constants and arena bounds\n *\n * @example\n * ```typescript\n * const { layoutConstants, arenaBounds, isMobile, screenSize } = useCombatLayout(1200, 800);\n * ```\n */\n\nimport { useMemo } from \"react\";\nimport { getScreenSize } from \"../../../../systems/ResponsiveScaling\";\nimport { calculateArenaWorldDimensions } from \"../../../../utils/arenaWorldDimensions\";\nimport { shouldUseMobileControls } from \"../../../../utils/deviceDetection\";\nimport { calculateMobileAreaBounds } from \"../../../../utils/mobileLayoutHelpers\";\nimport {\n mobileControlsBottomClearance,\n PORTRAIT_FORCE_MAX_WIDTH_PX,\n PORTRAIT_HYSTERESIS_FACTOR,\n} from \"../../../../utils/responsiveOrientationConstants\";\nimport {\n getCombatLayoutConstants,\n getDesktopArenaWidthBudget,\n} from \"../../../../utils/responsiveLayoutHelpers\";\n\nimport type { ScreenSize } from \"../../../../systems/ResponsiveScaling\";\n\nexport interface LayoutConstants {\n readonly padding: number;\n readonly hudHeight: number;\n readonly controlsHeight: number;\n readonly footerHeight: number;\n readonly healthBarHeight: number;\n}\n\nexport interface ArenaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number; // 3D scale factor for arena (1.0 = desktop, <1.0 = mobile)\n readonly worldWidthMeters: number; // Physical arena width in meters\n readonly worldDepthMeters: number; // Physical arena depth in meters\n}\n\nexport interface CombatLayout {\n readonly layoutConstants: LayoutConstants;\n readonly arenaBounds: ArenaBounds;\n readonly isMobile: boolean;\n readonly isPortrait: boolean;\n readonly screenSize: ScreenSize;\n}\n\n/**\n * Custom hook for combat screen layout calculations\n * Enhanced with centralized responsive scaling system\n * Optimized to reduce recalculations and improve 60fps performance\n */\nexport function useCombatLayout(width: number, height: number): CombatLayout {\n // Determine screen size category using centralized scaling system\n const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n // Portrait orientation detection. The hysteresis factor provides stability\n // so viewports near 1:1 don't flap on every resize event.\n // 세로 모드 감지\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n // Device detection has its own internal caching based on screen dimensions.\n // In addition to its user-agent result we force the mobile branch for any\n // narrow portrait viewport so that devtools emulation and real rotated\n // phones both render the mobile-optimized layout.\n // 모바일 레이아웃 강제: 세로 + 좁은 화면\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n // Centralized layout constants for easier tweaking\n // Enhanced with tablet-specific values for better responsive support\n // Updated mobile controls height for new sizing: D-Pad (140px), buttons (80px+70px)\n // Uses centralized responsive helper for consistent scaling\n // Now passes isMobile flag to ensure high-res mobile devices get mobile layouts\n const layoutConstants = useMemo<LayoutConstants>(\n () => getCombatLayoutConstants(width, isMobile),\n [width, isMobile],\n );\n\n // Arena bounds calculation using physics-first aspect-ratio sizing\n // Landscape mobile: 4:3 (width > height)\n // Portrait mobile: 3:4 (height > width) — fits both fighters vertically\n // without being occluded by bottom HUD + D-Pad\n const arenaBounds = useMemo<ArenaBounds>(() => {\n // In portrait mobile we render a compact two-player status strip\n // directly below the top HUD to replace the collapsed side HUDs.\n // Reserve its height here so the arena is pushed below it instead of\n // being drawn underneath. Use a tighter strip on extra-small phones\n // (< 380 px wide) to preserve the playable arena area.\n const isExtraSmallWidth = width < 380;\n const portraitStatusStripHeight =\n isMobile && isPortrait\n ? Math.max(isExtraSmallWidth ? 28 : 36, Math.round(height * 0.055))\n : 0;\n\n const arenaY =\n layoutConstants.hudHeight +\n portraitStatusStripHeight +\n layoutConstants.padding;\n\n // Calculate world dimensions based on screen resolution (not device type)\n // All arenas are SQUARE for consistent combat mechanics\n const worldDimensions = calculateArenaWorldDimensions(width);\n\n // Mobile-specific arena sizing for better screen fit\n if (isMobile) {\n const isExtraSmall = isExtraSmallWidth;\n const minTopClearance =\n (isExtraSmall ? 75 : 80) + portraitStatusStripHeight;\n\n // In portrait we must reserve space for the whole bottom band\n // (technique bar + mobile controls + footer) or the arena ends up\n // behind the D-Pad. See responsiveOrientationConstants.ts for the\n // derivation of the mobile-controls reservation.\n const minBottomClearance = mobileControlsBottomClearance(\n layoutConstants.controlsHeight,\n layoutConstants.footerHeight,\n isExtraSmall,\n isPortrait,\n \"combat\",\n );\n\n const mobileBounds = calculateMobileAreaBounds(\n width,\n height,\n minTopClearance,\n minBottomClearance,\n arenaY,\n isPortrait ? \"portrait\" : \"landscape\",\n );\n\n // Mobile bounds already include world dimensions from resolution\n return mobileBounds;\n }\n\n // Desktop arena sizing - create 4:3 aspect ratio arena\n const totalReservedHeight =\n layoutConstants.hudHeight +\n layoutConstants.controlsHeight +\n layoutConstants.footerHeight;\n const totalPadding = layoutConstants.padding * 3;\n const availableHeight = height - totalReservedHeight - totalPadding;\n const availableWidth = getDesktopArenaWidthBudget(width);\n\n // Calculate arena dimensions with 4:3 aspect ratio (width > height)\n // Start with available width, constrain by height if needed\n let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n // If height is constrained, recalculate from height\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\n // Calculate pixels-per-meter and scale\n const pixelsPerMeter = arenaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - arenaWidth) / 2, // Center horizontally\n y: arenaY,\n width: arenaWidth,\n height: arenaHeight, // 4:3 aspect ratio\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n }, [width, height, layoutConstants, isMobile, isPortrait]);\n\n return {\n layoutConstants,\n arenaBounds,\n isMobile,\n isPortrait,\n screenSize,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,SAAgB,gBAAgB,OAAe,QAA8B;CAE3E,MAAM,aAAa,cAAc,cAAc,MAAM,EAAE,CAAC,MAAM,CAAC;CAK/D,MAAM,aAAa,SAAS,QAAQ;CAOpC,MAAM,WACJ,yBAAyB,IACxB,cAAc,QAAA;CAOjB,MAAM,kBAAkB,cAChB,yBAAyB,OAAO,SAAS,EAC/C,CAAC,OAAO,SAAS,CAClB;
|
|
1
|
+
{"version":3,"file":"useCombatLayout.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatLayout.ts"],"sourcesContent":["/**\n * useCombatLayout Hook - Enhanced Responsive Combat Layout\n *\n * Custom hook for managing responsive combat screen layout calculations with\n * comprehensive support for all screen sizes from mobile to ultra-wide displays.\n *\n * Enhanced Features:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional scaling for consistent sizing across devices\n * - Optimized arena sizing for each device category\n * - Smooth transitions for resize operations\n * - 60fps performance maintained\n *\n * Uses robust device detection combining user-agent and screen size to ensure\n * mobile controls are shown on all mobile devices, including high-resolution phones.\n *\n * Performance:\n * - Reduces recalculations by checking only breakpoint changes, not exact dimensions\n * - Memoizes arena bounds to prevent cascading re-renders\n * - Targets <1ms execution time for layout calculations\n *\n * @param width - Screen width\n * @param height - Screen height\n *\n * @returns Layout constants and arena bounds\n *\n * @example\n * ```typescript\n * const { layoutConstants, arenaBounds, isMobile, screenSize } = useCombatLayout(1200, 800);\n * ```\n */\n\nimport { useMemo } from \"react\";\nimport { getScreenSize } from \"../../../../systems/ResponsiveScaling\";\nimport { calculateArenaWorldDimensions } from \"../../../../utils/arenaWorldDimensions\";\nimport { shouldUseMobileControls } from \"../../../../utils/deviceDetection\";\nimport { calculateMobileAreaBounds } from \"../../../../utils/mobileLayoutHelpers\";\nimport {\n mobileControlsBottomClearance,\n PORTRAIT_FORCE_MAX_WIDTH_PX,\n PORTRAIT_HYSTERESIS_FACTOR,\n} from \"../../../../utils/responsiveOrientationConstants\";\nimport {\n getCombatLayoutConstants,\n getDesktopArenaWidthBudget,\n} from \"../../../../utils/responsiveLayoutHelpers\";\n\nimport type { ScreenSize } from \"../../../../systems/ResponsiveScaling\";\n\nexport interface LayoutConstants {\n readonly padding: number;\n readonly hudHeight: number;\n readonly controlsHeight: number;\n readonly footerHeight: number;\n readonly healthBarHeight: number;\n}\n\nexport interface ArenaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number; // 3D scale factor for arena (1.0 = desktop, <1.0 = mobile)\n readonly worldWidthMeters: number; // Physical arena width in meters\n readonly worldDepthMeters: number; // Physical arena depth in meters\n}\n\nexport interface CombatLayout {\n readonly layoutConstants: LayoutConstants;\n readonly arenaBounds: ArenaBounds;\n readonly isMobile: boolean;\n readonly isPortrait: boolean;\n readonly screenSize: ScreenSize;\n}\n\n/**\n * Custom hook for combat screen layout calculations\n * Enhanced with centralized responsive scaling system\n * Optimized to reduce recalculations and improve 60fps performance\n */\nexport function useCombatLayout(width: number, height: number): CombatLayout {\n // Determine screen size category using centralized scaling system\n const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n // Portrait orientation detection. The hysteresis factor provides stability\n // so viewports near 1:1 don't flap on every resize event.\n // 세로 모드 감지\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n // Device detection has its own internal caching based on screen dimensions.\n // In addition to its user-agent result we force the mobile branch for any\n // narrow portrait viewport so that devtools emulation and real rotated\n // phones both render the mobile-optimized layout.\n // 모바일 레이아웃 강제: 세로 + 좁은 화면\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n // Centralized layout constants for easier tweaking\n // Enhanced with tablet-specific values for better responsive support\n // Updated mobile controls height for new sizing: D-Pad (140px), buttons (80px+70px)\n // Uses centralized responsive helper for consistent scaling\n // Now passes isMobile flag to ensure high-res mobile devices get mobile layouts\n const layoutConstants = useMemo<LayoutConstants>(\n () => getCombatLayoutConstants(width, isMobile),\n [width, isMobile],\n );\n\n // Arena bounds calculation using physics-first aspect-ratio sizing\n // Landscape mobile: 4:3 (width > height)\n // Portrait mobile: 3:4 (height > width) — fits both fighters vertically\n // without being occluded by bottom HUD + D-Pad\n const arenaBounds = useMemo<ArenaBounds>(() => {\n // In portrait mobile we render a compact two-player status strip\n // directly below the top HUD to replace the collapsed side HUDs.\n // Reserve its height here so the arena is pushed below it instead of\n // being drawn underneath. Use a tighter strip on extra-small phones\n // (< 380 px wide) to preserve the playable arena area.\n const isExtraSmallWidth = width < 380;\n const portraitStatusStripHeight =\n isMobile && isPortrait\n ? Math.max(isExtraSmallWidth ? 28 : 36, Math.round(height * 0.055))\n : 0;\n\n const arenaY =\n layoutConstants.hudHeight +\n portraitStatusStripHeight +\n layoutConstants.padding;\n\n // Calculate world dimensions based on screen resolution (not device type)\n // All arenas are SQUARE for consistent combat mechanics\n const worldDimensions = calculateArenaWorldDimensions(width);\n\n // Mobile-specific arena sizing for better screen fit\n if (isMobile) {\n const isExtraSmall = isExtraSmallWidth;\n const minTopClearance =\n (isExtraSmall ? 75 : 80) + portraitStatusStripHeight;\n\n // In portrait we must reserve space for the whole bottom band\n // (technique bar + mobile controls + footer) or the arena ends up\n // behind the D-Pad. See responsiveOrientationConstants.ts for the\n // derivation of the mobile-controls reservation.\n const minBottomClearance = mobileControlsBottomClearance(\n layoutConstants.controlsHeight,\n layoutConstants.footerHeight,\n isExtraSmall,\n isPortrait,\n \"combat\",\n );\n\n const mobileBounds = calculateMobileAreaBounds(\n width,\n height,\n minTopClearance,\n minBottomClearance,\n arenaY,\n isPortrait ? \"portrait\" : \"landscape\",\n );\n\n // Mobile bounds already include world dimensions from resolution\n return mobileBounds;\n }\n\n // Desktop arena sizing - create 4:3 aspect ratio arena\n const totalReservedHeight =\n layoutConstants.hudHeight +\n layoutConstants.controlsHeight +\n layoutConstants.footerHeight;\n const totalPadding = layoutConstants.padding * 3;\n const availableHeight = height - totalReservedHeight - totalPadding;\n const availableWidth = getDesktopArenaWidthBudget(width);\n\n // Calculate arena dimensions with 4:3 aspect ratio (width > height)\n // Start with available width, constrain by height if needed\n let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n // If height is constrained, recalculate from height\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\n // Calculate pixels-per-meter and scale\n const pixelsPerMeter = arenaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - arenaWidth) / 2, // Center horizontally\n y: arenaY,\n width: arenaWidth,\n height: arenaHeight, // 4:3 aspect ratio\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n }, [width, height, layoutConstants, isMobile, isPortrait]);\n\n return {\n layoutConstants,\n arenaBounds,\n isMobile,\n isPortrait,\n screenSize,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,SAAgB,gBAAgB,OAAe,QAA8B;CAE3E,MAAM,aAAa,cAAc,cAAc,MAAM,EAAE,CAAC,MAAM,CAAC;CAK/D,MAAM,aAAa,SAAS,QAAQ;CAOpC,MAAM,WACJ,yBAAyB,IACxB,cAAc,QAAA;CAOjB,MAAM,kBAAkB,cAChB,yBAAyB,OAAO,SAAS,EAC/C,CAAC,OAAO,SAAS,CAClB;CA8FD,OAAO;EACL;EACA,aA1FkB,cAA2B;GAM7C,MAAM,oBAAoB,QAAQ;GAClC,MAAM,4BACJ,YAAY,aACR,KAAK,IAAI,oBAAoB,KAAK,IAAI,KAAK,MAAM,SAAS,KAAM,CAAC,GACjE;GAEN,MAAM,SACJ,gBAAgB,YAChB,4BACA,gBAAgB;GAIlB,MAAM,kBAAkB,8BAA8B,MAAM;GAG5D,IAAI,UAAU;IACZ,MAAM,eAAe;IA0BrB,OAVqB,0BACnB,OACA,SAhBC,eAAe,KAAK,MAAM,2BAMF,8BACzB,gBAAgB,gBAChB,gBAAgB,cAChB,cACA,YACA,SAOA,EACA,QACA,aAAa,aAAa,YAIrB;;GAIT,MAAM,sBACJ,gBAAgB,YAChB,gBAAgB,iBAChB,gBAAgB;GAClB,MAAM,eAAe,gBAAgB,UAAU;GAC/C,MAAM,kBAAkB,SAAS,sBAAsB;GAKvD,IAAI,aAJmB,2BAA2B,MAIjC;GACjB,IAAI,cAAc,cAAc,IAAI;GAGpC,IAAI,cAAc,iBAAiB;IACjC,cAAc;IACd,aAAa,eAAe,IAAI;;GAMlC,MAAM,QAFiB,aAAa,gBAAgB,cAErB;GAE/B,OAAO;IACL,IAAI,QAAQ,cAAc;IAC1B,GAAG;IACH,OAAO;IACP,QAAQ;IACR;IACA,kBAAkB,gBAAgB;IAClC,kBAAkB,gBAAgB;IACnC;KACA;GAAC;GAAO;GAAQ;GAAiB;GAAU;GAAW,CAIvD;EACA;EACA;EACA;EACD"}
|