blacktrigram 0.7.98 → 0.7.99

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.
Files changed (216) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioManager.js +2 -4
  3. package/lib/audio/AudioManager.js.map +1 -1
  4. package/lib/components/screens/combat/CombatScreen3D.js +8 -9
  5. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  6. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  7. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  8. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  9. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  10. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  11. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  12. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  13. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  14. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js +1 -1
  15. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  16. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  17. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js +2 -1
  18. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  19. package/lib/components/screens/combat/components/effects/InternalDamage3D.js +3 -1
  20. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  23. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +16 -15
  24. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  25. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  26. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  27. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  28. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  29. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  30. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  31. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  32. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  33. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  34. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  36. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  37. package/lib/components/screens/combat/hooks/useAICombat.js +0 -1
  38. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  39. package/lib/components/screens/combat/hooks/useCombatActions.js +7 -6
  40. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  41. package/lib/components/screens/combat/hooks/useCombatLayout.js +3 -1
  42. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  43. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  44. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  45. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js +7 -6
  46. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  47. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  48. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +34 -30
  49. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  50. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  51. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  52. package/lib/components/screens/endscreen/EndScreen3D.js +2 -2
  53. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  54. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js +1 -1
  55. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  56. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  57. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  58. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  59. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  60. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js +2 -2
  61. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  62. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js +2 -1
  63. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  64. package/lib/components/screens/intro/IntroScreen3D.js +2 -4
  65. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  66. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  67. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  68. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  69. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  72. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  74. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  75. package/lib/components/screens/training/TrainingScreen3D.js +10 -9
  76. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  77. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js +14 -13
  78. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  79. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  80. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  81. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  83. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  84. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  85. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  86. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  87. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  88. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/training/components/VitalPointMarker3D.js +8 -7
  90. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  91. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js +15 -13
  92. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  93. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  94. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  95. package/lib/components/screens/training/components/hud/TrainingRightHUD.js +3 -2
  96. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  97. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  98. package/lib/components/screens/training/hooks/useTrainingActions.js +1 -2
  99. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  100. package/lib/components/screens/training/hooks/useTrainingLayout.js +3 -1
  101. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  102. package/lib/components/shared/base/BaseButton.js.map +1 -1
  103. package/lib/components/shared/base/BaseButtonOverlayHtml.js +59 -53
  104. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  105. package/lib/components/shared/base/BasePanel.js +19 -18
  106. package/lib/components/shared/base/BasePanel.js.map +1 -1
  107. package/lib/components/shared/base/BaseText.js.map +1 -1
  108. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  109. package/lib/components/shared/mobile/GestureRecognizerPure.js +22 -18
  110. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  111. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  112. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  113. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  114. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js +4 -5
  115. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  116. package/lib/components/shared/three/anatomy/BoneClothing.js +29 -24
  117. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  118. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  119. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  120. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  121. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  122. package/lib/components/shared/three/effects/ActionFeedback.js +3 -2
  123. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  124. package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
  125. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  126. package/lib/components/shared/three/effects/HitEffects3D.js +3 -2
  127. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  128. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  129. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  130. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  131. package/lib/components/shared/three/effects/VitalPointMarkers3D.js +6 -5
  132. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  133. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  134. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  135. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  136. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  137. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  138. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  139. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  140. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  141. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  142. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  143. package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -1
  144. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  145. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  146. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  147. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  148. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  149. package/lib/components/shared/three/ui/PlayerHUD.js +2 -1
  150. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  151. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  152. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  153. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  154. package/lib/components/shared/three/ui/TechniqueBar.js +25 -24
  155. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  156. package/lib/components/shared/three/ui/TechniqueCard.js +1 -3
  157. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  158. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  159. package/lib/components/shared/ui/BackButton.js +2 -1
  160. package/lib/components/shared/ui/BackButton.js.map +1 -1
  161. package/lib/components/shared/ui/BaseHUDContainer.js +69 -68
  162. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  163. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  164. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  165. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  166. package/lib/components/shared/ui/SplashScreen.js +2 -2
  167. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  168. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  169. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  170. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  171. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  172. package/lib/hooks/useKeyboardControls.js +0 -1
  173. package/lib/hooks/useKeyboardControls.js.map +1 -1
  174. package/lib/hooks/useSkeletalAnimation.js +12 -3
  175. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  176. package/lib/systems/CombatSystem.js +6 -2
  177. package/lib/systems/CombatSystem.js.map +1 -1
  178. package/lib/systems/PlayerEffectManager.js +0 -1
  179. package/lib/systems/PlayerEffectManager.js.map +1 -1
  180. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  181. package/lib/systems/ai/DecisionTree.js +10 -21
  182. package/lib/systems/ai/DecisionTree.js.map +1 -1
  183. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  184. package/lib/systems/animation/systems/AdvancedJointMovements.js +1 -3
  185. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  186. package/lib/systems/animation/systems/MuscleActivation.js +3 -4
  187. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  188. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  189. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js +2 -1
  190. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  191. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  192. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  193. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  194. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  195. package/lib/systems/combat/GrappleSystem.js +3 -9
  196. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  197. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  198. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  199. package/lib/systems/movement/InjuryMovementModifier.js +1 -1
  200. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  201. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  202. package/lib/systems/vitalpoint/DamageCalculator.js +8 -5
  203. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  204. package/lib/types/facial.js.map +1 -1
  205. package/lib/utils/accessibility.js +0 -1
  206. package/lib/utils/accessibility.js.map +1 -1
  207. package/lib/utils/effectUtils.js +1 -3
  208. package/lib/utils/effectUtils.js.map +1 -1
  209. package/lib/utils/inputSystem.js +0 -2
  210. package/lib/utils/inputSystem.js.map +1 -1
  211. package/lib/utils/koreanThemeHelpers.js +6 -3
  212. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  213. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  214. package/lib/utils/playerUtils.js +4 -2
  215. package/lib/utils/playerUtils.js.map +1 -1
  216. package/package.json +5 -5
