blacktrigram 0.7.45 → 0.7.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioAssetLoader.js.map +1 -1
- package/lib/audio/AudioAssetRegistry.js.map +1 -1
- package/lib/audio/AudioCache.js.map +1 -1
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioMonitor.js.map +1 -1
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/AudioProvider.js.map +1 -1
- package/lib/audio/AudioUtils.js.map +1 -1
- package/lib/audio/BoneImpactAudioMap.js.map +1 -1
- package/lib/audio/VariantSelector.js.map +1 -1
- package/lib/audio/types.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.d.ts.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +22 -11
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
- package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
- package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.d.ts.map +1 -1
- package/lib/components/screens/combat/helpers/AnimationUpdater.js +4 -2
- package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
- package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/Key3D.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
- package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.d.ts.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +1 -0
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
- package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.d.ts +1 -0
- package/lib/components/screens/training/hooks/useTrainingActions.d.ts.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +6 -4
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
- package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.d.ts.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js +7 -5
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
- package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/constants/bodyDimensions.js.map +1 -1
- package/lib/constants/bodyRenderingConstants.js.map +1 -1
- package/lib/data/archetypeClothing.js.map +1 -1
- package/lib/data/archetypePhysicalAttributes.js.map +1 -1
- package/lib/data/techniqueMappings.js.map +1 -1
- package/lib/data/techniques.js.map +1 -1
- package/lib/hooks/useActionFeedback.js.map +1 -1
- package/lib/hooks/useBalanceAnimations.js.map +1 -1
- package/lib/hooks/useCombatTimer.js.map +1 -1
- package/lib/hooks/useDebounce.js.map +1 -1
- package/lib/hooks/useHUDLayout.js.map +1 -1
- package/lib/hooks/useHandPoseTransitions.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useMatchCountdown.js.map +1 -1
- package/lib/hooks/useMuscleActivation.js.map +1 -1
- package/lib/hooks/usePauseMenu.js.map +1 -1
- package/lib/hooks/usePlayerAnimation.js.map +1 -1
- package/lib/hooks/useResponsiveLayout.js.map +1 -1
- package/lib/hooks/useRoundTransition.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.d.ts.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +1 -1
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTechniqueSelection.js.map +1 -1
- package/lib/hooks/useThrottle.js.map +1 -1
- package/lib/hooks/useTouchControls.js.map +1 -1
- package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
- package/lib/hooks/useWindowSize.js.map +1 -1
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/EffectCalculator.js.map +1 -1
- package/lib/systems/LayoutSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ResponsiveScaling.js.map +1 -1
- package/lib/systems/TrigramSystem.js.map +1 -1
- package/lib/systems/VitalPointSystem.js.map +1 -1
- package/lib/systems/ai/AIPersonality.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js +16 -16
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
- package/lib/systems/ai/ComboSystem.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/ai/TrainingAI.js.map +1 -1
- package/lib/systems/ai/types.js.map +1 -1
- package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/HandPoses.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.d.ts +6 -0
- package/lib/systems/animation/builders/KickPhaseApplicator.d.ts.map +1 -1
- package/lib/systems/animation/builders/KickPhaseApplicator.js +16 -9
- package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/KoreanGuardPositions.d.ts +4 -4
- package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js +5 -5
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.d.ts +112 -71
- package/lib/systems/animation/builders/MartialArtsConstants.d.ts.map +1 -1
- package/lib/systems/animation/builders/MartialArtsConstants.js +113 -72
- package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
- package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
- package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
- package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/EnhancedAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/EnhancedElbowKneeAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamStanceAnimations.js +21 -0
- package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -0
- package/lib/systems/animation/catalogs/GamTechniqueAnimations.js +34 -2
- package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts +9 -0
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts.map +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +288 -0
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -0
- package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/KickAnimations.d.ts +2 -2
- package/lib/systems/animation/catalogs/KickAnimations.js +2 -2
- package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js +14 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.d.ts +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceGuardPoses.d.ts +6 -6
- package/lib/systems/animation/catalogs/StanceGuardPoses.js +36 -36
- package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
- package/lib/systems/animation/constants/AnatomicalLimits.js.map +1 -1
- package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
- package/lib/systems/animation/core/AnimationPriority.js +15 -15
- package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.d.ts +30 -0
- package/lib/systems/animation/core/AnimationRegistry.d.ts.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.js +74 -12
- package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js +16 -16
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.d.ts.map +1 -1
- package/lib/systems/animation/core/AnimationTransitions.js +34 -0
- package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
- package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
- package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
- package/lib/systems/animation/core/index.d.ts +1 -1
- package/lib/systems/animation/core/index.d.ts.map +1 -1
- package/lib/systems/animation/core/types.d.ts +24 -0
- package/lib/systems/animation/core/types.d.ts.map +1 -1
- package/lib/systems/animation/core/types.js +27 -11
- package/lib/systems/animation/core/types.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
- package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
- package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
- package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
- package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
- package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
- package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/bodypart/types.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js +19 -19
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/breathing/feedback.js.map +1 -1
- package/lib/systems/breathing/integration.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js +19 -19
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js +17 -17
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js +24 -24
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/FallIntegration.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js +21 -21
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
- package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
- package/lib/systems/combat/typeGuards.js.map +1 -1
- package/lib/systems/effects.js.map +1 -1
- package/lib/systems/game.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
- package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
- package/lib/systems/movement/integration.js.map +1 -1
- package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
- package/lib/systems/physics/CollisionDetection.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
- package/lib/systems/physics/SpeedModifierSystem.js +6 -6
- package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
- package/lib/systems/trigram/KoreanCulture.js.map +1 -1
- package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
- package/lib/systems/trigram/StanceManager.js.map +1 -1
- package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
- package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
- package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GeonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/JinTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/LiTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TaeTechniques.js.map +1 -1
- package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
- package/lib/systems/trigram/techniques/index.js.map +1 -1
- package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
- package/lib/systems/trigram/types.js.map +1 -1
- package/lib/systems/types.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
- package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
- package/lib/systems/vitalpoint/VitalPointsData.js.map +1 -1
- package/lib/types/AccessibilityTypes.js.map +1 -1
- package/lib/types/LayoutTypes.js.map +1 -1
- package/lib/types/PhysicsTypes.js.map +1 -1
- package/lib/types/common.js.map +1 -1
- package/lib/types/constants/animations.js.map +1 -1
- package/lib/types/constants/colors.js.map +1 -1
- package/lib/types/constants/designSystem.js.map +1 -1
- package/lib/types/constants/index.js.map +1 -1
- package/lib/types/constants/layout.js.map +1 -1
- package/lib/types/constants/performance.js.map +1 -1
- package/lib/types/constants/typography.js.map +1 -1
- package/lib/types/constants/ui.js.map +1 -1
- package/lib/types/facial.js +19 -19
- package/lib/types/facial.js.map +1 -1
- package/lib/types/hand-animation.js.map +1 -1
- package/lib/types/injury.js.map +1 -1
- package/lib/types/muscle.js.map +1 -1
- package/lib/types/physics.js.map +1 -1
- package/lib/types/physicsConstants.js.map +1 -1
- package/lib/types/player-visual.d.ts +1 -1
- package/lib/types/player-visual.d.ts.map +1 -1
- package/lib/types/skeletal.js.map +1 -1
- package/lib/types/techniqueId.js.map +1 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/arenaWorldDimensions.js.map +1 -1
- package/lib/utils/assetConfig.js.map +1 -1
- package/lib/utils/characterScaling.js.map +1 -1
- package/lib/utils/colorHelpers.js.map +1 -1
- package/lib/utils/colorUtils.js.map +1 -1
- package/lib/utils/combatReadiness.js.map +1 -1
- package/lib/utils/controlMapping.js.map +1 -1
- package/lib/utils/deviceDetection.js +6 -7
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/fabricTextures.js.map +1 -1
- package/lib/utils/hapticFeedback.js.map +1 -1
- package/lib/utils/haptics.js.map +1 -1
- package/lib/utils/htmlOverlayHelpers.js.map +1 -1
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/math.js.map +1 -1
- package/lib/utils/mobileLayoutHelpers.js.map +1 -1
- package/lib/utils/mobileUIUtils.js.map +1 -1
- package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
- package/lib/utils/performanceOptimization.js.map +1 -1
- package/lib/utils/player3DHelpers.js.map +1 -1
- package/lib/utils/playerUtils.js.map +1 -1
- package/lib/utils/responsiveLayout.js.map +1 -1
- package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
- package/lib/utils/responsiveOrientationConstants.js.map +1 -1
- package/lib/utils/safeAreaUtils.js.map +1 -1
- package/lib/utils/sharedPhysicsConfig.js.map +1 -1
- package/lib/utils/skeletonScaling.js.map +1 -1
- package/lib/utils/stanceHelpers.js.map +1 -1
- package/lib/utils/threeObjectPool.js.map +1 -1
- package/lib/utils/visualEffects.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"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 if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\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 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 const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n let injuryType = determineInjuryType(result, technique);\n\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 const severity = Math.min(1.0, result.damage / 30);\n\n const basePosition = getBodyRegionPosition(bodyRegion);\n\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 const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\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 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 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 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 let attackTechnique: KoreanTechnique;\n\n if (technique) {\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\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 const selectedTechnique = availableTechniques[0];\n\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 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 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 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 const hitPosition = calculateHitPosition(playerPositions[1]);\n\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 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 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 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 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 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 if (result.knockback.duration > 0) {\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\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 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 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 const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\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 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 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 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 const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 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 onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n combatAudio?.playStanceChangeSound?.();\n\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\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 const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\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 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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const handleAIDefend = useCallback(() => {\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 const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\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 combatAudio?.playSpecialTechniqueSound();\n\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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\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 let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\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 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 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 if (technique.damageType === \"slashing\") {\n return result.damage > 20 ? InjuryType.LACERATION : InjuryType.CUT;\n }\n\n if (result.damage > 25) {\n return InjuryType.BRUISE;\n }\n\n if (result.damage > 15) {\n return InjuryType.BRUISE;\n }\n\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 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 const bodyRegion = BodyRegion.TORSO; // TODO: Extract from result when available\n\n let injuryType = determineInjuryType(result, technique);\n\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 const severity = Math.min(1.0, result.damage / 30);\n\n const basePosition = getBodyRegionPosition(bodyRegion);\n\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 const newPos = {\n x: defenderPos.x + result.knockback.displacement.x,\n y: defenderPos.y + result.knockback.displacement.z,\n };\n\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 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 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 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 let attackTechnique: KoreanTechnique;\n\n if (technique) {\n attackTechnique = convertTechniqueToKorean(technique, currentStance);\n } else {\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 const selectedTechnique = availableTechniques[0];\n\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 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 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 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 const hitPosition = calculateHitPosition(playerPositions[1]);\n\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 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 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 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 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 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 if (result.knockback.duration > 0) {\n if (player2KnockbackTimeoutRef.current) {\n clearTimeout(player2KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(1, { isStunned: true });\n\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 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 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 const handleDefend = useCallback(() => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 combatAudio?.playSpecialTechniqueSound();\n\n addHitEffect(HitEffectType.CRITICAL_HIT, playerPositions[0], 1.5);\n\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 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 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 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 const handleStanceSwitch = useCallback(\n (stance: TrigramStance) => {\n if (!combatState.roundStarted || combatState.roundEnded) return;\n\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 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 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 onPlayerUpdate(playerIndex, result.updatedPlayer);\n\n onLateralityUpdate?.(playerIndex, result.laterality);\n\n combatAudio?.playStanceChangeSound?.();\n\n const koreanText =\n result.laterality === \"left\" ? \"왼발서기\" : \"오른발서기\";\n const englishText =\n result.laterality === \"left\" ? \"Left Stance\" : \"Right Stance\";\n addCombatMessage(koreanText, englishText);\n\n addHitEffect(\n HitEffectType.STATUS_EFFECT,\n playerPositions[playerIndex],\n 0.5,\n );\n } else {\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 const attackTechnique = technique ?? createAITechnique(\"basic\", aiPlayer);\n\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 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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const handleAIDefend = useCallback(() => {\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 const specialTechnique =\n technique ?? createAITechnique(\"special\", aiPlayer);\n\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 combatAudio?.playSpecialTechniqueSound();\n\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 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 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 const { updatedAttacker, updatedDefender } =\n combatSystem.applyCombatResult(result, aiPlayer, targetPlayer);\n\n onPlayerUpdate(1, updatedAttacker);\n onPlayerUpdate(0, updatedDefender);\n\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 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 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 if (result.knockback.duration > 0) {\n if (player1KnockbackTimeoutRef.current) {\n clearTimeout(player1KnockbackTimeoutRef.current);\n }\n\n onPlayerUpdate(0, { isStunned: true });\n\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 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 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 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 const moveAIPlayer = useCallback(\n (targetPos: Position) => {\n const currentPos = playerPositions[1];\n const aiPlayer = validPlayers[1];\n\n const AI_DECISION_FREQUENCY_HZ = 20; // 20 calls/second (50ms interval)\n const baseSpeed = 2.5 / AI_DECISION_FREQUENCY_HZ; // meters per call (0.125m per call)\n\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 let finalSpeed = baseSpeed;\n if (aiPlayer.bodyPartHealth && aiPlayer.bodyPartMaxHealth) {\n const movementDirection = {\n x: distance > 0 ? dx / distance : 0,\n y: distance > 0 ? dy / distance : 0,\n };\n\n finalSpeed = movementPenaltySystem.calculateModifiedSpeed(\n baseSpeed,\n aiPlayer.bodyPartHealth,\n aiPlayer.bodyPartMaxHealth,\n movementDirection,\n );\n }\n\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 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 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 const p1Duration = player1AnimationDuration ?? animationDuration;\n const p2Duration = player2AnimationDuration ?? animationDuration;\n\n const player1PhysicsRef = useRef(new AttackMovementPhysics());\n const player2PhysicsRef = useRef(new AttackMovementPhysics());\n\n const player1BasePosVectorRef = useRef(new THREE.Vector3());\n const player2BasePosVectorRef = useRef(new THREE.Vector3());\n\n const player1AttackStartTimeRef = useRef<number | null>(null);\n const player1MovementResultRef = useRef<ReturnType<\n typeof player1PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n const player2AttackStartTimeRef = useRef<number | null>(null);\n const player2MovementResultRef = useRef<ReturnType<\n typeof player2PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n const [player1Position, setPlayer1Position] =\n useState<[number, number, number]>(player1BasePosition);\n const [player2Position, setPlayer2Position] =\n useState<[number, number, number]>(player2BasePosition);\n\n const [player1IsLunging, setPlayer1IsLunging] = useState(false);\n const [player2IsLunging, setPlayer2IsLunging] = useState(false);\n\n const player1WasAttackingRef = useRef(false);\n const player2WasAttackingRef = useRef(false);\n\n useEffect(() => {\n const wasAttacking = player1WasAttackingRef.current;\n player1WasAttackingRef.current = player1Attacking;\n\n if (player1Attacking && player1AnimationType && !wasAttacking) {\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 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 setPlayer1Position(player1BasePosition);\n setPlayer1IsLunging(false);\n player1AttackStartTimeRef.current = null;\n player1MovementResultRef.current = null;\n return;\n }\n\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 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 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 useEffect(() => {\n const wasAttacking = player2WasAttackingRef.current;\n player2WasAttackingRef.current = player2Attacking;\n\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 const p1Duration = player1AnimationDuration ?? animationDuration;\n const p2Duration = player2AnimationDuration ?? animationDuration;\n\n const player1PhysicsRef = useRef(new AttackMovementPhysics());\n const player2PhysicsRef = useRef(new AttackMovementPhysics());\n\n const player1BasePosVectorRef = useRef(new THREE.Vector3());\n const player2BasePosVectorRef = useRef(new THREE.Vector3());\n\n const player1AttackStartTimeRef = useRef<number | null>(null);\n const player1MovementResultRef = useRef<ReturnType<\n typeof player1PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n const player2AttackStartTimeRef = useRef<number | null>(null);\n const player2MovementResultRef = useRef<ReturnType<\n typeof player2PhysicsRef.current.calculateAttackMovement\n > | null>(null);\n\n const [player1Position, setPlayer1Position] =\n useState<[number, number, number]>(player1BasePosition);\n const [player2Position, setPlayer2Position] =\n useState<[number, number, number]>(player2BasePosition);\n\n const [player1IsLunging, setPlayer1IsLunging] = useState(false);\n const [player2IsLunging, setPlayer2IsLunging] = useState(false);\n\n const player1WasAttackingRef = useRef(false);\n const player2WasAttackingRef = useRef(false);\n\n useEffect(() => {\n const wasAttacking = player1WasAttackingRef.current;\n player1WasAttackingRef.current = player1Attacking;\n\n if (player1Attacking && player1AnimationType && !wasAttacking) {\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 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 setPlayer1Position(player1BasePosition);\n setPlayer1IsLunging(false);\n player1AttackStartTimeRef.current = null;\n player1MovementResultRef.current = null;\n return;\n }\n\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 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 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 useEffect(() => {\n const wasAttacking = player2WasAttackingRef.current;\n player2WasAttackingRef.current = player2Attacking;\n\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 useEffect(() => {\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 if (now - lastTime < minInterval) {\n return false;\n }\n\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 soundId = getRandomVariant(\"attack_punch_light\", 8);\n break;\n case \"medium\":\n soundId = getRandomVariant(\"attack_punch_medium\", 4);\n break;\n case \"heavy\":\n soundId = \"attack_heavy\";\n break;\n case \"critical\":\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 if (damage >= 40) {\n soundId = getRandomVariant(\"hit_critical\", 4);\n } else if (damage >= 25) {\n soundId = getRandomVariant(\"hit_heavy\", 4);\n } else if (damage >= 10) {\n soundId = getRandomVariant(\"hit_medium\", 4);\n } else {\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 soundId = getRandomVariant(\"block_break\", 4);\n } else {\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 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 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 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 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 let region = options.region;\n if (!region && options.hitPosition) {\n region = detectAudioBodyRegion(options.hitPosition);\n }\n\n region ??= \"torso\";\n\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 intensity ??= \"medium\";\n\n const soundId = getBoneImpactSoundId(region, intensity, true);\n\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 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 useEffect(() => {\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 if (now - lastTime < minInterval) {\n return false;\n }\n\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 soundId = getRandomVariant(\"attack_punch_light\", 8);\n break;\n case \"medium\":\n soundId = getRandomVariant(\"attack_punch_medium\", 4);\n break;\n case \"heavy\":\n soundId = \"attack_heavy\";\n break;\n case \"critical\":\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 if (damage >= 40) {\n soundId = getRandomVariant(\"hit_critical\", 4);\n } else if (damage >= 25) {\n soundId = getRandomVariant(\"hit_heavy\", 4);\n } else if (damage >= 10) {\n soundId = getRandomVariant(\"hit_medium\", 4);\n } else {\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 soundId = getRandomVariant(\"block_break\", 4);\n } else {\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 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 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 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 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 let region = options.region;\n if (!region && options.hitPosition) {\n region = detectAudioBodyRegion(options.hitPosition);\n }\n\n region ??= \"torso\";\n\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 intensity ??= \"medium\";\n\n const soundId = getBoneImpactSoundId(region, intensity, true);\n\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 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 const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n const layoutConstants = useMemo<LayoutConstants>(\n () => getCombatLayoutConstants(width, isMobile),\n [width, isMobile],\n );\n\n const arenaBounds = useMemo<ArenaBounds>(() => {\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 const worldDimensions = calculateArenaWorldDimensions(width);\n\n if (isMobile) {\n const isExtraSmall = isExtraSmallWidth;\n const minTopClearance =\n (isExtraSmall ? 75 : 80) + portraitStatusStripHeight;\n\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 return mobileBounds;\n }\n\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 let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\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;CAC3E,MAAM,aAAa,cAAc,cAAc,
|
|
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 const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n const layoutConstants = useMemo<LayoutConstants>(\n () => getCombatLayoutConstants(width, isMobile),\n [width, isMobile],\n );\n\n const arenaBounds = useMemo<ArenaBounds>(() => {\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 const worldDimensions = calculateArenaWorldDimensions(width);\n\n if (isMobile) {\n const isExtraSmall = isExtraSmallWidth;\n const minTopClearance =\n (isExtraSmall ? 75 : 80) + portraitStatusStripHeight;\n\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 return mobileBounds;\n }\n\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 let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\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;CAC3E,MAAM,aAAa,cAAc,cAAc,KAAK,GAAG,CAAC,KAAK,CAAC;CAE9D,MAAM,aAAa,SAAS,QAAQ;CAEpC,MAAM,WACJ,wBAAwB,KACvB,cAAc,QAAA;CAEjB,MAAM,kBAAkB,cAChB,yBAAyB,OAAO,QAAQ,GAC9C,CAAC,OAAO,QAAQ,CAClB;CAwEA,OAAO;EACL;EACA,aAxEkB,cAA2B;GAC7C,MAAM,oBAAoB,QAAQ;GAClC,MAAM,4BACJ,YAAY,aACR,KAAK,IAAI,oBAAoB,KAAK,IAAI,KAAK,MAAM,SAAS,IAAK,CAAC,IAChE;GAEN,MAAM,SACJ,gBAAgB,YAChB,4BACA,gBAAgB;GAElB,MAAM,kBAAkB,8BAA8B,KAAK;GAE3D,IAAI,UAAU;IACZ,MAAM,eAAe;IAqBrB,OATqB,0BACnB,OACA,SAZC,eAAe,KAAK,MAAM,2BAEF,8BACzB,gBAAgB,gBAChB,gBAAgB,cAChB,cACA,YACA,QAOA,GACA,QACA,aAAa,aAAa,WAGrB;GACT;GAEA,MAAM,sBACJ,gBAAgB,YAChB,gBAAgB,iBAChB,gBAAgB;GAClB,MAAM,eAAe,gBAAgB,UAAU;GAC/C,MAAM,kBAAkB,SAAS,sBAAsB;GAGvD,IAAI,aAFmB,2BAA2B,KAEjC;GACjB,IAAI,cAAc,cAAc,IAAI;GAEpC,IAAI,cAAc,iBAAiB;IACjC,cAAc;IACd,aAAa,eAAe,IAAI;GAClC;GAIA,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;GACpC;EACF,GAAG;GAAC;GAAO;GAAQ;GAAiB;GAAU;EAAU,CAItD;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCombatState.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatState.ts"],"sourcesContent":["/**\n * useCombatState Hook - Consolidated Combat State Management\n * \n * Custom hook for managing complex combat state using useReducer.\n * Consolidates multiple useState calls into a single reducer for better\n * performance and maintainability.\n *\n * Performance:\n * - Uses useReducer to batch state updates\n * - Reduces number of re-renders\n * - Provides clear action-based state updates\n *\n * @returns Combat state and dispatch actions\n * \n * @example\n * ```typescript\n * const { state, actions } = useCombatState();\n * actions.setHitEffects([effect1, effect2]);\n * actions.incrementCombo();\n * ```\n */\n\nimport { HitEffect } from \"@/systems\";\nimport type { StanceLaterality } from \"@/systems/trigram/types\";\nimport { useCallback, useReducer } from \"react\";\n\n/**\n * Combat screen state managed by the reducer\n * Note: Renamed from CombatState to avoid conflict with CombatState enum in types/common.ts\n */\nexport interface CombatScreenState {\n readonly hitEffects: HitEffect[];\n readonly isExecutingTechnique: boolean;\n readonly combatMessages: string[];\n readonly roundStarted: boolean;\n readonly roundEnded: boolean;\n readonly roundDisplayStatus: \"start\" | \"fight\" | \"ko\" | \"end\" | null;\n readonly comboCount: number;\n readonly lastHitTime: number;\n readonly screenShake: { x: number; y: number };\n readonly playerLaterality: readonly [StanceLaterality, StanceLaterality];\n}\n\n/**\n * Combat state actions\n */\ntype CombatAction =\n | { type: \"SET_HIT_EFFECTS\"; payload: HitEffect[] }\n | { type: \"ADD_HIT_EFFECT\"; payload: HitEffect }\n | { type: \"REMOVE_HIT_EFFECT\"; payload: string }\n | { type: \"SET_EXECUTING_TECHNIQUE\"; payload: boolean }\n | { type: \"ADD_COMBAT_MESSAGE\"; payload: string }\n | { type: \"SET_ROUND_STARTED\"; payload: boolean }\n | { type: \"SET_ROUND_ENDED\"; payload: boolean }\n | { type: \"SET_ROUND_DISPLAY_STATUS\"; payload: \"start\" | \"fight\" | \"ko\" | \"end\" | null }\n | { type: \"SET_COMBO_COUNT\"; payload: number }\n | { type: \"INCREMENT_COMBO\" }\n | { type: \"RESET_COMBO\" }\n | { type: \"SET_LAST_HIT_TIME\"; payload: number }\n | { type: \"SET_SCREEN_SHAKE\"; payload: { x: number; y: number } }\n | { type: \"RESET_SCREEN_SHAKE\" }\n | { type: \"RESET_ROUND_STATE\" }\n | { type: \"SET_PLAYER_LATERALITY\"; payload: readonly [StanceLaterality, StanceLaterality] }\n | { type: \"SET_PLAYER_LATERALITY_INDEX\"; payload: { index: 0 | 1; laterality: StanceLaterality } };\n\n/**\n * Initial combat state\n */\nconst initialState: CombatScreenState = {\n hitEffects: [],\n isExecutingTechnique: false,\n combatMessages: [],\n roundStarted: false,\n roundEnded: false,\n roundDisplayStatus: null,\n comboCount: 0,\n lastHitTime: 0,\n screenShake: { x: 0, y: 0 },\n playerLaterality: [\"right\", \"right\"],\n};\n\n/**\n * Combat state reducer\n * Handles all combat state updates in a centralized, performant way\n */\nfunction combatReducer(state: CombatScreenState, action: CombatAction): CombatScreenState {\n switch (action.type) {\n case \"SET_HIT_EFFECTS\":\n return { ...state, hitEffects: action.payload };\n\n case \"ADD_HIT_EFFECT\":\n return { ...state, hitEffects: [...state.hitEffects, action.payload] };\n\n case \"REMOVE_HIT_EFFECT\":\n return {\n ...state,\n hitEffects: state.hitEffects.filter((effect) => effect.id !== action.payload),\n };\n\n case \"SET_EXECUTING_TECHNIQUE\":\n return { ...state, isExecutingTechnique: action.payload };\n\n case \"ADD_COMBAT_MESSAGE\":\n return {\n ...state,\n combatMessages: [action.payload, ...state.combatMessages.slice(0, 4)],\n };\n\n case \"SET_ROUND_STARTED\":\n return { ...state, roundStarted: action.payload };\n\n case \"SET_ROUND_ENDED\":\n return { ...state, roundEnded: action.payload };\n\n case \"SET_ROUND_DISPLAY_STATUS\":\n return { ...state, roundDisplayStatus: action.payload };\n\n case \"SET_COMBO_COUNT\":\n return { ...state, comboCount: action.payload };\n\n case \"INCREMENT_COMBO\":\n return { ...state, comboCount: state.comboCount + 1 };\n\n case \"RESET_COMBO\":\n return { ...state, comboCount: 0 };\n\n case \"SET_LAST_HIT_TIME\":\n return { ...state, lastHitTime: action.payload };\n\n case \"SET_SCREEN_SHAKE\":\n return { ...state, screenShake: action.payload };\n\n case \"RESET_SCREEN_SHAKE\":\n return { ...state, screenShake: { x: 0, y: 0 } };\n\n case \"RESET_ROUND_STATE\":\n return {\n ...state,\n roundStarted: false,\n roundEnded: false,\n roundDisplayStatus: null,\n comboCount: 0,\n hitEffects: [],\n combatMessages: [],\n };\n\n case \"SET_PLAYER_LATERALITY\":\n return { ...state, playerLaterality: action.payload };\n\n case \"SET_PLAYER_LATERALITY_INDEX\":\n const newLaterality: [StanceLaterality, StanceLaterality] = [...state.playerLaterality] as [StanceLaterality, StanceLaterality];\n newLaterality[action.payload.index] = action.payload.laterality;\n return { ...state, playerLaterality: newLaterality };\n\n default:\n return state;\n }\n}\n\n/**\n * Combat state action creators\n */\nexport interface CombatActions {\n readonly setHitEffects: (effects: HitEffect[]) => void;\n readonly addHitEffect: (effect: HitEffect) => void;\n readonly removeHitEffect: (effectId: string) => void;\n readonly setExecutingTechnique: (executing: boolean) => void;\n readonly addCombatMessage: (message: string) => void;\n readonly setRoundStarted: (started: boolean) => void;\n readonly setRoundEnded: (ended: boolean) => void;\n readonly setRoundDisplayStatus: (status: \"start\" | \"fight\" | \"ko\" | \"end\" | null) => void;\n readonly setComboCount: (count: number) => void;\n readonly incrementCombo: () => void;\n readonly resetCombo: () => void;\n readonly setLastHitTime: (time: number) => void;\n readonly setScreenShake: (shake: { x: number; y: number }) => void;\n readonly resetScreenShake: () => void;\n readonly resetRoundState: () => void;\n readonly setPlayerLaterality: (laterality: readonly [StanceLaterality, StanceLaterality]) => void;\n readonly setPlayerLateralityIndex: (index: 0 | 1, laterality: StanceLaterality) => void;\n}\n\n/**\n * Custom hook for combat state management\n */\nexport function useCombatState() {\n const [state, dispatch] = useReducer(combatReducer, initialState);\n\n const actions: CombatActions = {\n setHitEffects: useCallback(\n (effects: HitEffect[]) => dispatch({ type: \"SET_HIT_EFFECTS\", payload: effects }),\n []\n ),\n addHitEffect: useCallback(\n (effect: HitEffect) => dispatch({ type: \"ADD_HIT_EFFECT\", payload: effect }),\n []\n ),\n removeHitEffect: useCallback(\n (effectId: string) => dispatch({ type: \"REMOVE_HIT_EFFECT\", payload: effectId }),\n []\n ),\n setExecutingTechnique: useCallback(\n (executing: boolean) =>\n dispatch({ type: \"SET_EXECUTING_TECHNIQUE\", payload: executing }),\n []\n ),\n addCombatMessage: useCallback(\n (message: string) => dispatch({ type: \"ADD_COMBAT_MESSAGE\", payload: message }),\n []\n ),\n setRoundStarted: useCallback(\n (started: boolean) => dispatch({ type: \"SET_ROUND_STARTED\", payload: started }),\n []\n ),\n setRoundEnded: useCallback(\n (ended: boolean) => dispatch({ type: \"SET_ROUND_ENDED\", payload: ended }),\n []\n ),\n setRoundDisplayStatus: useCallback(\n (status: \"start\" | \"fight\" | \"ko\" | \"end\" | null) =>\n dispatch({ type: \"SET_ROUND_DISPLAY_STATUS\", payload: status }),\n []\n ),\n setComboCount: useCallback(\n (count: number) => dispatch({ type: \"SET_COMBO_COUNT\", payload: count }),\n []\n ),\n incrementCombo: useCallback(() => dispatch({ type: \"INCREMENT_COMBO\" }), []),\n resetCombo: useCallback(() => dispatch({ type: \"RESET_COMBO\" }), []),\n setLastHitTime: useCallback(\n (time: number) => dispatch({ type: \"SET_LAST_HIT_TIME\", payload: time }),\n []\n ),\n setScreenShake: useCallback(\n (shake: { x: number; y: number }) =>\n dispatch({ type: \"SET_SCREEN_SHAKE\", payload: shake }),\n []\n ),\n resetScreenShake: useCallback(() => dispatch({ type: \"RESET_SCREEN_SHAKE\" }), []),\n resetRoundState: useCallback(() => dispatch({ type: \"RESET_ROUND_STATE\" }), []),\n setPlayerLaterality: useCallback(\n (laterality: readonly [StanceLaterality, StanceLaterality]) =>\n dispatch({ type: \"SET_PLAYER_LATERALITY\", payload: laterality }),\n []\n ),\n setPlayerLateralityIndex: useCallback(\n (index: 0 | 1, laterality: StanceLaterality) =>\n dispatch({ type: \"SET_PLAYER_LATERALITY_INDEX\", payload: { index, laterality } }),\n []\n ),\n };\n\n return { state, actions };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useCombatState.js","names":[],"sources":["../../../../../src/components/screens/combat/hooks/useCombatState.ts"],"sourcesContent":["/**\n * useCombatState Hook - Consolidated Combat State Management\n * \n * Custom hook for managing complex combat state using useReducer.\n * Consolidates multiple useState calls into a single reducer for better\n * performance and maintainability.\n *\n * Performance:\n * - Uses useReducer to batch state updates\n * - Reduces number of re-renders\n * - Provides clear action-based state updates\n *\n * @returns Combat state and dispatch actions\n * \n * @example\n * ```typescript\n * const { state, actions } = useCombatState();\n * actions.setHitEffects([effect1, effect2]);\n * actions.incrementCombo();\n * ```\n */\n\nimport { HitEffect } from \"@/systems\";\nimport type { StanceLaterality } from \"@/systems/trigram/types\";\nimport { useCallback, useReducer } from \"react\";\n\n/**\n * Combat screen state managed by the reducer\n * Note: Renamed from CombatState to avoid conflict with CombatState enum in types/common.ts\n */\nexport interface CombatScreenState {\n readonly hitEffects: HitEffect[];\n readonly isExecutingTechnique: boolean;\n readonly combatMessages: string[];\n readonly roundStarted: boolean;\n readonly roundEnded: boolean;\n readonly roundDisplayStatus: \"start\" | \"fight\" | \"ko\" | \"end\" | null;\n readonly comboCount: number;\n readonly lastHitTime: number;\n readonly screenShake: { x: number; y: number };\n readonly playerLaterality: readonly [StanceLaterality, StanceLaterality];\n}\n\n/**\n * Combat state actions\n */\ntype CombatAction =\n | { type: \"SET_HIT_EFFECTS\"; payload: HitEffect[] }\n | { type: \"ADD_HIT_EFFECT\"; payload: HitEffect }\n | { type: \"REMOVE_HIT_EFFECT\"; payload: string }\n | { type: \"SET_EXECUTING_TECHNIQUE\"; payload: boolean }\n | { type: \"ADD_COMBAT_MESSAGE\"; payload: string }\n | { type: \"SET_ROUND_STARTED\"; payload: boolean }\n | { type: \"SET_ROUND_ENDED\"; payload: boolean }\n | { type: \"SET_ROUND_DISPLAY_STATUS\"; payload: \"start\" | \"fight\" | \"ko\" | \"end\" | null }\n | { type: \"SET_COMBO_COUNT\"; payload: number }\n | { type: \"INCREMENT_COMBO\" }\n | { type: \"RESET_COMBO\" }\n | { type: \"SET_LAST_HIT_TIME\"; payload: number }\n | { type: \"SET_SCREEN_SHAKE\"; payload: { x: number; y: number } }\n | { type: \"RESET_SCREEN_SHAKE\" }\n | { type: \"RESET_ROUND_STATE\" }\n | { type: \"SET_PLAYER_LATERALITY\"; payload: readonly [StanceLaterality, StanceLaterality] }\n | { type: \"SET_PLAYER_LATERALITY_INDEX\"; payload: { index: 0 | 1; laterality: StanceLaterality } };\n\n/**\n * Initial combat state\n */\nconst initialState: CombatScreenState = {\n hitEffects: [],\n isExecutingTechnique: false,\n combatMessages: [],\n roundStarted: false,\n roundEnded: false,\n roundDisplayStatus: null,\n comboCount: 0,\n lastHitTime: 0,\n screenShake: { x: 0, y: 0 },\n playerLaterality: [\"right\", \"right\"],\n};\n\n/**\n * Combat state reducer\n * Handles all combat state updates in a centralized, performant way\n */\nfunction combatReducer(state: CombatScreenState, action: CombatAction): CombatScreenState {\n switch (action.type) {\n case \"SET_HIT_EFFECTS\":\n return { ...state, hitEffects: action.payload };\n\n case \"ADD_HIT_EFFECT\":\n return { ...state, hitEffects: [...state.hitEffects, action.payload] };\n\n case \"REMOVE_HIT_EFFECT\":\n return {\n ...state,\n hitEffects: state.hitEffects.filter((effect) => effect.id !== action.payload),\n };\n\n case \"SET_EXECUTING_TECHNIQUE\":\n return { ...state, isExecutingTechnique: action.payload };\n\n case \"ADD_COMBAT_MESSAGE\":\n return {\n ...state,\n combatMessages: [action.payload, ...state.combatMessages.slice(0, 4)],\n };\n\n case \"SET_ROUND_STARTED\":\n return { ...state, roundStarted: action.payload };\n\n case \"SET_ROUND_ENDED\":\n return { ...state, roundEnded: action.payload };\n\n case \"SET_ROUND_DISPLAY_STATUS\":\n return { ...state, roundDisplayStatus: action.payload };\n\n case \"SET_COMBO_COUNT\":\n return { ...state, comboCount: action.payload };\n\n case \"INCREMENT_COMBO\":\n return { ...state, comboCount: state.comboCount + 1 };\n\n case \"RESET_COMBO\":\n return { ...state, comboCount: 0 };\n\n case \"SET_LAST_HIT_TIME\":\n return { ...state, lastHitTime: action.payload };\n\n case \"SET_SCREEN_SHAKE\":\n return { ...state, screenShake: action.payload };\n\n case \"RESET_SCREEN_SHAKE\":\n return { ...state, screenShake: { x: 0, y: 0 } };\n\n case \"RESET_ROUND_STATE\":\n return {\n ...state,\n roundStarted: false,\n roundEnded: false,\n roundDisplayStatus: null,\n comboCount: 0,\n hitEffects: [],\n combatMessages: [],\n };\n\n case \"SET_PLAYER_LATERALITY\":\n return { ...state, playerLaterality: action.payload };\n\n case \"SET_PLAYER_LATERALITY_INDEX\":\n const newLaterality: [StanceLaterality, StanceLaterality] = [...state.playerLaterality] as [StanceLaterality, StanceLaterality];\n newLaterality[action.payload.index] = action.payload.laterality;\n return { ...state, playerLaterality: newLaterality };\n\n default:\n return state;\n }\n}\n\n/**\n * Combat state action creators\n */\nexport interface CombatActions {\n readonly setHitEffects: (effects: HitEffect[]) => void;\n readonly addHitEffect: (effect: HitEffect) => void;\n readonly removeHitEffect: (effectId: string) => void;\n readonly setExecutingTechnique: (executing: boolean) => void;\n readonly addCombatMessage: (message: string) => void;\n readonly setRoundStarted: (started: boolean) => void;\n readonly setRoundEnded: (ended: boolean) => void;\n readonly setRoundDisplayStatus: (status: \"start\" | \"fight\" | \"ko\" | \"end\" | null) => void;\n readonly setComboCount: (count: number) => void;\n readonly incrementCombo: () => void;\n readonly resetCombo: () => void;\n readonly setLastHitTime: (time: number) => void;\n readonly setScreenShake: (shake: { x: number; y: number }) => void;\n readonly resetScreenShake: () => void;\n readonly resetRoundState: () => void;\n readonly setPlayerLaterality: (laterality: readonly [StanceLaterality, StanceLaterality]) => void;\n readonly setPlayerLateralityIndex: (index: 0 | 1, laterality: StanceLaterality) => void;\n}\n\n/**\n * Custom hook for combat state management\n */\nexport function useCombatState() {\n const [state, dispatch] = useReducer(combatReducer, initialState);\n\n const actions: CombatActions = {\n setHitEffects: useCallback(\n (effects: HitEffect[]) => dispatch({ type: \"SET_HIT_EFFECTS\", payload: effects }),\n []\n ),\n addHitEffect: useCallback(\n (effect: HitEffect) => dispatch({ type: \"ADD_HIT_EFFECT\", payload: effect }),\n []\n ),\n removeHitEffect: useCallback(\n (effectId: string) => dispatch({ type: \"REMOVE_HIT_EFFECT\", payload: effectId }),\n []\n ),\n setExecutingTechnique: useCallback(\n (executing: boolean) =>\n dispatch({ type: \"SET_EXECUTING_TECHNIQUE\", payload: executing }),\n []\n ),\n addCombatMessage: useCallback(\n (message: string) => dispatch({ type: \"ADD_COMBAT_MESSAGE\", payload: message }),\n []\n ),\n setRoundStarted: useCallback(\n (started: boolean) => dispatch({ type: \"SET_ROUND_STARTED\", payload: started }),\n []\n ),\n setRoundEnded: useCallback(\n (ended: boolean) => dispatch({ type: \"SET_ROUND_ENDED\", payload: ended }),\n []\n ),\n setRoundDisplayStatus: useCallback(\n (status: \"start\" | \"fight\" | \"ko\" | \"end\" | null) =>\n dispatch({ type: \"SET_ROUND_DISPLAY_STATUS\", payload: status }),\n []\n ),\n setComboCount: useCallback(\n (count: number) => dispatch({ type: \"SET_COMBO_COUNT\", payload: count }),\n []\n ),\n incrementCombo: useCallback(() => dispatch({ type: \"INCREMENT_COMBO\" }), []),\n resetCombo: useCallback(() => dispatch({ type: \"RESET_COMBO\" }), []),\n setLastHitTime: useCallback(\n (time: number) => dispatch({ type: \"SET_LAST_HIT_TIME\", payload: time }),\n []\n ),\n setScreenShake: useCallback(\n (shake: { x: number; y: number }) =>\n dispatch({ type: \"SET_SCREEN_SHAKE\", payload: shake }),\n []\n ),\n resetScreenShake: useCallback(() => dispatch({ type: \"RESET_SCREEN_SHAKE\" }), []),\n resetRoundState: useCallback(() => dispatch({ type: \"RESET_ROUND_STATE\" }), []),\n setPlayerLaterality: useCallback(\n (laterality: readonly [StanceLaterality, StanceLaterality]) =>\n dispatch({ type: \"SET_PLAYER_LATERALITY\", payload: laterality }),\n []\n ),\n setPlayerLateralityIndex: useCallback(\n (index: 0 | 1, laterality: StanceLaterality) =>\n dispatch({ type: \"SET_PLAYER_LATERALITY_INDEX\", payload: { index, laterality } }),\n []\n ),\n };\n\n return { state, actions };\n}\n"],"mappings":"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"}
|