package/lib/App2.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"App2.js","names":[],"sources":["../src/App.tsx"],"sourcesContent":["import {\n lazy,\n Suspense,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport \"./App.css\";\n// Main application component - manages game state and screen navigation\nimport { useAudio } from \"./audio/AudioProvider\";\nimport { CombatScreen3D as CombatScreen } from \"./components/screens/combat/CombatScreen3D\";\nimport { ControlsScreen3D as ControlsScreen } from \"./components/screens/controls/ControlsScreen3D\";\nimport { EndScreen3D } from \"./components/screens/endscreen\";\nimport { IntroScreen3D as IntroScreen } from \"./components/screens/intro/IntroScreen3D\";\nimport { PhilosophyScreen3D as PhilosophyScreen } from \"./components/screens/philosophy/PhilosophyScreen3D\";\nimport { PerformanceDebugOverlayHtml } from \"./components/shared/debug/PerformanceDebugOverlayHtml\";\nimport { ErrorModal } from \"./components/shared/ui/ErrorModal\";\nimport { LoadingState } from \"./components/shared/ui/LoadingState\";\nimport { SplashScreen } from \"./components/shared/ui/SplashScreen\";\nimport { PlayerState } from \"./systems\";\nimport { MatchStatistics } from \"./systems/combat\";\nimport { GameMode, PlayerArchetype } from \"./types/common\";\nimport { clearPlatformCache, detectPlatform } from \"./utils/deviceDetection\";\nimport { createPlayerFromArchetype } from \"./utils/playerUtils\";\n\n// Lazy load heavy screens\nconst TrainingScreen = lazy(() =>\n import(\"./components/screens/training/TrainingScreen3D\").then((m) => ({\n default: m.TrainingScreen3D,\n })),\n);\n\n// 150ms delay to allow WebGL context cleanup between full-screen 3D scene transitions\nconst SCREEN_TRANSITION_DELAY_MS = 150;\n// 100ms delay for lighter menu/UI transitions where WebGL teardown/re-init cost is lower\nconst MENU_TRANSITION_DELAY_MS = 100;\n\nfunction App() {\n const [gameMode, setGameMode] = useState<GameMode | null>(null);\n const [selectedArchetype, setSelectedArchetype] = useState<PlayerArchetype>(\n PlayerArchetype.MUSA,\n );\n const [isGameActive, setIsGameActive] = useState(false);\n const [gameWinner, setGameWinner] = useState<PlayerState | null>(null);\n const [matchStats, setMatchStats] = useState<MatchStatistics | null>(null);\n const [appReady, setAppReady] = useState(false);\n const [showSplash, setShowSplash] = useState(true);\n const [showAudioError, setShowAudioError] = useState(false);\n // Performance debug overlay toggle (P key in dev mode)\n const [showPerformanceDebug, setShowPerformanceDebug] = useState(false);\n // Transition state to allow WebGL cleanup between screens\n const [isTransitioning, setIsTransitioning] = useState(false);\n const pendingModeRef = useRef<{\n mode: GameMode;\n archetype?: PlayerArchetype;\n } | null>(null);\n\n // Combat players state - managed here so updates persist\n const [combatPlayers, setCombatPlayers] = useState<PlayerState[]>([]);\n\n const audio = useAudio();\n\n // Add responsive screen size detection with proper device detection\n // Uses user-agent detection first for high-res mobile devices\n const [screenSize, setScreenSize] = useState(() => {\n const platform = detectPlatform();\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n };\n });\n\n useEffect(() => {\n // Define handlers outside async function for proper cleanup\n const handleGlobalError = (e: ErrorEvent) => {\n console.error(\"Global error:\", e.error);\n };\n\n const handleUnhandledRejection = (e: PromiseRejectionEvent) => {\n console.error(\"Unhandled promise rejection:\", e.reason);\n if (\n e.reason?.message?.includes(\"Failed to load\") ||\n e.reason?.message?.includes(\"no supported source\")\n ) {\n e.preventDefault();\n }\n };\n\n const initializeApp = async () => {\n try {\n window.focus();\n\n window.addEventListener(\"error\", handleGlobalError);\n window.addEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n // PHASE 2: Performance optimization initialization\n console.log(\"🔧 Initializing animation performance optimizations...\");\n\n // 1. Prewarm object pools for animation optimization\n // This eliminates GC pressure from ~1,344 allocations per frame\n const { ThreeObjectPools } = await import(\"./utils/threeObjectPool\");\n ThreeObjectPools.prewarmAll();\n const poolStatus = ThreeObjectPools.getStatus();\n console.log(\" ✓ Object pools prewarmed:\", poolStatus);\n\n // 2. Precompute all animations for 90%+ cache hit rate\n const { precomputeAnimation } =\n await import(\"./systems/animation/core/AnimationOptimizations\");\n const { ALL_ANIMATIONS } =\n await import(\"./systems/animation/core/AnimationRegistry\");\n\n let precomputedCount = 0;\n ALL_ANIMATIONS.forEach((animation) => {\n // Precompute at 60fps for smooth playback\n // Use animation.name as the unique identifier\n precomputeAnimation(animation.name, animation, 60);\n precomputedCount++;\n });\n console.log(` ✓ Precomputed ${precomputedCount} animations at 60fps`);\n\n console.log(\n \"✅ Animation optimizations ready (expect <5ms frame time, 90%+ cache hit)\",\n );\n\n setAppReady(true);\n console.log(\"🎯 Black Trigram app initialized\");\n } catch (error) {\n console.error(\"Failed to initialize app:\", error);\n setAppReady(true);\n }\n };\n\n initializeApp();\n\n // Cleanup global event handlers to prevent memory leaks\n return () => {\n window.removeEventListener(\"error\", handleGlobalError);\n window.removeEventListener(\n \"unhandledrejection\",\n handleUnhandledRejection,\n );\n };\n }, []);\n\n // Shared audio initialization logic for splash and retry\n const initializeAudioWithRetry = useCallback(async () => {\n if (!appReady) {\n console.warn(\"App not ready yet, please wait...\");\n return false;\n }\n try {\n await audio.initializeAudio();\n console.log(\"🎵 Audio initialized\");\n return true;\n } catch (error) {\n console.error(\"Failed to initialize audio:\", error);\n return false;\n }\n }, [audio, appReady]);\n\n // Handle splash screen start - initialize audio on user gesture\n const handleSplashStart = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorRetry = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorContinue = useCallback(() => {\n // Continue without sound\n setShowAudioError(false);\n setShowSplash(false);\n console.log(\"Continuing without audio (silent mode)\");\n }, []);\n\n // ✅ SIMPLIFIED: Handle game mode selection directly\n const handleGameStart = useCallback(\n (mode: GameMode, archetype?: PlayerArchetype) => {\n console.log(\"🎮 Starting game mode:\", mode, \"with archetype:\", archetype);\n\n // Store pending mode and start transition to allow WebGL cleanup\n pendingModeRef.current = { mode, archetype };\n setIsTransitioning(true);\n\n // Clear current mode first (unmounts Canvas)\n setGameMode(null);\n setIsGameActive(false);\n\n // After brief delay, mount new screen\n // Increased delay to allow proper WebGL context cleanup\n setTimeout(() => {\n const pending = pendingModeRef.current;\n if (!pending) return;\n\n // ✅ NEW: Handle controls and philosophy as separate modes\n if (\n pending.mode === GameMode.CONTROLS ||\n pending.mode === GameMode.PHILOSOPHY\n ) {\n setGameMode(pending.mode);\n setIsGameActive(false); // These are not game modes, just screens\n } else {\n setGameMode(pending.mode);\n setIsGameActive(true);\n }\n\n setGameWinner(null);\n setMatchStats(null);\n if (pending.archetype) {\n setSelectedArchetype(pending.archetype);\n }\n\n setIsTransitioning(false);\n pendingModeRef.current = null;\n }, SCREEN_TRANSITION_DELAY_MS); // Delay for WebGL cleanup\n },\n [],\n );\n\n const handleGameEnd = useCallback(\n (winner: number) => {\n setIsGameActive(false);\n setGameWinner(createPlayerFromArchetype(selectedArchetype, winner));\n // Reset combat players for next match\n setCombatPlayers([]);\n\n setMatchStats({\n totalDamageDealt: 150,\n totalDamageTaken: 100,\n criticalHits: 3,\n vitalPointHits: 2,\n techniquesUsed: 8,\n perfectStrikes: 1,\n consecutiveWins: 1,\n matchDuration: 120,\n totalMatches: 1,\n maxRounds: 3,\n winner: winner,\n totalRounds: 2,\n currentRound: 2,\n timeRemaining: 0,\n combatEvents: [],\n finalScore: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n roundsWon: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n player1: {\n wins: winner === 0 ? 1 : 0,\n losses: winner === 0 ? 0 : 1,\n hitsTaken: 5,\n hitsLanded: 8,\n totalDamageDealt: winner === 0 ? 150 : 100,\n totalDamageReceived: winner === 0 ? 100 : 150,\n techniques: [\"천둥벽력\", \"유수연타\"],\n perfectStrikes: winner === 0 ? 1 : 0,\n vitalPointHits: winner === 0 ? 2 : 1,\n consecutiveWins: winner === 0 ? 1 : 0,\n matchDuration: 120,\n },\n player2: {\n wins: winner === 1 ? 1 : 0,\n losses: winner === 1 ? 0 : 1,\n hitsTaken: 8,\n hitsLanded: 5,\n totalDamageDealt: winner === 1 ? 150 : 100,\n totalDamageReceived: winner === 1 ? 100 : 150,\n techniques: [\"화염지창\", \"벽력일섬\"],\n perfectStrikes: winner === 1 ? 1 : 0,\n vitalPointHits: winner === 1 ? 2 : 1,\n consecutiveWins: winner === 1 ? 1 : 0,\n matchDuration: 120,\n },\n });\n },\n [selectedArchetype],\n );\n\n const handleReturnToMenu = useCallback(() => {\n // Use same transition logic for return to menu\n setIsTransitioning(true);\n setGameMode(null);\n setIsGameActive(false);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize next combat\n setCombatPlayers([]);\n setTimeout(() => setIsTransitioning(false), MENU_TRANSITION_DELAY_MS);\n }, []);\n\n const handleRematch = useCallback(() => {\n // Restart combat with same settings\n if (!gameMode) {\n console.error(\n \"Cannot rematch: gameMode is not set. This should not happen - EndScreen only renders when gameMode is set.\",\n { gameMode, isGameActive, gameWinner, matchStats }\n );\n return;\n }\n \n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize for rematch\n setCombatPlayers([]);\n \n setTimeout(() => {\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- isGameActive, gameWinner, matchStats only used in error logging, not function logic\n }, [gameMode]);\n\n const handleViewTraining = useCallback(() => {\n // Navigate to training mode\n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n setCombatPlayers([]);\n \n setTimeout(() => {\n setGameMode(GameMode.TRAINING);\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n }, []);\n\n const renderCurrentScreen = () => {\n // Show loading during screen transitions\n if (isTransitioning) {\n return (\n <LoadingState\n progress={undefined}\n message=\"전환 중... | Transitioning...\"\n stage=\"assets\"\n />\n );\n }\n\n if (gameWinner && matchStats) {\n // ✅ NEW: Use EndScreen3D component\n return (\n <EndScreen3D\n winner={gameWinner}\n matchStats={matchStats}\n onReturnToMenu={handleReturnToMenu}\n onRematch={handleRematch}\n onViewReplay={handleViewTraining}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ NEW: Handle standalone screens first\n if (gameMode === GameMode.CONTROLS) {\n return (\n <ControlsScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n if (gameMode === GameMode.PHILOSOPHY) {\n return (\n <PhilosophyScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ SIMPLIFIED: Only active game modes use isGameActive\n if (isGameActive && gameMode) {\n switch (gameMode) {\n case GameMode.TRAINING:\n return (\n <Suspense\n fallback={\n <LoadingState\n progress={undefined}\n message=\"훈련장 로딩 중... | Loading Training...\"\n stage=\"assets\"\n />\n }\n >\n <TrainingScreen\n onPlayerUpdate={(updates) => {\n console.log(\"Training player updated:\", updates);\n }}\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n initialArchetype={selectedArchetype}\n />\n </Suspense>\n );\n case GameMode.VERSUS:\n case GameMode.PRACTICE:\n // Initialize players if not already set\n if (combatPlayers.length === 0) {\n const player1 = createPlayerFromArchetype(selectedArchetype, 0);\n const player2 = createPlayerFromArchetype(\n PlayerArchetype.AMSALJA,\n 1,\n );\n // Use setTimeout to defer state update and avoid render-during-render\n setTimeout(() => setCombatPlayers([player1, player2]), 0);\n // Return loading state while players initialize\n return (\n <LoadingState\n progress={undefined}\n message=\"전투 준비 중... | Preparing Combat...\"\n stage=\"assets\"\n />\n );\n }\n\n return (\n <CombatScreen\n players={combatPlayers}\n currentRound={1}\n timeRemaining={180}\n isPaused={false}\n onPlayerUpdate={(playerIndex, updates) => {\n // Actually update the player state so damage persists!\n setCombatPlayers((prevPlayers) => {\n const newPlayers = [...prevPlayers];\n if (newPlayers[playerIndex]) {\n newPlayers[playerIndex] = {\n ...newPlayers[playerIndex],\n ...updates,\n };\n }\n return newPlayers;\n });\n }}\n onReturnToMenu={handleReturnToMenu}\n onGameEnd={handleGameEnd}\n gameMode={gameMode}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n default:\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n }\n\n // ✅ SIMPLIFIED: Default to intro screen\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n };\n\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n containerRef.current?.focus();\n }, [appReady]);\n\n useEffect(() => {\n const handleResize = () => {\n // Clear cached platform info to get fresh detection on resize\n clearPlatformCache();\n const platform = detectPlatform();\n setScreenSize({\n width: platform.screenWidth,\n height: platform.screenHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n });\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // F9 key toggle for performance debug overlay (dev mode only)\n // Note: P key is reserved for Philosophy screen\n useEffect(() => {\n if (!import.meta.env.DEV) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"F9\") {\n e.preventDefault();\n setShowPerformanceDebug((prev) => !prev);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, []);\n\n if (!appReady) {\n return (\n <div className=\"app loading\" data-testid=\"app-container\">\n <LoadingState\n progress={undefined}\n message=\"앱 초기화 중 | Initializing app...\"\n stage=\"initialization\"\n />\n </div>\n );\n }\n\n // Show splash screen first to get user gesture for audio\n if (showSplash) {\n return (\n <div className=\"app\" data-testid=\"app-container\">\n <SplashScreen\n onStart={handleSplashStart}\n width={screenSize.width}\n height={screenSize.height}\n />\n {showAudioError && (\n <ErrorModal\n message=\"오디오 초기화에 실패했습니다. 재시도하거나 소리 없이 계속할 수 있습니다. | Audio initialization failed. You can retry or continue without sound.\"\n onRetry={handleAudioErrorRetry}\n onContinue={handleAudioErrorContinue}\n />\n )}\n </div>\n );\n }\n\n return (\n <div\n className=\"app\"\n tabIndex={0}\n ref={containerRef}\n data-testid=\"app-container\"\n >\n {/* All screens now use Three.js or pure React/HTML */}\n {renderCurrentScreen()}\n\n {/* Performance debug overlay (dev mode only, toggle with P key) */}\n {showPerformanceDebug && <PerformanceDebugOverlayHtml />}\n </div>\n );\n}\n\nexport default App;\n"],"mappings":";;;;;;;;;;;;;;;;;AA2BA,IAAM,iBAAiB,WACrB,OAAO,oDAAiD,CAAC,MAAM,OAAO,EACpE,SAAS,EAAE,iBACb,EAAE,CACJ;AAGA,IAAM,6BAA6B;AAEnC,IAAM,2BAA2B;AAEjC,SAAS,MAAM;CACb,MAAM,CAAC,UAAU,eAAe,SAA0B,IAAI;CAC9D,MAAM,CAAC,mBAAmB,wBAAwB,SAChD,gBAAgB,IAClB;CACA,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,YAAY,iBAAiB,SAA6B,IAAI;CACrE,MAAM,CAAC,YAAY,iBAAiB,SAAiC,IAAI;CACzE,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,CAAC,YAAY,iBAAiB,SAAS,IAAI;CACjD,MAAM,CAAC,gBAAgB,qBAAqB,SAAS,KAAK;CAE1D,MAAM,CAAC,sBAAsB,2BAA2B,SAAS,KAAK;CAEtE,MAAM,CAAC,iBAAiB,sBAAsB,SAAS,KAAK;CAC5D,MAAM,iBAAiB,OAGb,IAAI;CAGd,MAAM,CAAC,eAAe,oBAAoB,SAAwB,CAAC,CAAC;CAEpE,MAAM,QAAQ,SAAS;CAIvB,MAAM,CAAC,YAAY,iBAAiB,eAAe;EACjD,MAAM,WAAW,eAAe;EAChC,OAAO;GACL,OAAO,OAAO;GACd,QAAQ,OAAO;GACf,UAAU,SAAS;GACnB,UAAU,SAAS;GACnB,WAAW,SAAS;EACtB;CACF,CAAC;CAED,gBAAgB;EAEd,MAAM,qBAAqB,MAAkB;GAC3C,QAAQ,MAAM,iBAAiB,EAAE,KAAK;EACxC;EAEA,MAAM,4BAA4B,MAA6B;GAC7D,QAAQ,MAAM,gCAAgC,EAAE,MAAM;GACtD,IACE,EAAE,QAAQ,SAAS,SAAS,gBAAgB,KAC5C,EAAE,QAAQ,SAAS,SAAS,qBAAqB,GAEjD,EAAE,eAAe;EAErB;EAEA,MAAM,gBAAgB,YAAY;GAChC,IAAI;IACF,OAAO,MAAM;IAEb,OAAO,iBAAiB,SAAS,iBAAiB;IAClD,OAAO,iBAAiB,sBAAsB,wBAAwB;IAGtE,QAAQ,IAAI,wDAAwD;IAIpE,MAAM,EAAE,qBAAqB,MAAM,OAAO;IAC1C,iBAAiB,WAAW;IAC5B,MAAM,aAAa,iBAAiB,UAAU;IAC9C,QAAQ,IAAI,+BAA+B,UAAU;IAGrD,MAAM,EAAE,wBACN,MAAM,OAAO;IACf,MAAM,EAAE,mBACN,MAAM,OAAO;IAEf,IAAI,mBAAmB;IACvB,eAAe,SAAS,cAAc;KAGpC,oBAAoB,UAAU,MAAM,WAAW,EAAE;KACjD;IACF,CAAC;IACD,QAAQ,IAAI,mBAAmB,iBAAiB,qBAAqB;IAErE,QAAQ,IACN,0EACF;IAEA,YAAY,IAAI;IAChB,QAAQ,IAAI,kCAAkC;GAChD,SAAS,OAAO;IACd,QAAQ,MAAM,6BAA6B,KAAK;IAChD,YAAY,IAAI;GAClB;EACF;EAEA,cAAc;EAGd,aAAa;GACX,OAAO,oBAAoB,SAAS,iBAAiB;GACrD,OAAO,oBACL,sBACA,wBACF;EACF;CACF,GAAG,CAAC,CAAC;CAGL,MAAM,2BAA2B,YAAY,YAAY;EACvD,IAAI,CAAC,UAAU;GACb,QAAQ,KAAK,mCAAmC;GAChD,OAAO;EACT;EACA,IAAI;GACF,MAAM,MAAM,gBAAgB;GAC5B,QAAQ,IAAI,sBAAsB;GAClC,OAAO;EACT,SAAS,OAAO;GACd,QAAQ,MAAM,+BAA+B,KAAK;GAClD,OAAO;EACT;CACF,GAAG,CAAC,OAAO,QAAQ,CAAC;CAGpB,MAAM,oBAAoB,YAAY,YAAY;EAChD,kBAAkB,KAAK;EAEvB,IAAI,MADkB,yBAAyB,GAE7C,cAAc,KAAK;OAEnB,kBAAkB,IAAI;CAE1B,GAAG,CAAC,wBAAwB,CAAC;CAE7B,MAAM,wBAAwB,YAAY,YAAY;EACpD,kBAAkB,KAAK;EAEvB,IAAI,MADkB,yBAAyB,GAE7C,cAAc,KAAK;OAEnB,kBAAkB,IAAI;CAE1B,GAAG,CAAC,wBAAwB,CAAC;CAE7B,MAAM,2BAA2B,kBAAkB;EAEjD,kBAAkB,KAAK;EACvB,cAAc,KAAK;EACnB,QAAQ,IAAI,wCAAwC;CACtD,GAAG,CAAC,CAAC;CAGL,MAAM,kBAAkB,aACrB,MAAgB,cAAgC;EAC/C,QAAQ,IAAI,0BAA0B,MAAM,mBAAmB,SAAS;EAGxE,eAAe,UAAU;GAAE;GAAM;EAAU;EAC3C,mBAAmB,IAAI;EAGvB,YAAY,IAAI;EAChB,gBAAgB,KAAK;EAIrB,iBAAiB;GACf,MAAM,UAAU,eAAe;GAC/B,IAAI,CAAC,SAAS;GAGd,IACE,QAAQ,SAAS,SAAS,YAC1B,QAAQ,SAAS,SAAS,YAC1B;IACA,YAAY,QAAQ,IAAI;IACxB,gBAAgB,KAAK;GACvB,OAAO;IACL,YAAY,QAAQ,IAAI;IACxB,gBAAgB,IAAI;GACtB;GAEA,cAAc,IAAI;GAClB,cAAc,IAAI;GAClB,IAAI,QAAQ,WACV,qBAAqB,QAAQ,SAAS;GAGxC,mBAAmB,KAAK;GACxB,eAAe,UAAU;EAC3B,GAAG,0BAA0B;CAC/B,GACA,CAAC,CACH;CAEA,MAAM,gBAAgB,aACnB,WAAmB;EAClB,gBAAgB,KAAK;EACrB,cAAc,0BAA0B,mBAAmB,MAAM,CAAC;EAElE,iBAAiB,CAAC,CAAC;EAEnB,cAAc;GACZ,kBAAkB;GAClB,kBAAkB;GAClB,cAAc;GACd,gBAAgB;GAChB,gBAAgB;GAChB,gBAAgB;GAChB,iBAAiB;GACjB,eAAe;GACf,cAAc;GACd,WAAW;GACH;GACR,aAAa;GACb,cAAc;GACd,eAAe;GACf,cAAc,CAAC;GACf,YAAY;IACV,SAAS,WAAW,IAAI,IAAI;IAC5B,SAAS,WAAW,IAAI,IAAI;GAC9B;GACA,WAAW;IACT,SAAS,WAAW,IAAI,IAAI;IAC5B,SAAS,WAAW,IAAI,IAAI;GAC9B;GACA,SAAS;IACP,MAAM,WAAW,IAAI,IAAI;IACzB,QAAQ,WAAW,IAAI,IAAI;IAC3B,WAAW;IACX,YAAY;IACZ,kBAAkB,WAAW,IAAI,MAAM;IACvC,qBAAqB,WAAW,IAAI,MAAM;IAC1C,YAAY,CAAC,QAAQ,MAAM;IAC3B,gBAAgB,WAAW,IAAI,IAAI;IACnC,gBAAgB,WAAW,IAAI,IAAI;IACnC,iBAAiB,WAAW,IAAI,IAAI;IACpC,eAAe;GACjB;GACA,SAAS;IACP,MAAM,WAAW,IAAI,IAAI;IACzB,QAAQ,WAAW,IAAI,IAAI;IAC3B,WAAW;IACX,YAAY;IACZ,kBAAkB,WAAW,IAAI,MAAM;IACvC,qBAAqB,WAAW,IAAI,MAAM;IAC1C,YAAY,CAAC,QAAQ,MAAM;IAC3B,gBAAgB,WAAW,IAAI,IAAI;IACnC,gBAAgB,WAAW,IAAI,IAAI;IACnC,iBAAiB,WAAW,IAAI,IAAI;IACpC,eAAe;GACjB;EACF,CAAC;CACH,GACA,CAAC,iBAAiB,CACpB;CAEA,MAAM,qBAAqB,kBAAkB;EAE3C,mBAAmB,IAAI;EACvB,YAAY,IAAI;EAChB,gBAAgB,KAAK;EACrB,cAAc,IAAI;EAClB,cAAc,IAAI;EAElB,iBAAiB,CAAC,CAAC;EACnB,iBAAiB,mBAAmB,KAAK,GAAG,wBAAwB;CACtE,GAAG,CAAC,CAAC;CAEL,MAAM,gBAAgB,kBAAkB;EAEtC,IAAI,CAAC,UAAU;GACb,QAAQ,MACN,8GACA;IAAE;IAAU;IAAc;IAAY;GAAW,CACnD;GACA;EACF;EAEA,mBAAmB,IAAI;EACvB,cAAc,IAAI;EAClB,cAAc,IAAI;EAElB,iBAAiB,CAAC,CAAC;EAEnB,iBAAiB;GACf,gBAAgB,IAAI;GACpB,mBAAmB,KAAK;EAC1B,GAAG,0BAA0B;CAE/B,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,qBAAqB,kBAAkB;EAE3C,mBAAmB,IAAI;EACvB,cAAc,IAAI;EAClB,cAAc,IAAI;EAClB,iBAAiB,CAAC,CAAC;EAEnB,iBAAiB;GACf,YAAY,SAAS,QAAQ;GAC7B,gBAAgB,IAAI;GACpB,mBAAmB,KAAK;EAC1B,GAAG,0BAA0B;CAC/B,GAAG,CAAC,CAAC;CAEL,MAAM,4BAA4B;EAEhC,IAAI,iBACF,OACE,oBAAC,cAAD;GACE,UAAU,KAAA;GACV,SAAQ;GACR,OAAM;EACP,CAAA;EAIL,IAAI,cAAc,YAEhB,OACE,oBAAC,aAAD;GACE,QAAQ;GACI;GACZ,gBAAgB;GAChB,WAAW;GACX,cAAc;GACd,OAAO,WAAW;GAClB,QAAQ,WAAW;EACpB,CAAA;EAKL,IAAI,aAAa,SAAS,UACxB,OACE,oBAAC,kBAAD;GACE,gBAAgB;GAChB,OAAO,WAAW;GAClB,QAAQ,WAAW;EACpB,CAAA;EAIL,IAAI,aAAa,SAAS,YACxB,OACE,oBAAC,oBAAD;GACE,gBAAgB;GAChB,OAAO,WAAW;GAClB,QAAQ,WAAW;EACpB,CAAA;EAKL,IAAI,gBAAgB,UAClB,QAAQ,UAAR;GACE,KAAK,SAAS,UACZ,OACE,oBAAC,UAAD;IACE,UACE,oBAAC,cAAD;KACE,UAAU,KAAA;KACV,SAAQ;KACR,OAAM;IACP,CAAA;cAGH,oBAAC,gBAAD;KACE,iBAAiB,YAAY;MAC3B,QAAQ,IAAI,4BAA4B,OAAO;KACjD;KACA,gBAAgB;KAChB,OAAO,WAAW;KAClB,QAAQ,WAAW;KACnB,kBAAkB;IACnB,CAAA;GACO,CAAA;GAEd,KAAK,SAAS;GACd,KAAK,SAAS;IAEZ,IAAI,cAAc,WAAW,GAAG;KAC9B,MAAM,UAAU,0BAA0B,mBAAmB,CAAC;KAC9D,MAAM,UAAU,0BACd,gBAAgB,SAChB,CACF;KAEA,iBAAiB,iBAAiB,CAAC,SAAS,OAAO,CAAC,GAAG,CAAC;KAExD,OACE,oBAAC,cAAD;MACE,UAAU,KAAA;MACV,SAAQ;MACR,OAAM;KACP,CAAA;IAEL;IAEA,OACE,oBAAC,gBAAD;KACE,SAAS;KACT,cAAc;KACd,eAAe;KACf,UAAU;KACV,iBAAiB,aAAa,YAAY;MAExC,kBAAkB,gBAAgB;OAChC,MAAM,aAAa,CAAC,GAAG,WAAW;OAClC,IAAI,WAAW,cACb,WAAW,eAAe;QACxB,GAAG,WAAW;QACd,GAAG;OACL;OAEF,OAAO;MACT,CAAC;KACH;KACA,gBAAgB;KAChB,WAAW;KACD;KACV,OAAO,WAAW;KAClB,QAAQ,WAAW;IACpB,CAAA;GAEL,SACE,OACE,oBAAC,eAAD;IACE,cAAc;IACd,mBAAmB;IACA;IACnB,OAAO,WAAW;IAClB,QAAQ,WAAW;GACpB,CAAA;EAEP;EAIF,OACE,oBAAC,eAAD;GACE,cAAc;GACd,mBAAmB;GACA;GACnB,OAAO,WAAW;GAClB,QAAQ,WAAW;EACpB,CAAA;CAEL;CAEA,MAAM,eAAe,OAAuB,IAAI;CAEhD,gBAAgB;EACd,aAAa,SAAS,MAAM;CAC9B,GAAG,CAAC,QAAQ,CAAC;CAEb,gBAAgB;EACd,MAAM,qBAAqB;GAEzB,mBAAmB;GACnB,MAAM,WAAW,eAAe;GAChC,cAAc;IACZ,OAAO,SAAS;IAChB,QAAQ,SAAS;IACjB,UAAU,SAAS;IACnB,UAAU,SAAS;IACnB,WAAW,SAAS;GACtB,CAAC;EACH;EAEA,OAAO,iBAAiB,UAAU,YAAY;EAC9C,aAAa,OAAO,oBAAoB,UAAU,YAAY;CAChE,GAAG,CAAC,CAAC;CAIL,gBAAgB,CAYhB,GAAG,CAAC,CAAC;CAEL,IAAI,CAAC,UACH,OACE,oBAAC,OAAD;EAAK,WAAU;EAAc,eAAY;YACvC,oBAAC,cAAD;GACE,UAAU,KAAA;GACV,SAAQ;GACR,OAAM;EACP,CAAA;CACE,CAAA;CAKT,IAAI,YACF,OACE,qBAAC,OAAD;EAAK,WAAU;EAAM,eAAY;YAAjC,CACE,oBAAC,cAAD;GACE,SAAS;GACT,OAAO,WAAW;GAClB,QAAQ,WAAW;EACpB,CAAA,GACA,kBACC,oBAAC,YAAD;GACE,SAAQ;GACR,SAAS;GACT,YAAY;EACb,CAAA,CAEA;;CAIT,OACE,qBAAC,OAAD;EACE,WAAU;EACV,UAAU;EACV,KAAK;EACL,eAAY;YAJd,CAOG,oBAAoB,GAGpB,wBAAwB,oBAAC,6BAAD,CAA8B,CAAA,CACpD;;AAET"}
1
+ {"version":3,"file":"App2.js","names":[],"sources":["../src/App.tsx"],"sourcesContent":["import {\n lazy,\n Suspense,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport \"./App.css\";\n// Main application component - manages game state and screen navigation\nimport { useAudio } from \"./audio/AudioProvider\";\nimport { CombatScreen3D as CombatScreen } from \"./components/screens/combat/CombatScreen3D\";\nimport { ControlsScreen3D as ControlsScreen } from \"./components/screens/controls/ControlsScreen3D\";\nimport { EndScreen3D } from \"./components/screens/endscreen\";\nimport { IntroScreen3D as IntroScreen } from \"./components/screens/intro/IntroScreen3D\";\nimport { PhilosophyScreen3D as PhilosophyScreen } from \"./components/screens/philosophy/PhilosophyScreen3D\";\nimport { PerformanceDebugOverlayHtml } from \"./components/shared/debug/PerformanceDebugOverlayHtml\";\nimport { ErrorModal } from \"./components/shared/ui/ErrorModal\";\nimport { LoadingState } from \"./components/shared/ui/LoadingState\";\nimport { SplashScreen } from \"./components/shared/ui/SplashScreen\";\nimport { PlayerState } from \"./systems\";\nimport { MatchStatistics } from \"./systems/combat\";\nimport { GameMode, PlayerArchetype } from \"./types/common\";\nimport { clearPlatformCache, detectPlatform } from \"./utils/deviceDetection\";\nimport { createPlayerFromArchetype } from \"./utils/playerUtils\";\n\n// Lazy load heavy screens\nconst TrainingScreen = lazy(() =>\n import(\"./components/screens/training/TrainingScreen3D\").then((m) => ({\n default: m.TrainingScreen3D,\n })),\n);\n\n// 150ms delay to allow WebGL context cleanup between full-screen 3D scene transitions\nconst SCREEN_TRANSITION_DELAY_MS = 150;\n// 100ms delay for lighter menu/UI transitions where WebGL teardown/re-init cost is lower\nconst MENU_TRANSITION_DELAY_MS = 100;\n\nfunction App() {\n const [gameMode, setGameMode] = useState<GameMode | null>(null);\n const [selectedArchetype, setSelectedArchetype] = useState<PlayerArchetype>(\n PlayerArchetype.MUSA,\n );\n const [isGameActive, setIsGameActive] = useState(false);\n const [gameWinner, setGameWinner] = useState<PlayerState | null>(null);\n const [matchStats, setMatchStats] = useState<MatchStatistics | null>(null);\n const [appReady, setAppReady] = useState(false);\n const [showSplash, setShowSplash] = useState(true);\n const [showAudioError, setShowAudioError] = useState(false);\n // Performance debug overlay toggle (P key in dev mode)\n const [showPerformanceDebug, setShowPerformanceDebug] = useState(false);\n // Transition state to allow WebGL cleanup between screens\n const [isTransitioning, setIsTransitioning] = useState(false);\n const pendingModeRef = useRef<{\n mode: GameMode;\n archetype?: PlayerArchetype;\n } | null>(null);\n\n // Combat players state - managed here so updates persist\n const [combatPlayers, setCombatPlayers] = useState<PlayerState[]>([]);\n\n const audio = useAudio();\n\n // Add responsive screen size detection with proper device detection\n // Uses user-agent detection first for high-res mobile devices\n const [screenSize, setScreenSize] = useState(() => {\n const platform = detectPlatform();\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n };\n });\n\n useEffect(() => {\n // Define handlers outside async function for proper cleanup\n const handleGlobalError = (e: ErrorEvent) => {\n console.error(\"Global error:\", e.error);\n };\n\n const handleUnhandledRejection = (e: PromiseRejectionEvent) => {\n console.error(\"Unhandled promise rejection:\", e.reason);\n if (\n e.reason?.message?.includes(\"Failed to load\") ||\n e.reason?.message?.includes(\"no supported source\")\n ) {\n e.preventDefault();\n }\n };\n\n const initializeApp = async () => {\n try {\n window.focus();\n\n window.addEventListener(\"error\", handleGlobalError);\n window.addEventListener(\"unhandledrejection\", handleUnhandledRejection);\n\n // PHASE 2: Performance optimization initialization\n console.log(\"🔧 Initializing animation performance optimizations...\");\n\n // 1. Prewarm object pools for animation optimization\n // This eliminates GC pressure from ~1,344 allocations per frame\n const { ThreeObjectPools } = await import(\"./utils/threeObjectPool\");\n ThreeObjectPools.prewarmAll();\n const poolStatus = ThreeObjectPools.getStatus();\n console.log(\" ✓ Object pools prewarmed:\", poolStatus);\n\n // 2. Precompute all animations for 90%+ cache hit rate\n const { precomputeAnimation } =\n await import(\"./systems/animation/core/AnimationOptimizations\");\n const { ALL_ANIMATIONS } =\n await import(\"./systems/animation/core/AnimationRegistry\");\n\n let precomputedCount = 0;\n ALL_ANIMATIONS.forEach((animation) => {\n // Precompute at 60fps for smooth playback\n // Use animation.name as the unique identifier\n precomputeAnimation(animation.name, animation, 60);\n precomputedCount++;\n });\n console.log(` ✓ Precomputed ${precomputedCount} animations at 60fps`);\n\n console.log(\n \"✅ Animation optimizations ready (expect <5ms frame time, 90%+ cache hit)\",\n );\n\n setAppReady(true);\n console.log(\"🎯 Black Trigram app initialized\");\n } catch (error) {\n console.error(\"Failed to initialize app:\", error);\n setAppReady(true);\n }\n };\n\n initializeApp();\n\n // Cleanup global event handlers to prevent memory leaks\n return () => {\n window.removeEventListener(\"error\", handleGlobalError);\n window.removeEventListener(\n \"unhandledrejection\",\n handleUnhandledRejection,\n );\n };\n }, []);\n\n // Shared audio initialization logic for splash and retry\n const initializeAudioWithRetry = useCallback(async () => {\n if (!appReady) {\n console.warn(\"App not ready yet, please wait...\");\n return false;\n }\n try {\n await audio.initializeAudio();\n console.log(\"🎵 Audio initialized\");\n return true;\n } catch (error) {\n console.error(\"Failed to initialize audio:\", error);\n return false;\n }\n }, [audio, appReady]);\n\n // Handle splash screen start - initialize audio on user gesture\n const handleSplashStart = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorRetry = useCallback(async () => {\n setShowAudioError(false);\n const success = await initializeAudioWithRetry();\n if (success) {\n setShowSplash(false);\n } else {\n setShowAudioError(true);\n }\n }, [initializeAudioWithRetry]);\n\n const handleAudioErrorContinue = useCallback(() => {\n // Continue without sound\n setShowAudioError(false);\n setShowSplash(false);\n console.log(\"Continuing without audio (silent mode)\");\n }, []);\n\n // ✅ SIMPLIFIED: Handle game mode selection directly\n const handleGameStart = useCallback(\n (mode: GameMode, archetype?: PlayerArchetype) => {\n console.log(\"🎮 Starting game mode:\", mode, \"with archetype:\", archetype);\n\n // Store pending mode and start transition to allow WebGL cleanup\n pendingModeRef.current = { mode, archetype };\n setIsTransitioning(true);\n\n // Clear current mode first (unmounts Canvas)\n setGameMode(null);\n setIsGameActive(false);\n\n // After brief delay, mount new screen\n // Increased delay to allow proper WebGL context cleanup\n setTimeout(() => {\n const pending = pendingModeRef.current;\n if (!pending) return;\n\n // ✅ NEW: Handle controls and philosophy as separate modes\n if (\n pending.mode === GameMode.CONTROLS ||\n pending.mode === GameMode.PHILOSOPHY\n ) {\n setGameMode(pending.mode);\n setIsGameActive(false); // These are not game modes, just screens\n } else {\n setGameMode(pending.mode);\n setIsGameActive(true);\n }\n\n setGameWinner(null);\n setMatchStats(null);\n if (pending.archetype) {\n setSelectedArchetype(pending.archetype);\n }\n\n setIsTransitioning(false);\n pendingModeRef.current = null;\n }, SCREEN_TRANSITION_DELAY_MS); // Delay for WebGL cleanup\n },\n [],\n );\n\n const handleGameEnd = useCallback(\n (winner: number) => {\n setIsGameActive(false);\n setGameWinner(createPlayerFromArchetype(selectedArchetype, winner));\n // Reset combat players for next match\n setCombatPlayers([]);\n\n setMatchStats({\n totalDamageDealt: 150,\n totalDamageTaken: 100,\n criticalHits: 3,\n vitalPointHits: 2,\n techniquesUsed: 8,\n perfectStrikes: 1,\n consecutiveWins: 1,\n matchDuration: 120,\n totalMatches: 1,\n maxRounds: 3,\n winner: winner,\n totalRounds: 2,\n currentRound: 2,\n timeRemaining: 0,\n combatEvents: [],\n finalScore: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n roundsWon: {\n player1: winner === 0 ? 2 : 0,\n player2: winner === 1 ? 2 : 0,\n },\n player1: {\n wins: winner === 0 ? 1 : 0,\n losses: winner === 0 ? 0 : 1,\n hitsTaken: 5,\n hitsLanded: 8,\n totalDamageDealt: winner === 0 ? 150 : 100,\n totalDamageReceived: winner === 0 ? 100 : 150,\n techniques: [\"천둥벽력\", \"유수연타\"],\n perfectStrikes: winner === 0 ? 1 : 0,\n vitalPointHits: winner === 0 ? 2 : 1,\n consecutiveWins: winner === 0 ? 1 : 0,\n matchDuration: 120,\n },\n player2: {\n wins: winner === 1 ? 1 : 0,\n losses: winner === 1 ? 0 : 1,\n hitsTaken: 8,\n hitsLanded: 5,\n totalDamageDealt: winner === 1 ? 150 : 100,\n totalDamageReceived: winner === 1 ? 100 : 150,\n techniques: [\"화염지창\", \"벽력일섬\"],\n perfectStrikes: winner === 1 ? 1 : 0,\n vitalPointHits: winner === 1 ? 2 : 1,\n consecutiveWins: winner === 1 ? 1 : 0,\n matchDuration: 120,\n },\n });\n },\n [selectedArchetype],\n );\n\n const handleReturnToMenu = useCallback(() => {\n // Use same transition logic for return to menu\n setIsTransitioning(true);\n setGameMode(null);\n setIsGameActive(false);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize next combat\n setCombatPlayers([]);\n setTimeout(() => setIsTransitioning(false), MENU_TRANSITION_DELAY_MS);\n }, []);\n\n const handleRematch = useCallback(() => {\n // Restart combat with same settings\n if (!gameMode) {\n console.error(\n \"Cannot rematch: gameMode is not set. This should not happen - EndScreen only renders when gameMode is set.\",\n { gameMode, isGameActive, gameWinner, matchStats }\n );\n return;\n }\n \n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n // Reset combat players so they reinitialize for rematch\n setCombatPlayers([]);\n \n setTimeout(() => {\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- isGameActive, gameWinner, matchStats only used in error logging, not function logic\n }, [gameMode]);\n\n const handleViewTraining = useCallback(() => {\n // Navigate to training mode\n setIsTransitioning(true);\n setGameWinner(null);\n setMatchStats(null);\n setCombatPlayers([]);\n \n setTimeout(() => {\n setGameMode(GameMode.TRAINING);\n setIsGameActive(true);\n setIsTransitioning(false);\n }, SCREEN_TRANSITION_DELAY_MS);\n }, []);\n\n const renderCurrentScreen = () => {\n // Show loading during screen transitions\n if (isTransitioning) {\n return (\n <LoadingState\n progress={undefined}\n message=\"전환 중... | Transitioning...\"\n stage=\"assets\"\n />\n );\n }\n\n if (gameWinner && matchStats) {\n // ✅ NEW: Use EndScreen3D component\n return (\n <EndScreen3D\n winner={gameWinner}\n matchStats={matchStats}\n onReturnToMenu={handleReturnToMenu}\n onRematch={handleRematch}\n onViewReplay={handleViewTraining}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ NEW: Handle standalone screens first\n if (gameMode === GameMode.CONTROLS) {\n return (\n <ControlsScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n if (gameMode === GameMode.PHILOSOPHY) {\n return (\n <PhilosophyScreen\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n\n // ✅ SIMPLIFIED: Only active game modes use isGameActive\n if (isGameActive && gameMode) {\n switch (gameMode) {\n case GameMode.TRAINING:\n return (\n <Suspense\n fallback={\n <LoadingState\n progress={undefined}\n message=\"훈련장 로딩 중... | Loading Training...\"\n stage=\"assets\"\n />\n }\n >\n <TrainingScreen\n onPlayerUpdate={(updates) => {\n console.log(\"Training player updated:\", updates);\n }}\n onReturnToMenu={handleReturnToMenu}\n width={screenSize.width}\n height={screenSize.height}\n initialArchetype={selectedArchetype}\n />\n </Suspense>\n );\n case GameMode.VERSUS:\n case GameMode.PRACTICE:\n // Initialize players if not already set\n if (combatPlayers.length === 0) {\n const player1 = createPlayerFromArchetype(selectedArchetype, 0);\n const player2 = createPlayerFromArchetype(\n PlayerArchetype.AMSALJA,\n 1,\n );\n // Use setTimeout to defer state update and avoid render-during-render\n setTimeout(() => setCombatPlayers([player1, player2]), 0);\n // Return loading state while players initialize\n return (\n <LoadingState\n progress={undefined}\n message=\"전투 준비 중... | Preparing Combat...\"\n stage=\"assets\"\n />\n );\n }\n\n return (\n <CombatScreen\n players={combatPlayers}\n currentRound={1}\n timeRemaining={180}\n isPaused={false}\n onPlayerUpdate={(playerIndex, updates) => {\n // Actually update the player state so damage persists!\n setCombatPlayers((prevPlayers) => {\n const newPlayers = [...prevPlayers];\n if (newPlayers[playerIndex]) {\n newPlayers[playerIndex] = {\n ...newPlayers[playerIndex],\n ...updates,\n };\n }\n return newPlayers;\n });\n }}\n onReturnToMenu={handleReturnToMenu}\n onGameEnd={handleGameEnd}\n gameMode={gameMode}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n default:\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n }\n }\n\n // ✅ SIMPLIFIED: Default to intro screen\n return (\n <IntroScreen\n onMenuSelect={handleGameStart}\n onArchetypeSelect={setSelectedArchetype}\n selectedArchetype={selectedArchetype}\n width={screenSize.width}\n height={screenSize.height}\n />\n );\n };\n\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n containerRef.current?.focus();\n }, [appReady]);\n\n useEffect(() => {\n const handleResize = () => {\n // Clear cached platform info to get fresh detection on resize\n clearPlatformCache();\n const platform = detectPlatform();\n setScreenSize({\n width: platform.screenWidth,\n height: platform.screenHeight,\n isMobile: platform.isMobile,\n isTablet: platform.isTablet,\n isDesktop: platform.isDesktop,\n });\n };\n\n window.addEventListener(\"resize\", handleResize);\n return () => window.removeEventListener(\"resize\", handleResize);\n }, []);\n\n // F9 key toggle for performance debug overlay (dev mode only)\n // Note: P key is reserved for Philosophy screen\n useEffect(() => {\n if (!import.meta.env.DEV) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"F9\") {\n e.preventDefault();\n setShowPerformanceDebug((prev) => !prev);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, []);\n\n if (!appReady) {\n return (\n <div className=\"app loading\" data-testid=\"app-container\">\n <LoadingState\n progress={undefined}\n message=\"앱 초기화 중 | Initializing app...\"\n stage=\"initialization\"\n />\n </div>\n );\n }\n\n // Show splash screen first to get user gesture for audio\n if (showSplash) {\n return (\n <div className=\"app\" data-testid=\"app-container\">\n <SplashScreen\n onStart={handleSplashStart}\n width={screenSize.width}\n height={screenSize.height}\n />\n {showAudioError && (\n <ErrorModal\n message=\"오디오 초기화에 실패했습니다. 재시도하거나 소리 없이 계속할 수 있습니다. | Audio initialization failed. You can retry or continue without sound.\"\n onRetry={handleAudioErrorRetry}\n onContinue={handleAudioErrorContinue}\n />\n )}\n </div>\n );\n }\n\n return (\n <div\n className=\"app\"\n tabIndex={0}\n ref={containerRef}\n data-testid=\"app-container\"\n >\n {/* All screens now use Three.js or pure React/HTML */}\n {renderCurrentScreen()}\n\n {/* Performance debug overlay (dev mode only, toggle with P key) */}\n {showPerformanceDebug && <PerformanceDebugOverlayHtml />}\n </div>\n );\n}\n\nexport default App;\n"],"mappings":"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"}
@@ -72,7 +72,7 @@ var AudioManager = class {
72
72
  this.audioPool = new AudioElementPool();
73
73
  this.monitor = new AudioMonitor();
74
74
  const cacheConfig = {
75
- maxSizeBytes: 30 * 1024 * 1024,
75
+ maxSizeBytes: 31457280,
76
76
  criticalAssets: [...CRITICAL_AUDIO_ASSETS],
77
77
  debug: typeof process !== "undefined" && process.env.NODE_ENV === "development"
78
78
  };
@@ -151,7 +151,7 @@ var AudioManager = class {
151
151
  const estimatedSize = this.estimateAudioSize(asset);
152
152
  this.audioCache.set(asset.id, asset, estimatedSize);
153
153
  const loadTime = performance.now() - startTime;
154
- this.monitor.recordLoad(asset.id, loadTime, estimatedSize / (1024 * 1024));
154
+ this.monitor.recordLoad(asset.id, loadTime, estimatedSize / 1048576);
155
155
  if (this.frequentSounds.has(asset.id)) this.audioPool.createPool(asset.id, asset.url, {
156
156
  initialSize: 3,
157
157
  maxSize: 10,
@@ -327,8 +327,6 @@ var AudioManager = class {
327
327
  case "music":
328
328
  this._musicVolume = clampedVolume;
329
329
  if (this.currentMusic) this.currentMusic.volume = this._musicVolume * this._masterVolume;
330
- break;
331
- case "voice": break;
332
330
  }
333
331
  }
334
332
  mute() {
@@ -1 +1 @@
1
- {"version":3,"file":"AudioManager.js","names":[],"sources":["../../src/audio/AudioManager.ts"],"sourcesContent":["import { AudioAssetLoader, LoadOptions } from \"./AudioAssetLoader\";\nimport { AudioCache, AudioCacheConfig } from \"./AudioCache\";\nimport {\n AudioFPSImpact,\n AudioMemoryStats,\n AudioMonitor,\n AudioPerformanceStats,\n MemoryWarning,\n} from \"./AudioMonitor\";\nimport { AudioElementPool, PoolStatistics } from \"./AudioPool\";\nimport {\n AudioAsset,\n AudioConfig,\n IAudioManager,\n MusicTrackId,\n SoundEffectId,\n} from \"./types\";\n\n// Estimated average size per audio asset in MB (based on typical compressed audio file sizes)\nconst ESTIMATED_ASSET_SIZE_MB = 0.5;\n\n/**\n * Critical assets that should never be evicted from cache\n * 캐시에서 절대 제거되지 않아야 하는 중요한 자산\n */\nconst CRITICAL_AUDIO_ASSETS = [\n // Menu sounds - instant playback required\n \"menu_hover\",\n \"menu_select\",\n \"menu_click\",\n \"menu_navigate\",\n \"menu_back\",\n // Common combat sounds - instant playback required\n \"hit_impact\",\n \"hit_light\",\n \"hit_medium\",\n \"hit_heavy\",\n \"guard_block\",\n \"attack_whoosh\",\n \"attack_light\",\n \"stance_change\",\n] as const;\n\n/**\n * Manages audio playback for the Black Trigram combat game.\n *\n * Handles music tracks, sound effects, and Korean martial arts audio\n * including technique sounds, trigram stance audio, and vital point hit\n * feedback. Uses an LRU asset cache, pooled `HTMLAudioElement` instances,\n * and an `AudioMonitor` for performance tracking.\n *\n * @example\n * ```ts\n * const audio = new AudioManager({ masterVolume: 0.8 });\n * await audio.initialize();\n * await audio.playMusic(\"dojang_theme\");\n * await audio.playSFX(\"hit_medium\");\n * ```\n *\n * @korean 오디오관리자\n */\nexport class AudioManager implements IAudioManager {\n private _masterVolume: number = 1.0;\n private _musicVolume: number = 0.7;\n private _sfxVolume: number = 0.8;\n private _muted: boolean = false;\n private _currentMusicTrack: string | null = null;\n private _fallbackMode: boolean = false;\n private currentMusic: HTMLAudioElement | null = null;\n private soundCache: Map<string, HTMLAudioElement> = new Map();\n private _isInitialized: boolean = false;\n\n /** Music assets currently being loaded — prevents race conditions on concurrent playMusic calls */\n private loadingMusic: Set<string> = new Set();\n\n private assetLoader: AudioAssetLoader;\n private audioPool: AudioElementPool;\n private monitor: AudioMonitor;\n private audioCache: AudioCache;\n private frequentSounds: Set<string> = new Set([\n \"hit_light\",\n \"attack_light\",\n \"stance_change\",\n ]);\n\n constructor(config?: Partial<AudioConfig>) {\n if (config) {\n this._masterVolume = config.masterVolume ?? 1.0;\n this._musicVolume = config.musicVolume ?? 0.7;\n this._sfxVolume = config.sfxVolume ?? 0.8;\n }\n\n this.assetLoader = new AudioAssetLoader();\n this.audioPool = new AudioElementPool();\n this.monitor = new AudioMonitor();\n\n // Initialize AudioCache with 30MB limit and critical assets\n // 30MB 제한 및 중요 자산으로 AudioCache 초기화\n const cacheConfig: AudioCacheConfig = {\n maxSizeBytes: 30 * 1024 * 1024, // 30MB default\n criticalAssets: [...CRITICAL_AUDIO_ASSETS],\n debug: typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"development\",\n };\n this.audioCache = new AudioCache(cacheConfig);\n }\n\n /**\n * Helper method to release pooled audio back to the pool after playback\n * @param id - Asset ID\n * @param audio - Audio element to release\n */\n private releasePooledAudio(id: string, audio: HTMLAudioElement): void {\n if (!audio.paused) {\n audio.addEventListener(\n \"ended\",\n () => {\n this.audioPool.release(id, audio);\n },\n { once: true }\n );\n } else {\n this.audioPool.release(id, audio);\n }\n }\n\n /** @returns Whether the audio system has been initialized */\n get isInitialized(): boolean {\n return this._isInitialized;\n }\n\n get fallbackMode(): boolean {\n return this._fallbackMode;\n }\n\n get currentMusicTrack(): string | null {\n return this._currentMusicTrack;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get sfxVolume(): number {\n return this._sfxVolume;\n }\n\n get musicVolume(): number {\n return this._musicVolume;\n }\n\n get muted(): boolean {\n return this._muted;\n }\n\n async initialize(config?: AudioConfig): Promise<void> {\n try {\n // Create AudioContext using globalThis for cross-platform compatibility\n const AudioContextClass =\n globalThis.AudioContext ||\n (globalThis as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext;\n new AudioContextClass();\n this._isInitialized = true;\n this._fallbackMode = false;\n\n if (config) {\n this._masterVolume = config.masterVolume ?? this._masterVolume;\n this._musicVolume = config.musicVolume ?? this._musicVolume;\n this._sfxVolume = config.sfxVolume ?? this._sfxVolume;\n }\n\n // Set memory threshold if configured\n if (config?.maxSimultaneousSounds) {\n const estimatedMemoryMB =\n config.maxSimultaneousSounds * ESTIMATED_ASSET_SIZE_MB;\n this.monitor.setMemoryThreshold(estimatedMemoryMB);\n }\n } catch (error) {\n console.warn(\n \"AudioContext initialization failed, using fallback mode:\",\n error\n );\n this._isInitialized = true;\n this._fallbackMode = true;\n }\n }\n\n async loadAsset(asset: AudioAsset, options?: LoadOptions): Promise<void> {\n const startTime = performance.now();\n\n try {\n // Check cache first - instant playback for cached assets\n // 캐시 먼저 확인 - 캐시된 자산의 즉시 재생\n const cachedAsset = this.audioCache.get(asset.id);\n if (cachedAsset) {\n // Asset already loaded in cache, use it\n const cached = this.soundCache.get(asset.id);\n if (cached) {\n return; // Already loaded and cached\n }\n }\n\n // Use faster timeouts for testing environment\n // Safely check for Node.js environment to avoid runtime errors in browsers\n const isTest =\n typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"test\";\n const loadOptions: LoadOptions = {\n timeout: isTest ? 100 : 10000,\n maxRetries: isTest ? 1 : 3,\n retryDelay: isTest ? 10 : 1000,\n ...options,\n };\n\n // Use AudioAssetLoader with retry and fallback\n const result = await this.assetLoader.loadAsset(asset, loadOptions);\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Estimate size for cache management\n // 캐시 관리를 위한 크기 추정\n const estimatedSize = this.estimateAudioSize(asset);\n\n // Add to LRU cache with size tracking\n // 크기 추적과 함께 LRU 캐시에 추가\n this.audioCache.set(asset.id, asset, estimatedSize);\n\n // Track performance and memory\n const loadTime = performance.now() - startTime;\n this.monitor.recordLoad(asset.id, loadTime, estimatedSize / (1024 * 1024));\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n } else {\n // Record failure\n const error = result.error ?? new Error(\"Unknown load error\");\n this.monitor.recordLoadFailure(asset.id, error);\n\n // Still cache the placeholder audio if available\n if (result.audio) {\n this.soundCache.set(asset.id, result.audio);\n\n // Add to cache even if failed (placeholder)\n const estimatedSize = this.estimateAudioSize(asset);\n this.audioCache.set(asset.id, asset, estimatedSize);\n }\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n console.warn(`Failed to load audio asset ${asset.id}:`, err);\n this.monitor.recordLoadFailure(asset.id, err);\n }\n }\n\n /**\n * Estimate audio asset size in bytes\n * 바이트 단위의 오디오 자산 크기 추정\n *\n * @returns Estimated size in bytes\n */\n private estimateAudioSize(_asset: AudioAsset): number {\n // Use ESTIMATED_ASSET_SIZE_MB constant for consistent estimation\n // Conservative estimate: ~0.5MB per audio file (compressed MP3/WebM)\n return ESTIMATED_ASSET_SIZE_MB * 1024 * 1024;\n }\n\n async playSoundEffect(id: SoundEffectId): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n /**\n * Alias for {@link playSoundEffect} — preferred for explicit volume control.\n * @param id - Sound effect identifier\n * @param volume - Override volume (defaults to sfxVolume × masterVolume)\n */\n async playSFX(id: SoundEffectId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n async playMusic(id: MusicTrackId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n this.stopMusic();\n\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n let audio = this.soundCache.get(id);\n \n // On-demand loading: If music not in cache, load it first\n // 온디맨드 로딩: 음악이 캐시에 없으면 먼저 로드\n if (!audio) {\n // Check if this music is already being loaded to prevent race conditions\n // JavaScript's single-threaded nature ensures atomic check-and-add operations\n // 레이스 컨디션 방지를 위해 이미 로드 중인지 확인\n // JavaScript의 싱글 스레드 특성으로 원자적 check-and-add 작업 보장\n if (this.loadingMusic.has(id)) {\n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" is already being loaded, waiting...`);\n }\n // Wait for the ongoing load to complete\n // 진행 중인 로드가 완료될 때까지 대기\n await this.waitForMusicLoad(id);\n audio = this.soundCache.get(id);\n } else {\n // Mark as loading before starting the load (atomic operation in single-threaded JS)\n // 로드 시작 전에 로딩 중으로 표시 (싱글 스레드 JS에서 원자적 작업)\n this.loadingMusic.add(id);\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" not in cache, loading on-demand...`);\n }\n \n try {\n const { audioAssetRegistry } = await import(\"./AudioAssetRegistry\");\n const musicAsset = audioAssetRegistry.getMusic(id);\n \n if (musicAsset) {\n try {\n await this.loadAsset(musicAsset);\n audio = this.soundCache.get(id);\n \n if (import.meta.env.DEV && audio) {\n console.log(`[AudioManager] Successfully loaded music \"${id}\" on-demand`);\n }\n } catch (error) {\n console.warn(`Failed to load music asset ${id} on-demand:`, error);\n return;\n }\n } else {\n console.warn(`Music asset not found in registry: ${id}`);\n return;\n }\n } finally {\n // Always remove from loading set when done\n // 완료 시 항상 로딩 세트에서 제거\n this.loadingMusic.delete(id);\n }\n }\n }\n\n if (audio) {\n try {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._musicVolume) * this._masterVolume;\n audio.loop = true;\n this.currentMusic = audio;\n this._currentMusicTrack = id;\n await audio.play();\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Playing music: ${id} (volume: ${audio.volume.toFixed(2)})`);\n }\n } catch (error) {\n console.warn(`Failed to play music ${id}:`, error);\n }\n }\n }\n\n /**\n * Wait for a music asset to finish loading\n * 음악 자산 로드가 완료될 때까지 대기\n * @param id - Music track ID\n * @param maxWaitMs - Maximum time to wait in milliseconds\n */\n private async waitForMusicLoad(id: string, maxWaitMs: number = 5000): Promise<void> {\n const startTime = Date.now();\n const checkInterval = 100; // Check every 100ms\n \n while (this.loadingMusic.has(id)) {\n if (Date.now() - startTime > maxWaitMs) {\n console.warn(`[AudioManager] Timeout waiting for music \"${id}\" to load`);\n return;\n }\n await new Promise(resolve => setTimeout(resolve, checkInterval));\n }\n }\n\n stopMusic(): void {\n if (this.currentMusic) {\n this.currentMusic.pause();\n this.currentMusic.currentTime = 0;\n this.currentMusic = null;\n this._currentMusicTrack = null;\n }\n }\n\n stopAll(): void {\n this.stopMusic();\n this.soundCache.forEach((audio) => {\n if (!audio.paused) {\n audio.pause();\n audio.currentTime = 0;\n }\n });\n }\n\n setVolume(type: \"master\" | \"sfx\" | \"music\" | \"voice\", volume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, volume));\n\n switch (type) {\n case \"master\":\n this._masterVolume = clampedVolume;\n break;\n case \"sfx\":\n this._sfxVolume = clampedVolume;\n break;\n case \"music\":\n this._musicVolume = clampedVolume;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n break;\n case \"voice\":\n break;\n }\n }\n\n mute(): void {\n this._muted = true;\n if (this.currentMusic) {\n this.currentMusic.volume = 0;\n }\n }\n\n unmute(): void {\n this._muted = false;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n }\n\n async fadeOut(duration: number = 1000): Promise<void> {\n if (!this.currentMusic) return;\n\n const musicElement = this.currentMusic;\n return new Promise((resolve) => {\n const startVolume = musicElement.volume;\n const fadeStep = startVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume > 0) {\n this.currentMusic.volume = Math.max(\n 0,\n this.currentMusic.volume - fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n this.stopMusic();\n resolve();\n }\n }, 50);\n });\n }\n\n async fadeIn(trackId: MusicTrackId, duration: number = 1000): Promise<void> {\n await this.playMusic(trackId, 0);\n\n if (!this.currentMusic) return;\n\n return new Promise((resolve) => {\n const targetVolume = this._musicVolume * this._masterVolume;\n const fadeStep = targetVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume < targetVolume) {\n this.currentMusic.volume = Math.min(\n targetVolume,\n this.currentMusic.volume + fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n resolve();\n }\n }, 50);\n });\n }\n\n async crossfade(\n fromTrackId: MusicTrackId,\n toTrackId: MusicTrackId,\n duration: number = 1000\n ): Promise<void> {\n const fadeOutPromise = this.fadeOut(duration);\n await fadeOutPromise;\n await this.fadeIn(toTrackId, duration);\n console.log(`Crossfaded from ${fromTrackId} to ${toTrackId}`);\n }\n\n /** @returns A snapshot of all currently loaded sound elements */\n getLoadedAssets(): ReadonlyMap<string, HTMLAudioElement> {\n return new Map(this.soundCache);\n }\n\n async playVoice(id: string): Promise<void> {\n return this.playSoundEffect(id);\n }\n\n async playKoreanTechniqueSound(\n techniqueId: string,\n archetype: string\n ): Promise<void> {\n const soundId = `${archetype}_${techniqueId}`;\n return this.playSoundEffect(soundId);\n }\n\n async playTrigramStanceSound(stance: string): Promise<void> {\n const soundId = `stance_${stance}`;\n return this.playSoundEffect(soundId);\n }\n\n async playVitalPointHitSound(severity: string): Promise<void> {\n const soundId = `vital_point_${severity}`;\n return this.playSoundEffect(soundId);\n }\n\n async playDojiangAmbience(): Promise<void> {\n return this.playMusic(\"dojang_ambience\");\n }\n\n // Legacy getters for backward compatibility\n getMasterVolume(): number {\n return this._masterVolume;\n }\n\n getMusicVolume(): number {\n return this._musicVolume;\n }\n\n getSfxVolume(): number {\n return this._sfxVolume;\n }\n\n get initialized(): boolean {\n return this._isInitialized;\n }\n\n /**\n * Batch load multiple assets with progress tracking\n * @param assets - Array of audio assets to load\n * @param options - Optional load configuration (timeout, retries, etc.)\n * @param onProgress - Optional callback for progress updates with loaded and total counts\n * @returns Promise that resolves when all assets are processed\n */\n async batchLoadAssets(\n assets: readonly AudioAsset[],\n options?: LoadOptions,\n onProgress?: (loaded: number, total: number) => void\n ): Promise<void> {\n const results = await this.assetLoader.batchLoad(\n assets,\n options,\n (progress) => {\n onProgress?.(progress.loaded, progress.total);\n }\n );\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const asset = assets[i];\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n }\n }\n }\n\n /**\n * Unload an asset to free memory\n * @param assetId - ID of the asset to unload\n * @returns true if asset was unloaded, false if asset was not found\n */\n unloadAsset(assetId: string): boolean {\n const audio = this.soundCache.get(assetId);\n if (audio) {\n audio.pause();\n audio.src = \"\";\n this.soundCache.delete(assetId);\n }\n\n // LRU 캐시에서 제거\n const removedFromCache = this.audioCache.remove(assetId);\n\n const unloaded = this.assetLoader.unloadAsset(assetId);\n\n if (this.audioPool.hasPool(assetId)) {\n this.audioPool.removePool(assetId);\n }\n\n this.monitor.unregisterAsset(assetId);\n\n return unloaded || audio !== undefined || removedFromCache;\n }\n\n /**\n * Get cache statistics including LRU cache metrics\n * LRU 캐시 메트릭을 포함한 캐시 통계 가져오기\n *\n * @returns Cache statistics\n */\n getCacheStats(): {\n readonly lruCache: {\n readonly totalSize: number;\n readonly assetCount: number;\n readonly criticalCount: number;\n readonly utilizationPercent: number;\n readonly evictionCount: number;\n readonly hitCount: number;\n readonly missCount: number;\n readonly hitRate: number;\n };\n readonly soundCache: number;\n readonly poolStats: Map<string, PoolStatistics>;\n } {\n return {\n lruCache: this.audioCache.getStats(),\n soundCache: this.soundCache.size,\n poolStats: this.audioPool.getAllStatistics(),\n };\n }\n\n /**\n * Get memory statistics including total loaded MB, asset count, and warnings\n * @returns Memory statistics object\n */\n getMemoryStats(): AudioMemoryStats {\n return this.monitor.getMemoryStats();\n }\n\n /**\n * Get performance statistics including load times and playback latency\n * @returns Performance statistics object\n */\n getPerformanceStats(): AudioPerformanceStats {\n return this.monitor.getPerformanceStats();\n }\n\n /**\n * Get FPS impact analysis showing baseline vs current FPS and detected drops\n * @returns FPS impact analysis object\n */\n getFPSImpact(): AudioFPSImpact {\n return this.monitor.getFPSImpact();\n }\n\n /**\n * Update FPS measurement for monitoring\n * @param fps - Current frames per second measurement\n */\n updateFPS(fps: number): void {\n this.monitor.updateFPS(fps);\n }\n\n /**\n * Get comprehensive monitoring report including memory, performance, FPS, and warnings\n * @returns Comprehensive monitoring report object\n */\n getMonitoringReport(): {\n readonly memory: AudioMemoryStats;\n readonly performance: AudioPerformanceStats;\n readonly fps: AudioFPSImpact;\n readonly warnings: readonly MemoryWarning[];\n } {\n return this.monitor.getReport();\n }\n\n /**\n * Get all memory warnings\n * @returns Array of memory warnings\n */\n getMemoryWarnings(): readonly MemoryWarning[] {\n return this.monitor.getWarnings();\n }\n\n /**\n * Clear memory warnings\n */\n clearMemoryWarnings(): void {\n this.monitor.clearWarnings();\n }\n\n /**\n * Get pool statistics for a specific asset or all pools\n * @param assetId - Optional asset ID to get specific pool stats\n * @returns Pool statistics for the specified asset or all pools\n */\n getPoolStatistics(\n assetId?: string\n ): PoolStatistics | Map<string, PoolStatistics> | undefined {\n if (assetId) {\n return this.audioPool.getPoolStatistics(assetId);\n }\n return this.audioPool.getAllStatistics();\n }\n\n /**\n * Get loader statistics including cached assets and loading state\n * @returns Loader statistics object\n */\n getLoaderStatistics(): {\n readonly cached: number;\n readonly loading: number;\n readonly totalAttempts: number;\n } {\n return this.assetLoader.getStatistics();\n }\n}\n\nexport default AudioManager;\n"],"mappings":"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"}
1
+ {"version":3,"file":"AudioManager.js","names":[],"sources":["../../src/audio/AudioManager.ts"],"sourcesContent":["import { AudioAssetLoader, LoadOptions } from \"./AudioAssetLoader\";\nimport { AudioCache, AudioCacheConfig } from \"./AudioCache\";\nimport {\n AudioFPSImpact,\n AudioMemoryStats,\n AudioMonitor,\n AudioPerformanceStats,\n MemoryWarning,\n} from \"./AudioMonitor\";\nimport { AudioElementPool, PoolStatistics } from \"./AudioPool\";\nimport {\n AudioAsset,\n AudioConfig,\n IAudioManager,\n MusicTrackId,\n SoundEffectId,\n} from \"./types\";\n\n// Estimated average size per audio asset in MB (based on typical compressed audio file sizes)\nconst ESTIMATED_ASSET_SIZE_MB = 0.5;\n\n/**\n * Critical assets that should never be evicted from cache\n * 캐시에서 절대 제거되지 않아야 하는 중요한 자산\n */\nconst CRITICAL_AUDIO_ASSETS = [\n // Menu sounds - instant playback required\n \"menu_hover\",\n \"menu_select\",\n \"menu_click\",\n \"menu_navigate\",\n \"menu_back\",\n // Common combat sounds - instant playback required\n \"hit_impact\",\n \"hit_light\",\n \"hit_medium\",\n \"hit_heavy\",\n \"guard_block\",\n \"attack_whoosh\",\n \"attack_light\",\n \"stance_change\",\n] as const;\n\n/**\n * Manages audio playback for the Black Trigram combat game.\n *\n * Handles music tracks, sound effects, and Korean martial arts audio\n * including technique sounds, trigram stance audio, and vital point hit\n * feedback. Uses an LRU asset cache, pooled `HTMLAudioElement` instances,\n * and an `AudioMonitor` for performance tracking.\n *\n * @example\n * ```ts\n * const audio = new AudioManager({ masterVolume: 0.8 });\n * await audio.initialize();\n * await audio.playMusic(\"dojang_theme\");\n * await audio.playSFX(\"hit_medium\");\n * ```\n *\n * @korean 오디오관리자\n */\nexport class AudioManager implements IAudioManager {\n private _masterVolume: number = 1.0;\n private _musicVolume: number = 0.7;\n private _sfxVolume: number = 0.8;\n private _muted: boolean = false;\n private _currentMusicTrack: string | null = null;\n private _fallbackMode: boolean = false;\n private currentMusic: HTMLAudioElement | null = null;\n private soundCache: Map<string, HTMLAudioElement> = new Map();\n private _isInitialized: boolean = false;\n\n /** Music assets currently being loaded — prevents race conditions on concurrent playMusic calls */\n private loadingMusic: Set<string> = new Set();\n\n private assetLoader: AudioAssetLoader;\n private audioPool: AudioElementPool;\n private monitor: AudioMonitor;\n private audioCache: AudioCache;\n private frequentSounds: Set<string> = new Set([\n \"hit_light\",\n \"attack_light\",\n \"stance_change\",\n ]);\n\n constructor(config?: Partial<AudioConfig>) {\n if (config) {\n this._masterVolume = config.masterVolume ?? 1.0;\n this._musicVolume = config.musicVolume ?? 0.7;\n this._sfxVolume = config.sfxVolume ?? 0.8;\n }\n\n this.assetLoader = new AudioAssetLoader();\n this.audioPool = new AudioElementPool();\n this.monitor = new AudioMonitor();\n\n // Initialize AudioCache with 30MB limit and critical assets\n // 30MB 제한 및 중요 자산으로 AudioCache 초기화\n const cacheConfig: AudioCacheConfig = {\n maxSizeBytes: 30 * 1024 * 1024, // 30MB default\n criticalAssets: [...CRITICAL_AUDIO_ASSETS],\n debug: typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"development\",\n };\n this.audioCache = new AudioCache(cacheConfig);\n }\n\n /**\n * Helper method to release pooled audio back to the pool after playback\n * @param id - Asset ID\n * @param audio - Audio element to release\n */\n private releasePooledAudio(id: string, audio: HTMLAudioElement): void {\n if (!audio.paused) {\n audio.addEventListener(\n \"ended\",\n () => {\n this.audioPool.release(id, audio);\n },\n { once: true }\n );\n } else {\n this.audioPool.release(id, audio);\n }\n }\n\n /** @returns Whether the audio system has been initialized */\n get isInitialized(): boolean {\n return this._isInitialized;\n }\n\n get fallbackMode(): boolean {\n return this._fallbackMode;\n }\n\n get currentMusicTrack(): string | null {\n return this._currentMusicTrack;\n }\n\n get masterVolume(): number {\n return this._masterVolume;\n }\n\n get sfxVolume(): number {\n return this._sfxVolume;\n }\n\n get musicVolume(): number {\n return this._musicVolume;\n }\n\n get muted(): boolean {\n return this._muted;\n }\n\n async initialize(config?: AudioConfig): Promise<void> {\n try {\n // Create AudioContext using globalThis for cross-platform compatibility\n const AudioContextClass =\n globalThis.AudioContext ||\n (globalThis as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext;\n new AudioContextClass();\n this._isInitialized = true;\n this._fallbackMode = false;\n\n if (config) {\n this._masterVolume = config.masterVolume ?? this._masterVolume;\n this._musicVolume = config.musicVolume ?? this._musicVolume;\n this._sfxVolume = config.sfxVolume ?? this._sfxVolume;\n }\n\n // Set memory threshold if configured\n if (config?.maxSimultaneousSounds) {\n const estimatedMemoryMB =\n config.maxSimultaneousSounds * ESTIMATED_ASSET_SIZE_MB;\n this.monitor.setMemoryThreshold(estimatedMemoryMB);\n }\n } catch (error) {\n console.warn(\n \"AudioContext initialization failed, using fallback mode:\",\n error\n );\n this._isInitialized = true;\n this._fallbackMode = true;\n }\n }\n\n async loadAsset(asset: AudioAsset, options?: LoadOptions): Promise<void> {\n const startTime = performance.now();\n\n try {\n // Check cache first - instant playback for cached assets\n // 캐시 먼저 확인 - 캐시된 자산의 즉시 재생\n const cachedAsset = this.audioCache.get(asset.id);\n if (cachedAsset) {\n // Asset already loaded in cache, use it\n const cached = this.soundCache.get(asset.id);\n if (cached) {\n return; // Already loaded and cached\n }\n }\n\n // Use faster timeouts for testing environment\n // Safely check for Node.js environment to avoid runtime errors in browsers\n const isTest =\n typeof process !== \"undefined\" && process?.env?.NODE_ENV === \"test\";\n const loadOptions: LoadOptions = {\n timeout: isTest ? 100 : 10000,\n maxRetries: isTest ? 1 : 3,\n retryDelay: isTest ? 10 : 1000,\n ...options,\n };\n\n // Use AudioAssetLoader with retry and fallback\n const result = await this.assetLoader.loadAsset(asset, loadOptions);\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Estimate size for cache management\n // 캐시 관리를 위한 크기 추정\n const estimatedSize = this.estimateAudioSize(asset);\n\n // Add to LRU cache with size tracking\n // 크기 추적과 함께 LRU 캐시에 추가\n this.audioCache.set(asset.id, asset, estimatedSize);\n\n // Track performance and memory\n const loadTime = performance.now() - startTime;\n this.monitor.recordLoad(asset.id, loadTime, estimatedSize / (1024 * 1024));\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n } else {\n // Record failure\n const error = result.error ?? new Error(\"Unknown load error\");\n this.monitor.recordLoadFailure(asset.id, error);\n\n // Still cache the placeholder audio if available\n if (result.audio) {\n this.soundCache.set(asset.id, result.audio);\n\n // Add to cache even if failed (placeholder)\n const estimatedSize = this.estimateAudioSize(asset);\n this.audioCache.set(asset.id, asset, estimatedSize);\n }\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n console.warn(`Failed to load audio asset ${asset.id}:`, err);\n this.monitor.recordLoadFailure(asset.id, err);\n }\n }\n\n /**\n * Estimate audio asset size in bytes\n * 바이트 단위의 오디오 자산 크기 추정\n *\n * @returns Estimated size in bytes\n */\n private estimateAudioSize(_asset: AudioAsset): number {\n // Use ESTIMATED_ASSET_SIZE_MB constant for consistent estimation\n // Conservative estimate: ~0.5MB per audio file (compressed MP3/WebM)\n return ESTIMATED_ASSET_SIZE_MB * 1024 * 1024;\n }\n\n async playSoundEffect(id: SoundEffectId): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = this._sfxVolume * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n /**\n * Alias for {@link playSoundEffect} — preferred for explicit volume control.\n * @param id - Sound effect identifier\n * @param volume - Override volume (defaults to sfxVolume × masterVolume)\n */\n async playSFX(id: SoundEffectId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n const playbackStart = performance.now();\n\n try {\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n // Use pool for frequently played sounds\n if (this.frequentSounds.has(id) && this.audioPool.hasPool(id)) {\n const audio = this.audioPool.acquire(id);\n if (audio) {\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n // Release back to pool after playback (non-blocking)\n this.releasePooledAudio(id, audio);\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n return;\n }\n }\n\n // Fallback to cached audio\n const audio = this.soundCache.get(id);\n if (audio) {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._sfxVolume) * this._masterVolume;\n await audio.play();\n\n const latency = performance.now() - playbackStart;\n this.monitor.recordPlaybackLatency(latency);\n }\n } catch (error) {\n console.warn(`Failed to play sound effect ${id}:`, error);\n }\n }\n\n async playMusic(id: MusicTrackId, volume?: number): Promise<void> {\n if (this._muted) return;\n\n this.stopMusic();\n\n // Update cache access time for LRU tracking\n // LRU 추적을 위한 캐시 액세스 시간 업데이트\n this.audioCache.get(id);\n\n let audio = this.soundCache.get(id);\n \n // On-demand loading: If music not in cache, load it first\n // 온디맨드 로딩: 음악이 캐시에 없으면 먼저 로드\n if (!audio) {\n // Check if this music is already being loaded to prevent race conditions\n // JavaScript's single-threaded nature ensures atomic check-and-add operations\n // 레이스 컨디션 방지를 위해 이미 로드 중인지 확인\n // JavaScript의 싱글 스레드 특성으로 원자적 check-and-add 작업 보장\n if (this.loadingMusic.has(id)) {\n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" is already being loaded, waiting...`);\n }\n // Wait for the ongoing load to complete\n // 진행 중인 로드가 완료될 때까지 대기\n await this.waitForMusicLoad(id);\n audio = this.soundCache.get(id);\n } else {\n // Mark as loading before starting the load (atomic operation in single-threaded JS)\n // 로드 시작 전에 로딩 중으로 표시 (싱글 스레드 JS에서 원자적 작업)\n this.loadingMusic.add(id);\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Music \"${id}\" not in cache, loading on-demand...`);\n }\n \n try {\n const { audioAssetRegistry } = await import(\"./AudioAssetRegistry\");\n const musicAsset = audioAssetRegistry.getMusic(id);\n \n if (musicAsset) {\n try {\n await this.loadAsset(musicAsset);\n audio = this.soundCache.get(id);\n \n if (import.meta.env.DEV && audio) {\n console.log(`[AudioManager] Successfully loaded music \"${id}\" on-demand`);\n }\n } catch (error) {\n console.warn(`Failed to load music asset ${id} on-demand:`, error);\n return;\n }\n } else {\n console.warn(`Music asset not found in registry: ${id}`);\n return;\n }\n } finally {\n // Always remove from loading set when done\n // 완료 시 항상 로딩 세트에서 제거\n this.loadingMusic.delete(id);\n }\n }\n }\n\n if (audio) {\n try {\n audio.currentTime = 0;\n audio.volume = (volume ?? this._musicVolume) * this._masterVolume;\n audio.loop = true;\n this.currentMusic = audio;\n this._currentMusicTrack = id;\n await audio.play();\n \n if (import.meta.env.DEV) {\n console.log(`[AudioManager] Playing music: ${id} (volume: ${audio.volume.toFixed(2)})`);\n }\n } catch (error) {\n console.warn(`Failed to play music ${id}:`, error);\n }\n }\n }\n\n /**\n * Wait for a music asset to finish loading\n * 음악 자산 로드가 완료될 때까지 대기\n * @param id - Music track ID\n * @param maxWaitMs - Maximum time to wait in milliseconds\n */\n private async waitForMusicLoad(id: string, maxWaitMs: number = 5000): Promise<void> {\n const startTime = Date.now();\n const checkInterval = 100; // Check every 100ms\n \n while (this.loadingMusic.has(id)) {\n if (Date.now() - startTime > maxWaitMs) {\n console.warn(`[AudioManager] Timeout waiting for music \"${id}\" to load`);\n return;\n }\n await new Promise(resolve => setTimeout(resolve, checkInterval));\n }\n }\n\n stopMusic(): void {\n if (this.currentMusic) {\n this.currentMusic.pause();\n this.currentMusic.currentTime = 0;\n this.currentMusic = null;\n this._currentMusicTrack = null;\n }\n }\n\n stopAll(): void {\n this.stopMusic();\n this.soundCache.forEach((audio) => {\n if (!audio.paused) {\n audio.pause();\n audio.currentTime = 0;\n }\n });\n }\n\n setVolume(type: \"master\" | \"sfx\" | \"music\" | \"voice\", volume: number): void {\n const clampedVolume = Math.max(0, Math.min(1, volume));\n\n switch (type) {\n case \"master\":\n this._masterVolume = clampedVolume;\n break;\n case \"sfx\":\n this._sfxVolume = clampedVolume;\n break;\n case \"music\":\n this._musicVolume = clampedVolume;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n break;\n case \"voice\":\n break;\n }\n }\n\n mute(): void {\n this._muted = true;\n if (this.currentMusic) {\n this.currentMusic.volume = 0;\n }\n }\n\n unmute(): void {\n this._muted = false;\n if (this.currentMusic) {\n this.currentMusic.volume = this._musicVolume * this._masterVolume;\n }\n }\n\n async fadeOut(duration: number = 1000): Promise<void> {\n if (!this.currentMusic) return;\n\n const musicElement = this.currentMusic;\n return new Promise((resolve) => {\n const startVolume = musicElement.volume;\n const fadeStep = startVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume > 0) {\n this.currentMusic.volume = Math.max(\n 0,\n this.currentMusic.volume - fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n this.stopMusic();\n resolve();\n }\n }, 50);\n });\n }\n\n async fadeIn(trackId: MusicTrackId, duration: number = 1000): Promise<void> {\n await this.playMusic(trackId, 0);\n\n if (!this.currentMusic) return;\n\n return new Promise((resolve) => {\n const targetVolume = this._musicVolume * this._masterVolume;\n const fadeStep = targetVolume / (duration / 50);\n\n const fadeInterval = setInterval(() => {\n if (this.currentMusic && this.currentMusic.volume < targetVolume) {\n this.currentMusic.volume = Math.min(\n targetVolume,\n this.currentMusic.volume + fadeStep\n );\n } else {\n clearInterval(fadeInterval);\n resolve();\n }\n }, 50);\n });\n }\n\n async crossfade(\n fromTrackId: MusicTrackId,\n toTrackId: MusicTrackId,\n duration: number = 1000\n ): Promise<void> {\n const fadeOutPromise = this.fadeOut(duration);\n await fadeOutPromise;\n await this.fadeIn(toTrackId, duration);\n console.log(`Crossfaded from ${fromTrackId} to ${toTrackId}`);\n }\n\n /** @returns A snapshot of all currently loaded sound elements */\n getLoadedAssets(): ReadonlyMap<string, HTMLAudioElement> {\n return new Map(this.soundCache);\n }\n\n async playVoice(id: string): Promise<void> {\n return this.playSoundEffect(id);\n }\n\n async playKoreanTechniqueSound(\n techniqueId: string,\n archetype: string\n ): Promise<void> {\n const soundId = `${archetype}_${techniqueId}`;\n return this.playSoundEffect(soundId);\n }\n\n async playTrigramStanceSound(stance: string): Promise<void> {\n const soundId = `stance_${stance}`;\n return this.playSoundEffect(soundId);\n }\n\n async playVitalPointHitSound(severity: string): Promise<void> {\n const soundId = `vital_point_${severity}`;\n return this.playSoundEffect(soundId);\n }\n\n async playDojiangAmbience(): Promise<void> {\n return this.playMusic(\"dojang_ambience\");\n }\n\n // Legacy getters for backward compatibility\n getMasterVolume(): number {\n return this._masterVolume;\n }\n\n getMusicVolume(): number {\n return this._musicVolume;\n }\n\n getSfxVolume(): number {\n return this._sfxVolume;\n }\n\n get initialized(): boolean {\n return this._isInitialized;\n }\n\n /**\n * Batch load multiple assets with progress tracking\n * @param assets - Array of audio assets to load\n * @param options - Optional load configuration (timeout, retries, etc.)\n * @param onProgress - Optional callback for progress updates with loaded and total counts\n * @returns Promise that resolves when all assets are processed\n */\n async batchLoadAssets(\n assets: readonly AudioAsset[],\n options?: LoadOptions,\n onProgress?: (loaded: number, total: number) => void\n ): Promise<void> {\n const results = await this.assetLoader.batchLoad(\n assets,\n options,\n (progress) => {\n onProgress?.(progress.loaded, progress.total);\n }\n );\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n const asset = assets[i];\n\n if (result.success && result.audio) {\n result.audio.volume = asset.volume ?? 1.0;\n this.soundCache.set(asset.id, result.audio);\n\n // Create pool for frequently used sounds\n if (this.frequentSounds.has(asset.id)) {\n this.audioPool.createPool(asset.id, asset.url, {\n initialSize: 3,\n maxSize: 10,\n autoExpand: true,\n });\n }\n }\n }\n }\n\n /**\n * Unload an asset to free memory\n * @param assetId - ID of the asset to unload\n * @returns true if asset was unloaded, false if asset was not found\n */\n unloadAsset(assetId: string): boolean {\n const audio = this.soundCache.get(assetId);\n if (audio) {\n audio.pause();\n audio.src = \"\";\n this.soundCache.delete(assetId);\n }\n\n // LRU 캐시에서 제거\n const removedFromCache = this.audioCache.remove(assetId);\n\n const unloaded = this.assetLoader.unloadAsset(assetId);\n\n if (this.audioPool.hasPool(assetId)) {\n this.audioPool.removePool(assetId);\n }\n\n this.monitor.unregisterAsset(assetId);\n\n return unloaded || audio !== undefined || removedFromCache;\n }\n\n /**\n * Get cache statistics including LRU cache metrics\n * LRU 캐시 메트릭을 포함한 캐시 통계 가져오기\n *\n * @returns Cache statistics\n */\n getCacheStats(): {\n readonly lruCache: {\n readonly totalSize: number;\n readonly assetCount: number;\n readonly criticalCount: number;\n readonly utilizationPercent: number;\n readonly evictionCount: number;\n readonly hitCount: number;\n readonly missCount: number;\n readonly hitRate: number;\n };\n readonly soundCache: number;\n readonly poolStats: Map<string, PoolStatistics>;\n } {\n return {\n lruCache: this.audioCache.getStats(),\n soundCache: this.soundCache.size,\n poolStats: this.audioPool.getAllStatistics(),\n };\n }\n\n /**\n * Get memory statistics including total loaded MB, asset count, and warnings\n * @returns Memory statistics object\n */\n getMemoryStats(): AudioMemoryStats {\n return this.monitor.getMemoryStats();\n }\n\n /**\n * Get performance statistics including load times and playback latency\n * @returns Performance statistics object\n */\n getPerformanceStats(): AudioPerformanceStats {\n return this.monitor.getPerformanceStats();\n }\n\n /**\n * Get FPS impact analysis showing baseline vs current FPS and detected drops\n * @returns FPS impact analysis object\n */\n getFPSImpact(): AudioFPSImpact {\n return this.monitor.getFPSImpact();\n }\n\n /**\n * Update FPS measurement for monitoring\n * @param fps - Current frames per second measurement\n */\n updateFPS(fps: number): void {\n this.monitor.updateFPS(fps);\n }\n\n /**\n * Get comprehensive monitoring report including memory, performance, FPS, and warnings\n * @returns Comprehensive monitoring report object\n */\n getMonitoringReport(): {\n readonly memory: AudioMemoryStats;\n readonly performance: AudioPerformanceStats;\n readonly fps: AudioFPSImpact;\n readonly warnings: readonly MemoryWarning[];\n } {\n return this.monitor.getReport();\n }\n\n /**\n * Get all memory warnings\n * @returns Array of memory warnings\n */\n getMemoryWarnings(): readonly MemoryWarning[] {\n return this.monitor.getWarnings();\n }\n\n /**\n * Clear memory warnings\n */\n clearMemoryWarnings(): void {\n this.monitor.clearWarnings();\n }\n\n /**\n * Get pool statistics for a specific asset or all pools\n * @param assetId - Optional asset ID to get specific pool stats\n * @returns Pool statistics for the specified asset or all pools\n */\n getPoolStatistics(\n assetId?: string\n ): PoolStatistics | Map<string, PoolStatistics> | undefined {\n if (assetId) {\n return this.audioPool.getPoolStatistics(assetId);\n }\n return this.audioPool.getAllStatistics();\n }\n\n /**\n * Get loader statistics including cached assets and loading state\n * @returns Loader statistics object\n */\n getLoaderStatistics(): {\n readonly cached: number;\n readonly loading: number;\n readonly totalAttempts: number;\n } {\n return this.assetLoader.getStatistics();\n }\n}\n\nexport default AudioManager;\n"],"mappings":"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"}
@@ -508,7 +508,7 @@ var CombatScreen3D = ({ players, onPlayerUpdate, currentRound, timeRemaining, is
508
508
  setPlayer1AttackDuration(attackDuration);
509
509
  return attackDuration;
510
510
  }, []);
511
- const player1Animation = usePlayerAnimation({ events: useMemo(() => ({
511
+ const player1AnimationEvents = useMemo(() => ({
512
512
  onFrame: (frame, state) => {
513
513
  if (state === AnimationState.ATTACK && frame >= player1HitTriggerFrameRef.current && !player1AttackHitFiredRef.current) {
514
514
  player1AttackHitFiredRef.current = true;
@@ -525,7 +525,8 @@ var CombatScreen3D = ({ players, onPlayerUpdate, currentRound, timeRemaining, is
525
525
  if (currentStance && player1AnimationRef.current) player1AnimationRef.current.transitionToStanceGuard(currentStance);
526
526
  }
527
527
  }
528
- }), [combatActions, audio]) });
528
+ }), [combatActions, audio]);
529
+ const player1Animation = usePlayerAnimation({ events: player1AnimationEvents });
529
530
  useEffect(() => {
530
531
  player1AnimationRef.current = player1Animation;
531
532
  }, [player1Animation]);
@@ -943,7 +944,8 @@ var CombatScreen3D = ({ players, onPlayerUpdate, currentRound, timeRemaining, is
943
944
  const executeFallbackRecovery = useCallback(() => {
944
945
  const groundState = balanceSystem.getGroundState(player1Animation.currentState);
945
946
  if (groundState) {
946
- const animationState = getRecoveryAnimationState(determineRecoveryType(groundState));
947
+ const recoveryType = determineRecoveryType(groundState);
948
+ const animationState = getRecoveryAnimationState(recoveryType);
947
949
  player1Animation.transitionTo(animationState);
948
950
  }
949
951
  }, [balanceSystem, player1Animation]);
@@ -991,7 +993,6 @@ var CombatScreen3D = ({ players, onPlayerUpdate, currentRound, timeRemaining, is
991
993
  case "stance_side_switch":
992
994
  player1Animation.transitionTo(AnimationState.STANCE_SIDE_SWITCH);
993
995
  handleStanceSideSwitch(PLAYER_ONE_INDEX);
994
- break;
995
996
  }
996
997
  }, [
997
998
  techniqueSelection,
@@ -1101,9 +1102,7 @@ var CombatScreen3D = ({ players, onPlayerUpdate, currentRound, timeRemaining, is
1101
1102
  case "swipe-down":
1102
1103
  techniqueSelection.executeTechnique();
1103
1104
  break;
1104
- case "two-finger-tap":
1105
- audio.playSFX("menu_select");
1106
- break;
1105
+ case "two-finger-tap": audio.playSFX("menu_select");
1107
1106
  }
1108
1107
  }, [techniqueSelection, audio]);
1109
1108
  const toggleStanceWheel = useCallback(() => {
@@ -1181,7 +1180,8 @@ var CombatScreen3D = ({ players, onPlayerUpdate, currentRound, timeRemaining, is
1181
1180
  updateDifficultyTarget
1182
1181
  ]);
1183
1182
  const executeAIActionCallback = useCallback((action, targetPos, selectedTechnique, targetVitalPoint) => {
1184
- const aiFallbackTechnique = TRIGRAM_TECHNIQUES[validPlayers[1]?.currentStance ?? TrigramStance.GEON]?.[0];
1183
+ const aiStance = validPlayers[1]?.currentStance ?? TrigramStance.GEON;
1184
+ const aiFallbackTechnique = TRIGRAM_TECHNIQUES[aiStance]?.[0];
1185
1185
  const aiFallbackAnim = aiFallbackTechnique ? resolveTechniqueAnimation(aiFallbackTechnique) : "jab";
1186
1186
  switch (action) {
1187
1187
  case "attack":
@@ -1274,7 +1274,6 @@ var CombatScreen3D = ({ players, onPlayerUpdate, currentRound, timeRemaining, is
1274
1274
  }
1275
1275
  handleAIAttack(selectedTechnique, targetVitalPoint);
1276
1276
  addCombatMessage("AI 반격!", "AI Counter!");
1277
- break;
1278
1277
  }
1279
1278
  }, [
1280
1279
  handleAIAttack,