blacktrigram 0.7.98 → 0.7.100

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 +14 -14
@@ -1 +1 @@
1
- {"version":3,"file":"BloodParticles3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodParticles3D.tsx"],"sourcesContent":["/**\n * BloodParticles3D - Realistic blood splatter particle system\n *\n * Creates physics-based blood particles that spray from impact points with gravity,\n * creating pools on the arena floor. Optimized for 60fps with instanced rendering.\n *\n * Features:\n * - Gravity-based particle physics\n * - Blood pool accumulation on floor\n * - 10-second fade-out for pools\n * - Instanced rendering for performance\n * - Mobile-optimized particle counts\n *\n * @module components/combat/BloodParticles3D\n * @category Combat Effects\n * @korean 피입자3D\n */\n\nimport { Points, PointMaterial } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../../utils/threeObjectPool\";\n\n/**\n * Blood particle data structure for efficient simulation\n * \n * PERFORMANCE: position and velocity now use pooled Vector3 objects\n * that are acquired on particle creation and released on expiration\n */\ninterface BloodParticle {\n /** Current position [x, y, z] - POOLED Vector3 */\n position: THREE.Vector3;\n /** Current velocity [x, y, z] - POOLED Vector3 */\n velocity: THREE.Vector3;\n /** Particle lifetime in seconds */\n lifetime: number;\n /** Time elapsed since creation */\n age: number;\n /** Whether particle has settled on floor */\n settled: boolean;\n /** Flag to track if vectors are pooled and need release */\n isPooled: boolean;\n}\n\n/**\n * Blood splatter effect configuration\n */\nexport interface BloodSplatterEffect {\n /** Unique identifier */\n readonly id: string;\n /** Origin position in 3D world space */\n readonly position: [number, number, number];\n /** Impact direction for splatter */\n readonly direction: [number, number, number];\n /** Intensity of effect (0.0 to 1.0) */\n readonly intensity: number;\n /** Timestamp when effect was created */\n readonly startTime: number;\n}\n\n/**\n * Props for BloodParticles3D component\n */\nexport interface BloodParticles3DProps {\n /** Active blood splatter effects to render */\n readonly effects: readonly BloodSplatterEffect[];\n /** Whether to enable blood effects (violence settings) */\n readonly enabled?: boolean;\n /** Mobile device mode (reduced particle count) */\n readonly isMobile?: boolean;\n /** Callback when effect completes */\n readonly onEffectComplete?: (effectId: string) => void;\n}\n\n/**\n * Performance and physics constants\n */\nconst BLOOD_CONSTANTS = {\n /** Gravity acceleration (m/s²) */\n GRAVITY: -9.8,\n /** Maximum particles per splatter effect */\n MAX_PARTICLES_DESKTOP: 300,\n MAX_PARTICLES_MOBILE: 100,\n /** Particle lifetime in seconds */\n PARTICLE_LIFETIME: 2.0,\n /** Blood pool fade duration in seconds */\n POOL_FADE_DURATION: 10.0,\n /** Initial velocity range */\n VELOCITY_MIN: 2.0,\n VELOCITY_MAX: 5.0,\n /** Spread angle in radians */\n SPREAD_ANGLE: Math.PI / 3,\n /** Floor Y position */\n FLOOR_Y: 0.0,\n /** Particle size */\n PARTICLE_SIZE: 0.05,\n /**\n * Maximum per-frame delta time (seconds) used to clamp the blood physics update.\n * \n * We cap the simulation step at ~33ms (1/30) to avoid the classic\n * \"spiral of death\" when the frame rate drops: very large timesteps can\n * cause unstable motion, particles tunneling through the floor, or\n * visually exaggerated splatter. Treating anything below 30fps as\n * \"slow motion\" for this effect keeps the blood behavior stable even\n * on slower devices. If the engine's minimum target frame rate changes,\n * adjust this threshold accordingly.\n */\n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * Generate initial particles for a blood splatter effect\n * \n * PERFORMANCE OPTIMIZATION: Uses ThreeObjectPools for ALL Vector3 allocations\n * \n * Pooling Strategy (UPDATED):\n * - Acquire temp vectors for calculations (baseDir, origin, axis vectors) - RELEASED\n * - Acquire position/velocity from pool for particle ownership - MUST BE RELEASED on expiration\n * \n * Memory Impact:\n * - Before: 600 Vector3 allocations per splatter (2 per particle × 300)\n * - After: 5 temp vectors (reused) + particles use pooled vectors (released on expiration)\n * - Reduction: ~99.2% fewer allocations (600 → 5 temp + pooled reuse)\n * \n * @param effect - Blood splatter effect configuration\n * @param maxParticles - Maximum number of particles to generate\n * @returns Array of blood particles with ALL vectors from pool (must be released later)\n */\nconst generateBloodParticles = (\n effect: BloodSplatterEffect,\n maxParticles: number\n): BloodParticle[] => {\n const particles: BloodParticle[] = [];\n const particleCount = Math.floor(maxParticles * effect.intensity);\n\n const baseDir = ThreeObjectPools.vector3.acquire();\n const origin = ThreeObjectPools.vector3.acquire();\n const direction = ThreeObjectPools.vector3.acquire();\n const yAxis = ThreeObjectPools.vector3.acquire();\n const zAxis = ThreeObjectPools.vector3.acquire();\n\n baseDir.set(...effect.direction).normalize();\n origin.set(...effect.position);\n yAxis.set(0, 1, 0);\n zAxis.set(0, 0, 1);\n\n for (let i = 0; i < particleCount; i++) {\n const spreadAngle = BLOOD_CONSTANTS.SPREAD_ANGLE;\n const phi = (Math.random() - 0.5) * spreadAngle;\n const theta = Math.random() * Math.PI * 2;\n\n direction.copy(baseDir);\n direction.applyAxisAngle(yAxis, phi);\n direction.applyAxisAngle(zAxis, theta);\n\n const speed =\n BLOOD_CONSTANTS.VELOCITY_MIN +\n Math.random() * (BLOOD_CONSTANTS.VELOCITY_MAX - BLOOD_CONSTANTS.VELOCITY_MIN);\n\n const particlePosition = ThreeObjectPools.vector3.acquire();\n const particleVelocity = ThreeObjectPools.vector3.acquire();\n \n particlePosition.copy(origin);\n particleVelocity.copy(direction).multiplyScalar(speed);\n\n particles.push({\n position: particlePosition,\n velocity: particleVelocity,\n lifetime: BLOOD_CONSTANTS.PARTICLE_LIFETIME,\n age: 0,\n settled: false,\n isPooled: true, // Mark as pooled for cleanup\n });\n }\n\n ThreeObjectPools.vector3.release(baseDir);\n ThreeObjectPools.vector3.release(origin);\n ThreeObjectPools.vector3.release(direction);\n ThreeObjectPools.vector3.release(yAxis);\n ThreeObjectPools.vector3.release(zAxis);\n\n return particles;\n};\n\n/**\n * BloodParticles3D Component\n *\n * Renders realistic blood splatter effects using physics-based particle simulation.\n * Optimized for performance with instanced rendering and efficient physics updates.\n *\n * @example\n * ```tsx\n * const [bloodEffects, setBloodEffects] = useState<BloodSplatterEffect[]>([]);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], direction: [number, number, number]) => {\n * setBloodEffects([...bloodEffects, {\n * id: generateId(),\n * position,\n * direction,\n * intensity: 0.8,\n * startTime: Date.now(),\n * }]);\n * };\n *\n * <BloodParticles3D\n * effects={bloodEffects}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onEffectComplete={(id) => {\n * setBloodEffects(prev => prev.filter(e => e.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodParticles3D: React.FC<BloodParticles3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const particlesRef = useRef<Map<string, BloodParticle[]>>(new Map());\n const poolParticlesRef = useRef<BloodParticle[]>([]);\n const pointsRef = useRef<THREE.Points>(null);\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const maxParticles = isMobile\n ? BLOOD_CONSTANTS.MAX_PARTICLES_MOBILE\n : BLOOD_CONSTANTS.MAX_PARTICLES_DESKTOP;\n\n const bloodColor = useMemo(() => KOREAN_COLORS.BLOODLOSS_INDICATOR, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n effects.forEach((effect) => {\n if (!particlesRef.current.has(effect.id)) {\n const particles = generateBloodParticles(effect, maxParticles);\n particlesRef.current.set(effect.id, particles);\n }\n });\n\n const effectIds = new Set(effects.map((e) => e.id));\n particlesRef.current.forEach((particles, id) => {\n if (!effectIds.has(id)) {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n particlesRef.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n }, [effects, enabled, maxParticles]);\n\n React.useEffect(() => {\n const currentParticles = particlesRef.current;\n const currentPoolParticles = poolParticlesRef.current;\n const currentCompletedEffects = completedEffectsRef.current;\n \n return () => {\n currentParticles.forEach((particles) => {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n });\n \n currentPoolParticles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n \n poolParticlesRef.current = [];\n \n currentParticles.clear();\n currentCompletedEffects.clear();\n };\n }, []);\n\n const initialPositions = useMemo(() => {\n const positions = new Float32Array(maxParticles * 3);\n return positions;\n }, [maxParticles]);\n\n useFrame((_, delta) => {\n if (!enabled || !pointsRef.current) return;\n\n const safeDelta = Math.min(delta, BLOOD_CONSTANTS.MAX_DELTA);\n\n let totalParticleIndex = 0;\n const attr = pointsRef.current.geometry.attributes.position;\n const posArray = attr.array as Float32Array;\n\n particlesRef.current.forEach((particles, effectId) => {\n let hasActiveParticles = false;\n\n for (let i = 0; i < particles.length; i++) {\n const p = particles[i];\n p.age += safeDelta;\n\n if (!p.settled) {\n p.velocity.y += BLOOD_CONSTANTS.GRAVITY * safeDelta;\n\n p.position.addScaledVector(p.velocity, safeDelta);\n\n if (p.position.y <= BLOOD_CONSTANTS.FLOOR_Y) {\n p.position.y = BLOOD_CONSTANTS.FLOOR_Y;\n p.velocity.set(0, 0, 0);\n p.settled = true;\n p.lifetime = BLOOD_CONSTANTS.POOL_FADE_DURATION;\n p.age = 0;\n\n poolParticlesRef.current.push(p);\n } else {\n hasActiveParticles = true;\n }\n }\n\n if (totalParticleIndex < posArray.length / 3) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n } else if (process.env.NODE_ENV === \"development\") {\n console.warn(\n `BloodParticles3D: Particle buffer full (${posArray.length / 3} particles). ` +\n `Additional particles are not rendered. Consider reducing particle counts or ` +\n `increasing maxParticles if this happens frequently.`\n );\n }\n }\n\n if (!hasActiveParticles && !completedEffectsRef.current.has(effectId)) {\n completedEffectsRef.current.add(effectId);\n onEffectComplete?.(effectId);\n }\n });\n\n const maxVisibleParticles = Math.min(maxParticles, posArray.length / 3);\n\n if (totalParticleIndex >= maxVisibleParticles) {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n \n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n \n return isAlive;\n });\n } else {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n\n if (isAlive && totalParticleIndex < maxVisibleParticles) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n }\n\n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n\n return isAlive;\n });\n }\n\n attr.needsUpdate = true;\n });\n\n if (!enabled || effects.length === 0) {\n return null;\n }\n\n return (\n <Points\n ref={pointsRef}\n positions={initialPositions}\n data-testid=\"blood-particles-3d\"\n >\n <PointMaterial\n color={bloodColor}\n size={BLOOD_CONSTANTS.PARTICLE_SIZE}\n sizeAttenuation\n transparent\n opacity={0.9}\n depthWrite={false}\n blending={THREE.NormalBlending}\n />\n </Points>\n );\n};\n\nexport default BloodParticles3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+EA,IAAM,kBAAkB;;CAEtB,SAAS;;CAET,uBAAuB;CACvB,sBAAsB;;CAEtB,mBAAmB;;CAEnB,oBAAoB;;CAEpB,cAAc;CACd,cAAc;;CAEd,cAAc,KAAK,KAAK;;CAExB,SAAS;;CAET,eAAe;;;;;;;;;;;;CAYf,WAAW,IAAI;AACjB;;;;;;;;;;;;;;;;;;;AAoBA,IAAM,0BACJ,QACA,iBACoB;CACpB,MAAM,YAA6B,CAAC;CACpC,MAAM,gBAAgB,KAAK,MAAM,eAAe,OAAO,SAAS;CAEhE,MAAM,UAAU,iBAAiB,QAAQ,QAAQ;CACjD,MAAM,SAAS,iBAAiB,QAAQ,QAAQ;CAChD,MAAM,YAAY,iBAAiB,QAAQ,QAAQ;CACnD,MAAM,QAAQ,iBAAiB,QAAQ,QAAQ;CAC/C,MAAM,QAAQ,iBAAiB,QAAQ,QAAQ;CAE/C,QAAQ,IAAI,GAAG,OAAO,SAAS,CAAC,CAAC,UAAU;CAC3C,OAAO,IAAI,GAAG,OAAO,QAAQ;CAC7B,MAAM,IAAI,GAAG,GAAG,CAAC;CACjB,MAAM,IAAI,GAAG,GAAG,CAAC;CAEjB,KAAK,IAAI,IAAI,GAAG,IAAI,eAAe,KAAK;EACtC,MAAM,cAAc,gBAAgB;EACpC,MAAM,OAAO,KAAK,OAAO,IAAI,MAAO;EACpC,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,KAAK;EAExC,UAAU,KAAK,OAAO;EACtB,UAAU,eAAe,OAAO,GAAG;EACnC,UAAU,eAAe,OAAO,KAAK;EAErC,MAAM,QACJ,gBAAgB,eAChB,KAAK,OAAO,KAAK,gBAAgB,eAAe,gBAAgB;EAElE,MAAM,mBAAmB,iBAAiB,QAAQ,QAAQ;EAC1D,MAAM,mBAAmB,iBAAiB,QAAQ,QAAQ;EAE1D,iBAAiB,KAAK,MAAM;EAC5B,iBAAiB,KAAK,SAAS,CAAC,CAAC,eAAe,KAAK;EAErD,UAAU,KAAK;GACb,UAAU;GACV,UAAU;GACV,UAAU,gBAAgB;GAC1B,KAAK;GACL,SAAS;GACT,UAAU;EACZ,CAAC;CACH;CAEA,iBAAiB,QAAQ,QAAQ,OAAO;CACxC,iBAAiB,QAAQ,QAAQ,MAAM;CACvC,iBAAiB,QAAQ,QAAQ,SAAS;CAC1C,iBAAiB,QAAQ,QAAQ,KAAK;CACtC,iBAAiB,QAAQ,QAAQ,KAAK;CAEtC,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAa,oBAAqD,EAChE,SACA,UAAU,MACV,WAAW,OACX,uBACI;CACJ,MAAM,eAAe,uBAAqC,IAAI,IAAI,CAAC;CACnE,MAAM,mBAAmB,OAAwB,CAAC,CAAC;CACnD,MAAM,YAAY,OAAqB,IAAI;CAC3C,MAAM,sBAAsB,uBAAoB,IAAI,IAAI,CAAC;CAEzD,MAAM,eAAe,WACjB,gBAAgB,uBAChB,gBAAgB;CAEpB,MAAM,aAAa,cAAc,cAAc,qBAAqB,CAAC,CAAC;CAEtE,MAAM,gBAAgB;EACpB,IAAI,CAAC,SAAS;EAEd,QAAQ,SAAS,WAAW;GAC1B,IAAI,CAAC,aAAa,QAAQ,IAAI,OAAO,EAAE,GAAG;IACxC,MAAM,YAAY,uBAAuB,QAAQ,YAAY;IAC7D,aAAa,QAAQ,IAAI,OAAO,IAAI,SAAS;GAC/C;EACF,CAAC;EAED,MAAM,YAAY,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,EAAE,CAAC;EAClD,aAAa,QAAQ,SAAS,WAAW,OAAO;GAC9C,IAAI,CAAC,UAAU,IAAI,EAAE,GAAG;IACtB,UAAU,SAAS,MAAM;KACvB,IAAI,EAAE,UAAU;MACd,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;MAC3C,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;MAC3C,EAAE,WAAW;KACf;IACF,CAAC;IACD,aAAa,QAAQ,OAAO,EAAE;IAC9B,oBAAoB,QAAQ,OAAO,EAAE;GACvC;EACF,CAAC;CACH,GAAG;EAAC;EAAS;EAAS;CAAY,CAAC;CAEnC,MAAM,gBAAgB;EACpB,MAAM,mBAAmB,aAAa;EACtC,MAAM,uBAAuB,iBAAiB;EAC9C,MAAM,0BAA0B,oBAAoB;EAEpD,aAAa;GACX,iBAAiB,SAAS,cAAc;IACtC,UAAU,SAAS,MAAM;KACvB,IAAI,EAAE,UAAU;MACd,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;MAC3C,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;MAC3C,EAAE,WAAW;KACf;IACF,CAAC;GACH,CAAC;GAED,qBAAqB,SAAS,MAAM;IAClC,IAAI,EAAE,UAAU;KACd,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;KAC3C,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;KAC3C,EAAE,WAAW;IACf;GACF,CAAC;GAED,iBAAiB,UAAU,CAAC;GAE5B,iBAAiB,MAAM;GACvB,wBAAwB,MAAM;EAChC;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,mBAAmB,cAAc;EAErC,OAAO,IADe,aAAa,eAAe,CAC3C;CACT,GAAG,CAAC,YAAY,CAAC;CAEjB,UAAU,GAAG,UAAU;EACrB,IAAI,CAAC,WAAW,CAAC,UAAU,SAAS;EAEpC,MAAM,YAAY,KAAK,IAAI,OAAO,gBAAgB,SAAS;EAE3D,IAAI,qBAAqB;EACzB,MAAM,OAAO,UAAU,QAAQ,SAAS,WAAW;EACnD,MAAM,WAAW,KAAK;EAEtB,aAAa,QAAQ,SAAS,WAAW,aAAa;GACpD,IAAI,qBAAqB;GAEzB,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;IACzC,MAAM,IAAI,UAAU;IACpB,EAAE,OAAO;IAET,IAAI,CAAC,EAAE,SAAS;KACd,EAAE,SAAS,KAAK,gBAAgB,UAAU;KAE1C,EAAE,SAAS,gBAAgB,EAAE,UAAU,SAAS;KAEhD,IAAI,EAAE,SAAS,KAAK,gBAAgB,SAAS;MAC3C,EAAE,SAAS,IAAI,gBAAgB;MAC/B,EAAE,SAAS,IAAI,GAAG,GAAG,CAAC;MACtB,EAAE,UAAU;MACZ,EAAE,WAAW,gBAAgB;MAC7B,EAAE,MAAM;MAER,iBAAiB,QAAQ,KAAK,CAAC;KACjC,OACE,qBAAqB;IAEzB;IAEA,IAAI,qBAAqB,SAAS,SAAS,GAAG;KAC5C,SAAS,qBAAqB,KAAK,EAAE,SAAS;KAC9C,SAAS,qBAAqB,IAAI,KAAK,EAAE,SAAS;KAClD,SAAS,qBAAqB,IAAI,KAAK,EAAE,SAAS;KAClD;IACF,OAAO,IAAA,QAAA,IAAA,aAA6B,eAClC,QAAQ,KACN,2CAA2C,SAAS,SAAS,EAAE,6IAGjE;GAEJ;GAEA,IAAI,CAAC,sBAAsB,CAAC,oBAAoB,QAAQ,IAAI,QAAQ,GAAG;IACrE,oBAAoB,QAAQ,IAAI,QAAQ;IACxC,mBAAmB,QAAQ;GAC7B;EACF,CAAC;EAED,MAAM,sBAAsB,KAAK,IAAI,cAAc,SAAS,SAAS,CAAC;EAEtE,IAAI,sBAAsB,qBACxB,iBAAiB,UAAU,iBAAiB,QAAQ,QAAQ,MAAM;GAChE,EAAE,OAAO;GACT,MAAM,UAAU,EAAE,MAAM,EAAE;GAE1B,IAAI,CAAC,WAAW,EAAE,UAAU;IAC1B,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;IAC3C,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;IAC3C,EAAE,WAAW;GACf;GAEA,OAAO;EACT,CAAC;OAED,iBAAiB,UAAU,iBAAiB,QAAQ,QAAQ,MAAM;GAChE,EAAE,OAAO;GACT,MAAM,UAAU,EAAE,MAAM,EAAE;GAE1B,IAAI,WAAW,qBAAqB,qBAAqB;IACvD,SAAS,qBAAqB,KAAK,EAAE,SAAS;IAC9C,SAAS,qBAAqB,IAAI,KAAK,EAAE,SAAS;IAClD,SAAS,qBAAqB,IAAI,KAAK,EAAE,SAAS;IAClD;GACF;GAEA,IAAI,CAAC,WAAW,EAAE,UAAU;IAC1B,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;IAC3C,iBAAiB,QAAQ,QAAQ,EAAE,QAAQ;IAC3C,EAAE,WAAW;GACf;GAEA,OAAO;EACT,CAAC;EAGH,KAAK,cAAc;CACrB,CAAC;CAED,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC,OAAO;CAGT,OACE,oBAAC,QAAD;EACE,KAAK;EACL,WAAW;EACX,eAAY;YAEZ,oBAAC,eAAD;GACE,OAAO;GACP,MAAM,gBAAgB;GACtB,iBAAA;GACA,aAAA;GACA,SAAS;GACT,YAAY;GACZ,UAAU,MAAM;EACjB,CAAA;CACK,CAAA;AAEZ"}
1
+ {"version":3,"file":"BloodParticles3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodParticles3D.tsx"],"sourcesContent":["/**\n * BloodParticles3D - Realistic blood splatter particle system\n *\n * Creates physics-based blood particles that spray from impact points with gravity,\n * creating pools on the arena floor. Optimized for 60fps with instanced rendering.\n *\n * Features:\n * - Gravity-based particle physics\n * - Blood pool accumulation on floor\n * - 10-second fade-out for pools\n * - Instanced rendering for performance\n * - Mobile-optimized particle counts\n *\n * @module components/combat/BloodParticles3D\n * @category Combat Effects\n * @korean 피입자3D\n */\n\nimport { Points, PointMaterial } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../../utils/threeObjectPool\";\n\n/**\n * Blood particle data structure for efficient simulation\n * \n * PERFORMANCE: position and velocity now use pooled Vector3 objects\n * that are acquired on particle creation and released on expiration\n */\ninterface BloodParticle {\n /** Current position [x, y, z] - POOLED Vector3 */\n position: THREE.Vector3;\n /** Current velocity [x, y, z] - POOLED Vector3 */\n velocity: THREE.Vector3;\n /** Particle lifetime in seconds */\n lifetime: number;\n /** Time elapsed since creation */\n age: number;\n /** Whether particle has settled on floor */\n settled: boolean;\n /** Flag to track if vectors are pooled and need release */\n isPooled: boolean;\n}\n\n/**\n * Blood splatter effect configuration\n */\nexport interface BloodSplatterEffect {\n /** Unique identifier */\n readonly id: string;\n /** Origin position in 3D world space */\n readonly position: [number, number, number];\n /** Impact direction for splatter */\n readonly direction: [number, number, number];\n /** Intensity of effect (0.0 to 1.0) */\n readonly intensity: number;\n /** Timestamp when effect was created */\n readonly startTime: number;\n}\n\n/**\n * Props for BloodParticles3D component\n */\nexport interface BloodParticles3DProps {\n /** Active blood splatter effects to render */\n readonly effects: readonly BloodSplatterEffect[];\n /** Whether to enable blood effects (violence settings) */\n readonly enabled?: boolean;\n /** Mobile device mode (reduced particle count) */\n readonly isMobile?: boolean;\n /** Callback when effect completes */\n readonly onEffectComplete?: (effectId: string) => void;\n}\n\n/**\n * Performance and physics constants\n */\nconst BLOOD_CONSTANTS = {\n /** Gravity acceleration (m/s²) */\n GRAVITY: -9.8,\n /** Maximum particles per splatter effect */\n MAX_PARTICLES_DESKTOP: 300,\n MAX_PARTICLES_MOBILE: 100,\n /** Particle lifetime in seconds */\n PARTICLE_LIFETIME: 2.0,\n /** Blood pool fade duration in seconds */\n POOL_FADE_DURATION: 10.0,\n /** Initial velocity range */\n VELOCITY_MIN: 2.0,\n VELOCITY_MAX: 5.0,\n /** Spread angle in radians */\n SPREAD_ANGLE: Math.PI / 3,\n /** Floor Y position */\n FLOOR_Y: 0.0,\n /** Particle size */\n PARTICLE_SIZE: 0.05,\n /**\n * Maximum per-frame delta time (seconds) used to clamp the blood physics update.\n * \n * We cap the simulation step at ~33ms (1/30) to avoid the classic\n * \"spiral of death\" when the frame rate drops: very large timesteps can\n * cause unstable motion, particles tunneling through the floor, or\n * visually exaggerated splatter. Treating anything below 30fps as\n * \"slow motion\" for this effect keeps the blood behavior stable even\n * on slower devices. If the engine's minimum target frame rate changes,\n * adjust this threshold accordingly.\n */\n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * Generate initial particles for a blood splatter effect\n * \n * PERFORMANCE OPTIMIZATION: Uses ThreeObjectPools for ALL Vector3 allocations\n * \n * Pooling Strategy (UPDATED):\n * - Acquire temp vectors for calculations (baseDir, origin, axis vectors) - RELEASED\n * - Acquire position/velocity from pool for particle ownership - MUST BE RELEASED on expiration\n * \n * Memory Impact:\n * - Before: 600 Vector3 allocations per splatter (2 per particle × 300)\n * - After: 5 temp vectors (reused) + particles use pooled vectors (released on expiration)\n * - Reduction: ~99.2% fewer allocations (600 → 5 temp + pooled reuse)\n * \n * @param effect - Blood splatter effect configuration\n * @param maxParticles - Maximum number of particles to generate\n * @returns Array of blood particles with ALL vectors from pool (must be released later)\n */\nconst generateBloodParticles = (\n effect: BloodSplatterEffect,\n maxParticles: number\n): BloodParticle[] => {\n const particles: BloodParticle[] = [];\n const particleCount = Math.floor(maxParticles * effect.intensity);\n\n const baseDir = ThreeObjectPools.vector3.acquire();\n const origin = ThreeObjectPools.vector3.acquire();\n const direction = ThreeObjectPools.vector3.acquire();\n const yAxis = ThreeObjectPools.vector3.acquire();\n const zAxis = ThreeObjectPools.vector3.acquire();\n\n baseDir.set(...effect.direction).normalize();\n origin.set(...effect.position);\n yAxis.set(0, 1, 0);\n zAxis.set(0, 0, 1);\n\n for (let i = 0; i < particleCount; i++) {\n const spreadAngle = BLOOD_CONSTANTS.SPREAD_ANGLE;\n const phi = (Math.random() - 0.5) * spreadAngle;\n const theta = Math.random() * Math.PI * 2;\n\n direction.copy(baseDir);\n direction.applyAxisAngle(yAxis, phi);\n direction.applyAxisAngle(zAxis, theta);\n\n const speed =\n BLOOD_CONSTANTS.VELOCITY_MIN +\n Math.random() * (BLOOD_CONSTANTS.VELOCITY_MAX - BLOOD_CONSTANTS.VELOCITY_MIN);\n\n const particlePosition = ThreeObjectPools.vector3.acquire();\n const particleVelocity = ThreeObjectPools.vector3.acquire();\n \n particlePosition.copy(origin);\n particleVelocity.copy(direction).multiplyScalar(speed);\n\n particles.push({\n position: particlePosition,\n velocity: particleVelocity,\n lifetime: BLOOD_CONSTANTS.PARTICLE_LIFETIME,\n age: 0,\n settled: false,\n isPooled: true, // Mark as pooled for cleanup\n });\n }\n\n ThreeObjectPools.vector3.release(baseDir);\n ThreeObjectPools.vector3.release(origin);\n ThreeObjectPools.vector3.release(direction);\n ThreeObjectPools.vector3.release(yAxis);\n ThreeObjectPools.vector3.release(zAxis);\n\n return particles;\n};\n\n/**\n * BloodParticles3D Component\n *\n * Renders realistic blood splatter effects using physics-based particle simulation.\n * Optimized for performance with instanced rendering and efficient physics updates.\n *\n * @example\n * ```tsx\n * const [bloodEffects, setBloodEffects] = useState<BloodSplatterEffect[]>([]);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], direction: [number, number, number]) => {\n * setBloodEffects([...bloodEffects, {\n * id: generateId(),\n * position,\n * direction,\n * intensity: 0.8,\n * startTime: Date.now(),\n * }]);\n * };\n *\n * <BloodParticles3D\n * effects={bloodEffects}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onEffectComplete={(id) => {\n * setBloodEffects(prev => prev.filter(e => e.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodParticles3D: React.FC<BloodParticles3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const particlesRef = useRef<Map<string, BloodParticle[]>>(new Map());\n const poolParticlesRef = useRef<BloodParticle[]>([]);\n const pointsRef = useRef<THREE.Points>(null);\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const maxParticles = isMobile\n ? BLOOD_CONSTANTS.MAX_PARTICLES_MOBILE\n : BLOOD_CONSTANTS.MAX_PARTICLES_DESKTOP;\n\n const bloodColor = useMemo(() => KOREAN_COLORS.BLOODLOSS_INDICATOR, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n effects.forEach((effect) => {\n if (!particlesRef.current.has(effect.id)) {\n const particles = generateBloodParticles(effect, maxParticles);\n particlesRef.current.set(effect.id, particles);\n }\n });\n\n const effectIds = new Set(effects.map((e) => e.id));\n particlesRef.current.forEach((particles, id) => {\n if (!effectIds.has(id)) {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n particlesRef.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n }, [effects, enabled, maxParticles]);\n\n React.useEffect(() => {\n const currentParticles = particlesRef.current;\n const currentPoolParticles = poolParticlesRef.current;\n const currentCompletedEffects = completedEffectsRef.current;\n \n return () => {\n currentParticles.forEach((particles) => {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n });\n \n currentPoolParticles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n \n poolParticlesRef.current = [];\n \n currentParticles.clear();\n currentCompletedEffects.clear();\n };\n }, []);\n\n const initialPositions = useMemo(() => {\n const positions = new Float32Array(maxParticles * 3);\n return positions;\n }, [maxParticles]);\n\n useFrame((_, delta) => {\n if (!enabled || !pointsRef.current) return;\n\n const safeDelta = Math.min(delta, BLOOD_CONSTANTS.MAX_DELTA);\n\n let totalParticleIndex = 0;\n const attr = pointsRef.current.geometry.attributes.position;\n const posArray = attr.array as Float32Array;\n\n particlesRef.current.forEach((particles, effectId) => {\n let hasActiveParticles = false;\n\n for (let i = 0; i < particles.length; i++) {\n const p = particles[i];\n p.age += safeDelta;\n\n if (!p.settled) {\n p.velocity.y += BLOOD_CONSTANTS.GRAVITY * safeDelta;\n\n p.position.addScaledVector(p.velocity, safeDelta);\n\n if (p.position.y <= BLOOD_CONSTANTS.FLOOR_Y) {\n p.position.y = BLOOD_CONSTANTS.FLOOR_Y;\n p.velocity.set(0, 0, 0);\n p.settled = true;\n p.lifetime = BLOOD_CONSTANTS.POOL_FADE_DURATION;\n p.age = 0;\n\n poolParticlesRef.current.push(p);\n } else {\n hasActiveParticles = true;\n }\n }\n\n if (totalParticleIndex < posArray.length / 3) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n } else if (process.env.NODE_ENV === \"development\") {\n console.warn(\n `BloodParticles3D: Particle buffer full (${posArray.length / 3} particles). ` +\n `Additional particles are not rendered. Consider reducing particle counts or ` +\n `increasing maxParticles if this happens frequently.`\n );\n }\n }\n\n if (!hasActiveParticles && !completedEffectsRef.current.has(effectId)) {\n completedEffectsRef.current.add(effectId);\n onEffectComplete?.(effectId);\n }\n });\n\n const maxVisibleParticles = Math.min(maxParticles, posArray.length / 3);\n\n if (totalParticleIndex >= maxVisibleParticles) {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n \n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n \n return isAlive;\n });\n } else {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n\n if (isAlive && totalParticleIndex < maxVisibleParticles) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n }\n\n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n\n return isAlive;\n });\n }\n\n attr.needsUpdate = true;\n });\n\n if (!enabled || effects.length === 0) {\n return null;\n }\n\n return (\n <Points\n ref={pointsRef}\n positions={initialPositions}\n data-testid=\"blood-particles-3d\"\n >\n <PointMaterial\n color={bloodColor}\n size={BLOOD_CONSTANTS.PARTICLE_SIZE}\n sizeAttenuation\n transparent\n opacity={0.9}\n depthWrite={false}\n blending={THREE.NormalBlending}\n />\n </Points>\n );\n};\n\nexport default BloodParticles3D;\n"],"mappings":"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"}
@@ -131,7 +131,8 @@ var BloodViscosity3D = ({ effects, enabled = true, isMobile = false, onEffectCom
131
131
  colors[i * 3] = tempColor.r;
132
132
  colors[i * 3 + 1] = tempColor.g;
133
133
  colors[i * 3 + 2] = tempColor.b;
134
- sizes[i] = sizeRange.min + Math.random() * (sizeRange.max - sizeRange.min) * effect.intensity;
134
+ const size = sizeRange.min + Math.random() * (sizeRange.max - sizeRange.min) * effect.intensity;
135
+ sizes[i] = size;
135
136
  const speed = velocityRange.min + Math.random() * (velocityRange.max - velocityRange.min);
136
137
  const theta = (Math.random() - .5) * spreadAngle;
137
138
  const phi = Math.random() * Math.PI * 2;
@@ -1 +1 @@
1
- {"version":3,"file":"BloodViscosity3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodViscosity3D.tsx"],"sourcesContent":["/**\n * BloodViscosity3D - Enhanced blood droplets with thicker physics for brutal combat realism\n *\n * Priority #5: Enhanced Blood Viscosity\n * - Thicker droplets with slower fall rate\n * - Variable splatter sizes (gouts vs mist)\n * - Cling/drip physics (stick to surfaces)\n * - Enhanced viscosity simulation\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~85+ per effect to ~4 pooled objects\n * - Pooling strategy:\n * 1. Temporary calculation objects (direction, color, velocity) use pool\n * 2. Owned objects (particle velocities) are cloned from pooled objects\n * 3. All pooled objects released after particle creation\n * - Estimated reduction: \n * - thin: 50 Color + 50 Vector3 = 100 allocations → 4 pooled objects\n * - medium: 80 Color + 80 Vector3 = 160 allocations → 4 pooled objects\n * - thick: 120 Color + 120 Vector3 = 240 allocations → 4 pooled objects\n * - gout: 200 Color + 200 Vector3 = 400 allocations → 4 pooled objects\n *\n * Korean martial arts context:\n * - 절단격 (Cutting strikes) - Thin blood mist\n * - 타격 (Impact strikes) - Medium blood splatter\n * - 관통격 (Penetrating strikes) - Thick blood droplets\n * - 깊은 상처 (Deep wounds) - Large blood gouts\n */\n\nimport React, { useEffect, useMemo } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Viscosity types for blood splatter\n */\nexport type ViscosityType = 'thin' | 'medium' | 'thick' | 'gout';\n\n/**\n * Individual blood viscosity effect\n */\nexport interface BloodViscosityEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly direction: [number, number, number];\n readonly viscosityType: ViscosityType;\n readonly intensity: number; // 0.0-1.0\n readonly startTime: number;\n}\n\n/**\n * Props for BloodViscosity3D component\n */\nexport interface BloodViscosity3DProps {\n readonly effects: readonly BloodViscosityEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst BLOOD_VISCOSITY_CONSTANTS = {\n GRAVITY: -9.8, // m/s²\n AIR_RESISTANCE: 0.94, // Thicker than arterial (0.97) or bone (0.96)\n \n PARTICLE_COUNT: {\n thin: 50, // Mist\n medium: 80, // Normal splatter\n thick: 120, // Heavy droplets\n gout: 200, // Deep wound large gouts\n },\n \n VELOCITY: {\n thin: { min: 2.0, max: 4.0 },\n medium: { min: 1.5, max: 3.0 },\n thick: { min: 1.0, max: 2.5 },\n gout: { min: 0.5, max: 2.0 },\n },\n \n SIZE: {\n thin: { min: 0.02, max: 0.04 },\n medium: { min: 0.04, max: 0.08 },\n thick: { min: 0.06, max: 0.12 },\n gout: { min: 0.10, max: 0.20 },\n },\n \n SPREAD_ANGLE: {\n thin: Math.PI / 3, // 60° wide spray\n medium: Math.PI / 4, // 45° normal\n thick: Math.PI / 6, // 30° narrow\n gout: Math.PI / 8, // 22.5° very narrow\n },\n \n ACTIVE_LIFETIME: 2.5, // Active falling time\n CLING_LIFETIME: 3.0, // Time stuck to ground\n TOTAL_LIFETIME: 5.5, // Total before cleanup\n \n GROUND_LEVEL: 0.1, // y-position for ground contact\n CLING_DAMPING: 0.3, // Velocity reduction on contact\n \n BLOOD_COLOR: 0x8b0000, // Dark red\n \n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * BloodViscosity3D - Enhanced blood droplets with realistic thicker physics\n *\n * Features:\n * - Variable viscosity types (thin mist to thick gouts)\n * - Cling/drip physics when hitting ground\n * - Slower fall rates than arterial spray\n * - Larger droplet sizes\n * - Mobile optimization (50% particles)\n */\nexport const BloodViscosity3D: React.FC<BloodViscosity3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n particles: THREE.Points;\n velocities: THREE.Vector3[];\n startTime: number;\n effect: BloodViscosityEffect;\n clinging: boolean[];\n }\n >\n >(new Map());\n\n const getParticleCount = useMemo(\n () => (viscosityType: ViscosityType) => {\n const baseCount = BLOOD_VISCOSITY_CONSTANTS.PARTICLE_COUNT[viscosityType];\n return isMobile ? Math.floor(baseCount * 0.5) : baseCount;\n },\n [isMobile]\n );\n\n const createBloodParticles = useMemo(\n () => (effect: BloodViscosityEffect) => {\n const count = getParticleCount(effect.viscosityType);\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(count * 3);\n const colors = new Float32Array(count * 3);\n const sizes = new Float32Array(count);\n const velocities: THREE.Vector3[] = [];\n const clinging: boolean[] = [];\n\n const tempDirection = ThreeObjectPools.vector3.acquire();\n const tempColor = ThreeObjectPools.color.acquire();\n const tempVelocity = ThreeObjectPools.vector3.acquire();\n const tempDeviation = ThreeObjectPools.vector3.acquire();\n\n try {\n tempDirection.set(...effect.direction).normalize();\n const spreadAngle = BLOOD_VISCOSITY_CONSTANTS.SPREAD_ANGLE[effect.viscosityType];\n const velocityRange = BLOOD_VISCOSITY_CONSTANTS.VELOCITY[effect.viscosityType];\n const sizeRange = BLOOD_VISCOSITY_CONSTANTS.SIZE[effect.viscosityType];\n\n tempColor.set(BLOOD_VISCOSITY_CONSTANTS.BLOOD_COLOR);\n\n for (let i = 0; i < count; i++) {\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const size =\n sizeRange.min +\n Math.random() * (sizeRange.max - sizeRange.min) *\n effect.intensity;\n sizes[i] = size;\n\n const speed =\n velocityRange.min +\n Math.random() * (velocityRange.max - velocityRange.min);\n\n const theta = (Math.random() - 0.5) * spreadAngle;\n const phi = Math.random() * Math.PI * 2;\n\n tempDeviation.set(\n Math.sin(theta) * Math.cos(phi),\n Math.sin(theta) * Math.sin(phi),\n Math.cos(theta)\n );\n\n tempVelocity.copy(tempDirection)\n .add(tempDeviation)\n .normalize()\n .multiplyScalar(speed);\n\n velocities.push(tempVelocity.clone());\n clinging.push(false);\n }\n } finally {\n ThreeObjectPools.vector3.release(tempDirection);\n ThreeObjectPools.color.release(tempColor);\n ThreeObjectPools.vector3.release(tempVelocity);\n ThreeObjectPools.vector3.release(tempDeviation);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.08,\n vertexColors: true,\n transparent: true,\n opacity: 0.9,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return { points, velocities, clinging };\n },\n [getParticleCount]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const { points, velocities, clinging } = createBloodParticles(effect);\n updated.set(effect.id, {\n particles: points,\n velocities,\n startTime: effect.startTime,\n effect,\n clinging,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.particles.geometry.dispose();\n (instance.particles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createBloodParticles]);\n\n useFrame((_state, delta) => {\n if (!enabled || effectInstances.size === 0) return;\n\n const clampedDelta = Math.min(delta, BLOOD_VISCOSITY_CONSTANTS.MAX_DELTA);\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.TOTAL_LIFETIME) {\n completedIds.push(id);\n return;\n }\n\n const geometry = instance.particles.geometry;\n const positions = geometry.attributes.position.array as Float32Array;\n const count = positions.length / 3;\n\n for (let i = 0; i < count; i++) {\n const idx = i * 3;\n\n if (!instance.clinging[i]) {\n instance.velocities[i].y +=\n BLOOD_VISCOSITY_CONSTANTS.GRAVITY * clampedDelta;\n\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.AIR_RESISTANCE\n );\n\n positions[idx] += instance.velocities[i].x * clampedDelta;\n positions[idx + 1] += instance.velocities[i].y * clampedDelta;\n positions[idx + 2] += instance.velocities[i].z * clampedDelta;\n\n if (positions[idx + 1] <= BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL) {\n positions[idx + 1] = BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL;\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.CLING_DAMPING\n );\n instance.clinging[i] = true;\n }\n }\n }\n\n geometry.attributes.position.needsUpdate = true;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) {\n const fadeProgress =\n (elapsed - BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) /\n BLOOD_VISCOSITY_CONSTANTS.CLING_LIFETIME;\n const material = instance.particles.material as THREE.PointsMaterial;\n material.opacity = 0.9 * (1.0 - fadeProgress);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <primitive key={instance.effect.id} object={instance.particles} />\n ))}\n </>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"BloodViscosity3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodViscosity3D.tsx"],"sourcesContent":["/**\n * BloodViscosity3D - Enhanced blood droplets with thicker physics for brutal combat realism\n *\n * Priority #5: Enhanced Blood Viscosity\n * - Thicker droplets with slower fall rate\n * - Variable splatter sizes (gouts vs mist)\n * - Cling/drip physics (stick to surfaces)\n * - Enhanced viscosity simulation\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~85+ per effect to ~4 pooled objects\n * - Pooling strategy:\n * 1. Temporary calculation objects (direction, color, velocity) use pool\n * 2. Owned objects (particle velocities) are cloned from pooled objects\n * 3. All pooled objects released after particle creation\n * - Estimated reduction: \n * - thin: 50 Color + 50 Vector3 = 100 allocations → 4 pooled objects\n * - medium: 80 Color + 80 Vector3 = 160 allocations → 4 pooled objects\n * - thick: 120 Color + 120 Vector3 = 240 allocations → 4 pooled objects\n * - gout: 200 Color + 200 Vector3 = 400 allocations → 4 pooled objects\n *\n * Korean martial arts context:\n * - 절단격 (Cutting strikes) - Thin blood mist\n * - 타격 (Impact strikes) - Medium blood splatter\n * - 관통격 (Penetrating strikes) - Thick blood droplets\n * - 깊은 상처 (Deep wounds) - Large blood gouts\n */\n\nimport React, { useEffect, useMemo } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Viscosity types for blood splatter\n */\nexport type ViscosityType = 'thin' | 'medium' | 'thick' | 'gout';\n\n/**\n * Individual blood viscosity effect\n */\nexport interface BloodViscosityEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly direction: [number, number, number];\n readonly viscosityType: ViscosityType;\n readonly intensity: number; // 0.0-1.0\n readonly startTime: number;\n}\n\n/**\n * Props for BloodViscosity3D component\n */\nexport interface BloodViscosity3DProps {\n readonly effects: readonly BloodViscosityEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst BLOOD_VISCOSITY_CONSTANTS = {\n GRAVITY: -9.8, // m/s²\n AIR_RESISTANCE: 0.94, // Thicker than arterial (0.97) or bone (0.96)\n \n PARTICLE_COUNT: {\n thin: 50, // Mist\n medium: 80, // Normal splatter\n thick: 120, // Heavy droplets\n gout: 200, // Deep wound large gouts\n },\n \n VELOCITY: {\n thin: { min: 2.0, max: 4.0 },\n medium: { min: 1.5, max: 3.0 },\n thick: { min: 1.0, max: 2.5 },\n gout: { min: 0.5, max: 2.0 },\n },\n \n SIZE: {\n thin: { min: 0.02, max: 0.04 },\n medium: { min: 0.04, max: 0.08 },\n thick: { min: 0.06, max: 0.12 },\n gout: { min: 0.10, max: 0.20 },\n },\n \n SPREAD_ANGLE: {\n thin: Math.PI / 3, // 60° wide spray\n medium: Math.PI / 4, // 45° normal\n thick: Math.PI / 6, // 30° narrow\n gout: Math.PI / 8, // 22.5° very narrow\n },\n \n ACTIVE_LIFETIME: 2.5, // Active falling time\n CLING_LIFETIME: 3.0, // Time stuck to ground\n TOTAL_LIFETIME: 5.5, // Total before cleanup\n \n GROUND_LEVEL: 0.1, // y-position for ground contact\n CLING_DAMPING: 0.3, // Velocity reduction on contact\n \n BLOOD_COLOR: 0x8b0000, // Dark red\n \n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * BloodViscosity3D - Enhanced blood droplets with realistic thicker physics\n *\n * Features:\n * - Variable viscosity types (thin mist to thick gouts)\n * - Cling/drip physics when hitting ground\n * - Slower fall rates than arterial spray\n * - Larger droplet sizes\n * - Mobile optimization (50% particles)\n */\nexport const BloodViscosity3D: React.FC<BloodViscosity3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n particles: THREE.Points;\n velocities: THREE.Vector3[];\n startTime: number;\n effect: BloodViscosityEffect;\n clinging: boolean[];\n }\n >\n >(new Map());\n\n const getParticleCount = useMemo(\n () => (viscosityType: ViscosityType) => {\n const baseCount = BLOOD_VISCOSITY_CONSTANTS.PARTICLE_COUNT[viscosityType];\n return isMobile ? Math.floor(baseCount * 0.5) : baseCount;\n },\n [isMobile]\n );\n\n const createBloodParticles = useMemo(\n () => (effect: BloodViscosityEffect) => {\n const count = getParticleCount(effect.viscosityType);\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(count * 3);\n const colors = new Float32Array(count * 3);\n const sizes = new Float32Array(count);\n const velocities: THREE.Vector3[] = [];\n const clinging: boolean[] = [];\n\n const tempDirection = ThreeObjectPools.vector3.acquire();\n const tempColor = ThreeObjectPools.color.acquire();\n const tempVelocity = ThreeObjectPools.vector3.acquire();\n const tempDeviation = ThreeObjectPools.vector3.acquire();\n\n try {\n tempDirection.set(...effect.direction).normalize();\n const spreadAngle = BLOOD_VISCOSITY_CONSTANTS.SPREAD_ANGLE[effect.viscosityType];\n const velocityRange = BLOOD_VISCOSITY_CONSTANTS.VELOCITY[effect.viscosityType];\n const sizeRange = BLOOD_VISCOSITY_CONSTANTS.SIZE[effect.viscosityType];\n\n tempColor.set(BLOOD_VISCOSITY_CONSTANTS.BLOOD_COLOR);\n\n for (let i = 0; i < count; i++) {\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const size =\n sizeRange.min +\n Math.random() * (sizeRange.max - sizeRange.min) *\n effect.intensity;\n sizes[i] = size;\n\n const speed =\n velocityRange.min +\n Math.random() * (velocityRange.max - velocityRange.min);\n\n const theta = (Math.random() - 0.5) * spreadAngle;\n const phi = Math.random() * Math.PI * 2;\n\n tempDeviation.set(\n Math.sin(theta) * Math.cos(phi),\n Math.sin(theta) * Math.sin(phi),\n Math.cos(theta)\n );\n\n tempVelocity.copy(tempDirection)\n .add(tempDeviation)\n .normalize()\n .multiplyScalar(speed);\n\n velocities.push(tempVelocity.clone());\n clinging.push(false);\n }\n } finally {\n ThreeObjectPools.vector3.release(tempDirection);\n ThreeObjectPools.color.release(tempColor);\n ThreeObjectPools.vector3.release(tempVelocity);\n ThreeObjectPools.vector3.release(tempDeviation);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.08,\n vertexColors: true,\n transparent: true,\n opacity: 0.9,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return { points, velocities, clinging };\n },\n [getParticleCount]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const { points, velocities, clinging } = createBloodParticles(effect);\n updated.set(effect.id, {\n particles: points,\n velocities,\n startTime: effect.startTime,\n effect,\n clinging,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.particles.geometry.dispose();\n (instance.particles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createBloodParticles]);\n\n useFrame((_state, delta) => {\n if (!enabled || effectInstances.size === 0) return;\n\n const clampedDelta = Math.min(delta, BLOOD_VISCOSITY_CONSTANTS.MAX_DELTA);\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.TOTAL_LIFETIME) {\n completedIds.push(id);\n return;\n }\n\n const geometry = instance.particles.geometry;\n const positions = geometry.attributes.position.array as Float32Array;\n const count = positions.length / 3;\n\n for (let i = 0; i < count; i++) {\n const idx = i * 3;\n\n if (!instance.clinging[i]) {\n instance.velocities[i].y +=\n BLOOD_VISCOSITY_CONSTANTS.GRAVITY * clampedDelta;\n\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.AIR_RESISTANCE\n );\n\n positions[idx] += instance.velocities[i].x * clampedDelta;\n positions[idx + 1] += instance.velocities[i].y * clampedDelta;\n positions[idx + 2] += instance.velocities[i].z * clampedDelta;\n\n if (positions[idx + 1] <= BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL) {\n positions[idx + 1] = BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL;\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.CLING_DAMPING\n );\n instance.clinging[i] = true;\n }\n }\n }\n\n geometry.attributes.position.needsUpdate = true;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) {\n const fadeProgress =\n (elapsed - BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) /\n BLOOD_VISCOSITY_CONSTANTS.CLING_LIFETIME;\n const material = instance.particles.material as THREE.PointsMaterial;\n material.opacity = 0.9 * (1.0 - fadeProgress);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <primitive key={instance.effect.id} object={instance.particles} />\n ))}\n </>\n );\n};\n"],"mappings":"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"}
@@ -87,7 +87,9 @@ var InternalDamage3D = ({ effects, enabled = true, isMobile = false, onEffectCom
87
87
  colors[i * 3] = tempColor.r;
88
88
  colors[i * 3 + 1] = tempColor.g;
89
89
  colors[i * 3 + 2] = tempColor.b;
90
- sizes[i] = .03 * INTERNAL_DAMAGE_CONSTANTS.INTENSITY[effect.penetrationDepth];
90
+ const baseSize = .03;
91
+ const depthMultiplier = INTERNAL_DAMAGE_CONSTANTS.INTENSITY[effect.penetrationDepth];
92
+ sizes[i] = baseSize * depthMultiplier;
91
93
  }
92
94
  } finally {
93
95
  ThreeObjectPools.color.release(tempColor);
@@ -1 +1 @@
1
- {"version":3,"file":"InternalDamage3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/InternalDamage3D.tsx"],"sourcesContent":["/**\n * InternalDamage3D - Deep red organ pulses for Korean martial arts organ strikes\n *\n * Priority #4: Internal Damage Visualization\n * - Deep red organ pulses (liver, kidney, spleen, stomach, heart)\n * - Tissue deformation ripples\n * - Penetration depth visualization\n * - Critical organ damage feedback\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~120+ per effect to 2 pooled objects\n * - Pooling strategy:\n * 1. Temporary Color objects for particle initialization use pool\n * 2. Colors are copied to Float32Array (no ownership needed)\n * 3. All pooled objects released after particle system creation\n * - Estimated reduction:\n * - Pulse particles: 60 Color allocations → 1 pooled object\n * - Ripple particles: 30 Color allocations → 1 pooled object\n * - Total: ~90 Color allocations per effect → 2 pooled objects\n *\n * Korean martial arts context (내장 공격 - Internal organ strikes):\n * - 간격 (Liver strike) - Right side body blow\n * - 신장격 (Kidney strike) - Lower back strike\n * - 비장격 (Spleen strike) - Left side body blow\n * - 명치격 (Solar plexus) - Stomach strike\n * - 심장격 (Heart strike) - Chest strike (critical)\n */\n\nimport React, { useEffect, useMemo, useRef } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Organ types for Korean martial arts internal strikes\n */\nexport type OrganType = 'liver' | 'kidney' | 'spleen' | 'stomach' | 'heart';\n\n/**\n * Penetration depth levels for organ damage\n */\nexport type PenetrationDepth = 'surface' | 'shallow' | 'deep' | 'critical';\n\n/**\n * Individual internal damage effect\n */\nexport interface InternalDamageEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly organType: OrganType;\n readonly penetrationDepth: PenetrationDepth;\n readonly startTime: number;\n}\n\n/**\n * Props for InternalDamage3D component\n */\nexport interface InternalDamage3DProps {\n readonly effects: readonly InternalDamageEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst INTERNAL_DAMAGE_CONSTANTS = {\n PULSE_SPEED: 2.5, // m/s expansion rate\n \n PULSE_LIFETIME: 1.5, // seconds for pulse to complete\n RIPPLE_LIFETIME: 0.8, // seconds for tissue ripple\n \n MAX_RADIUS: {\n surface: 0.4,\n shallow: 0.7,\n deep: 1.0,\n critical: 1.2,\n },\n \n INTENSITY: {\n surface: 0.5,\n shallow: 0.8,\n deep: 1.2,\n critical: 1.5,\n },\n \n PULSE_PARTICLES: 60,\n RIPPLE_PARTICLES: 30,\n \n ORGAN_COLOR: 0x8b0000, // Dark red for organs\n RIPPLE_COLOR: 0xdc143c, // Crimson for tissue ripples\n \n EMISSIVE_INTENSITY: 0.8,\n} as const;\n\n/**\n * InternalDamage3D - Visualizes internal organ damage with deep red pulses\n *\n * Features:\n * - Expanding sphere pulses from organ impacts\n * - Tissue deformation ripples\n * - Penetration depth-based sizing\n * - Korean martial arts organ strike feedback\n * - Mobile optimization (50% particles)\n */\nexport const InternalDamage3D: React.FC<InternalDamage3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >\n >(new Map());\n\n const particleCounts = useMemo(() => {\n const pulseCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES;\n const rippleCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES;\n return { pulseCount, rippleCount };\n }, [isMobile]);\n\n const createPulseParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { pulseCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(pulseCount * 3);\n const colors = new Float32Array(pulseCount * 3);\n const sizes = new Float32Array(pulseCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.ORGAN_COLOR);\n\n for (let i = 0; i < pulseCount; i++) {\n\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const baseSize = 0.03;\n const depthMultiplier =\n INTERNAL_DAMAGE_CONSTANTS.INTENSITY[effect.penetrationDepth];\n sizes[i] = baseSize * depthMultiplier;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.05,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData = { positions: positions.slice() };\n for (let i = 0; i < pulseCount; i++) {\n const phi = Math.acos(1 - 2 * (i + 0.5) / pulseCount);\n const theta = Math.PI * (1 + Math.sqrt(5)) * i;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`phi_${i}`] = phi;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`theta_${i}`] = theta;\n }\n\n return points;\n },\n [particleCounts]\n );\n\n const createRippleParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { rippleCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(rippleCount * 3);\n const colors = new Float32Array(rippleCount * 3);\n const sizes = new Float32Array(rippleCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_COLOR);\n\n for (let i = 0; i < rippleCount; i++) {\n const angle = (i / rippleCount) * Math.PI * 2;\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n sizes[i] = 0.02;\n\n (geometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`] = angle;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.03,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return points;\n },\n [particleCounts]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const pulseParticles = createPulseParticles(effect);\n const rippleParticles = createRippleParticles(effect);\n updated.set(effect.id, {\n pulseParticles,\n rippleParticles,\n startTime: effect.startTime,\n effect,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createPulseParticles, createRippleParticles]);\n\n const effectInstancesRef = useRef<Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >>(effectInstances);\n useEffect(() => {\n effectInstancesRef.current = effectInstances;\n }, [effectInstances]);\n\n useEffect(() => {\n return () => {\n effectInstancesRef.current.forEach((instance: {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }) => {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n });\n };\n }, []); // Empty deps - cleanup on unmount only\n\n useFrame(() => {\n if (!enabled || effectInstances.size === 0) return;\n\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n const { effect } = instance;\n\n const pulseProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME,\n 1.0\n );\n if (pulseProgress < 1.0) {\n const pulseGeometry = instance.pulseParticles.geometry;\n const pulsePositions = pulseGeometry.attributes.position\n .array as Float32Array;\n const maxRadius =\n INTERNAL_DAMAGE_CONSTANTS.MAX_RADIUS[effect.penetrationDepth];\n\n const { pulseCount } = particleCounts;\n const sphereData = (pulseGeometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData;\n for (let i = 0; i < pulseCount; i++) {\n const phi = sphereData[`phi_${i}`] as number;\n const theta = sphereData[`theta_${i}`] as number;\n const radius = maxRadius * pulseProgress;\n\n pulsePositions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);\n pulsePositions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);\n pulsePositions[i * 3 + 2] = radius * Math.cos(phi);\n }\n\n pulseGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.pulseParticles.material as THREE.PointsMaterial;\n material.opacity = 1.0 - pulseProgress * 0.7;\n }\n\n const rippleProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME,\n 1.0\n );\n if (rippleProgress < 1.0) {\n const rippleGeometry = instance.rippleParticles.geometry;\n const ripplePositions = rippleGeometry.attributes.position\n .array as Float32Array;\n\n const { rippleCount } = particleCounts;\n const rippleRadius = 0.6 * rippleProgress;\n for (let i = 0; i < rippleCount; i++) {\n const angle = (rippleGeometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`];\n ripplePositions[i * 3] = rippleRadius * Math.cos(angle);\n ripplePositions[i * 3 + 1] = 0;\n ripplePositions[i * 3 + 2] = rippleRadius * Math.sin(angle);\n }\n\n rippleGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.rippleParticles\n .material as THREE.PointsMaterial;\n material.opacity = 1.0 - rippleProgress;\n }\n\n if (\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME &&\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME\n ) {\n completedIds.push(id);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <React.Fragment key={instance.effect.id}>\n <primitive object={instance.pulseParticles} />\n <primitive object={instance.rippleParticles} />\n </React.Fragment>\n ))}\n </>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"InternalDamage3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/InternalDamage3D.tsx"],"sourcesContent":["/**\n * InternalDamage3D - Deep red organ pulses for Korean martial arts organ strikes\n *\n * Priority #4: Internal Damage Visualization\n * - Deep red organ pulses (liver, kidney, spleen, stomach, heart)\n * - Tissue deformation ripples\n * - Penetration depth visualization\n * - Critical organ damage feedback\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~120+ per effect to 2 pooled objects\n * - Pooling strategy:\n * 1. Temporary Color objects for particle initialization use pool\n * 2. Colors are copied to Float32Array (no ownership needed)\n * 3. All pooled objects released after particle system creation\n * - Estimated reduction:\n * - Pulse particles: 60 Color allocations → 1 pooled object\n * - Ripple particles: 30 Color allocations → 1 pooled object\n * - Total: ~90 Color allocations per effect → 2 pooled objects\n *\n * Korean martial arts context (내장 공격 - Internal organ strikes):\n * - 간격 (Liver strike) - Right side body blow\n * - 신장격 (Kidney strike) - Lower back strike\n * - 비장격 (Spleen strike) - Left side body blow\n * - 명치격 (Solar plexus) - Stomach strike\n * - 심장격 (Heart strike) - Chest strike (critical)\n */\n\nimport React, { useEffect, useMemo, useRef } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Organ types for Korean martial arts internal strikes\n */\nexport type OrganType = 'liver' | 'kidney' | 'spleen' | 'stomach' | 'heart';\n\n/**\n * Penetration depth levels for organ damage\n */\nexport type PenetrationDepth = 'surface' | 'shallow' | 'deep' | 'critical';\n\n/**\n * Individual internal damage effect\n */\nexport interface InternalDamageEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly organType: OrganType;\n readonly penetrationDepth: PenetrationDepth;\n readonly startTime: number;\n}\n\n/**\n * Props for InternalDamage3D component\n */\nexport interface InternalDamage3DProps {\n readonly effects: readonly InternalDamageEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst INTERNAL_DAMAGE_CONSTANTS = {\n PULSE_SPEED: 2.5, // m/s expansion rate\n \n PULSE_LIFETIME: 1.5, // seconds for pulse to complete\n RIPPLE_LIFETIME: 0.8, // seconds for tissue ripple\n \n MAX_RADIUS: {\n surface: 0.4,\n shallow: 0.7,\n deep: 1.0,\n critical: 1.2,\n },\n \n INTENSITY: {\n surface: 0.5,\n shallow: 0.8,\n deep: 1.2,\n critical: 1.5,\n },\n \n PULSE_PARTICLES: 60,\n RIPPLE_PARTICLES: 30,\n \n ORGAN_COLOR: 0x8b0000, // Dark red for organs\n RIPPLE_COLOR: 0xdc143c, // Crimson for tissue ripples\n \n EMISSIVE_INTENSITY: 0.8,\n} as const;\n\n/**\n * InternalDamage3D - Visualizes internal organ damage with deep red pulses\n *\n * Features:\n * - Expanding sphere pulses from organ impacts\n * - Tissue deformation ripples\n * - Penetration depth-based sizing\n * - Korean martial arts organ strike feedback\n * - Mobile optimization (50% particles)\n */\nexport const InternalDamage3D: React.FC<InternalDamage3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >\n >(new Map());\n\n const particleCounts = useMemo(() => {\n const pulseCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES;\n const rippleCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES;\n return { pulseCount, rippleCount };\n }, [isMobile]);\n\n const createPulseParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { pulseCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(pulseCount * 3);\n const colors = new Float32Array(pulseCount * 3);\n const sizes = new Float32Array(pulseCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.ORGAN_COLOR);\n\n for (let i = 0; i < pulseCount; i++) {\n\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const baseSize = 0.03;\n const depthMultiplier =\n INTERNAL_DAMAGE_CONSTANTS.INTENSITY[effect.penetrationDepth];\n sizes[i] = baseSize * depthMultiplier;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.05,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData = { positions: positions.slice() };\n for (let i = 0; i < pulseCount; i++) {\n const phi = Math.acos(1 - 2 * (i + 0.5) / pulseCount);\n const theta = Math.PI * (1 + Math.sqrt(5)) * i;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`phi_${i}`] = phi;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`theta_${i}`] = theta;\n }\n\n return points;\n },\n [particleCounts]\n );\n\n const createRippleParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { rippleCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(rippleCount * 3);\n const colors = new Float32Array(rippleCount * 3);\n const sizes = new Float32Array(rippleCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_COLOR);\n\n for (let i = 0; i < rippleCount; i++) {\n const angle = (i / rippleCount) * Math.PI * 2;\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n sizes[i] = 0.02;\n\n (geometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`] = angle;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.03,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return points;\n },\n [particleCounts]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const pulseParticles = createPulseParticles(effect);\n const rippleParticles = createRippleParticles(effect);\n updated.set(effect.id, {\n pulseParticles,\n rippleParticles,\n startTime: effect.startTime,\n effect,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createPulseParticles, createRippleParticles]);\n\n const effectInstancesRef = useRef<Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >>(effectInstances);\n useEffect(() => {\n effectInstancesRef.current = effectInstances;\n }, [effectInstances]);\n\n useEffect(() => {\n return () => {\n effectInstancesRef.current.forEach((instance: {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }) => {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n });\n };\n }, []); // Empty deps - cleanup on unmount only\n\n useFrame(() => {\n if (!enabled || effectInstances.size === 0) return;\n\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n const { effect } = instance;\n\n const pulseProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME,\n 1.0\n );\n if (pulseProgress < 1.0) {\n const pulseGeometry = instance.pulseParticles.geometry;\n const pulsePositions = pulseGeometry.attributes.position\n .array as Float32Array;\n const maxRadius =\n INTERNAL_DAMAGE_CONSTANTS.MAX_RADIUS[effect.penetrationDepth];\n\n const { pulseCount } = particleCounts;\n const sphereData = (pulseGeometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData;\n for (let i = 0; i < pulseCount; i++) {\n const phi = sphereData[`phi_${i}`] as number;\n const theta = sphereData[`theta_${i}`] as number;\n const radius = maxRadius * pulseProgress;\n\n pulsePositions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);\n pulsePositions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);\n pulsePositions[i * 3 + 2] = radius * Math.cos(phi);\n }\n\n pulseGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.pulseParticles.material as THREE.PointsMaterial;\n material.opacity = 1.0 - pulseProgress * 0.7;\n }\n\n const rippleProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME,\n 1.0\n );\n if (rippleProgress < 1.0) {\n const rippleGeometry = instance.rippleParticles.geometry;\n const ripplePositions = rippleGeometry.attributes.position\n .array as Float32Array;\n\n const { rippleCount } = particleCounts;\n const rippleRadius = 0.6 * rippleProgress;\n for (let i = 0; i < rippleCount; i++) {\n const angle = (rippleGeometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`];\n ripplePositions[i * 3] = rippleRadius * Math.cos(angle);\n ripplePositions[i * 3 + 1] = 0;\n ripplePositions[i * 3 + 2] = rippleRadius * Math.sin(angle);\n }\n\n rippleGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.rippleParticles\n .material as THREE.PointsMaterial;\n material.opacity = 1.0 - rippleProgress;\n }\n\n if (\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME &&\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME\n ) {\n completedIds.push(id);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <React.Fragment key={instance.effect.id}>\n <primitive object={instance.pulseParticles} />\n <primitive object={instance.rippleParticles} />\n </React.Fragment>\n ))}\n </>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,IAAM,4BAA4B;CAChC,aAAa;CAEb,gBAAgB;CAChB,iBAAiB;CAEjB,YAAY;EACV,SAAS;EACT,SAAS;EACT,MAAM;EACN,UAAU;CACZ;CAEA,WAAW;EACT,SAAS;EACT,SAAS;EACT,MAAM;EACN,UAAU;CACZ;CAEA,iBAAiB;CACjB,kBAAkB;CAElB,aAAa;CACb,cAAc;CAEd,oBAAoB;AACtB;;;;;;;;;;;AAYA,IAAa,oBAAqD,EAChE,SACA,UAAU,MACV,WAAW,OACX,uBACI;CACJ,MAAM,CAAC,iBAAiB,sBAAsB,MAAM,yBAUlD,IAAI,IAAI,CAAC;CAEX,MAAM,iBAAiB,cAAc;EAOnC,OAAO;GAAE,YANU,WACf,KAAK,MAAM,0BAA0B,kBAAkB,EAAG,IAC1D,0BAA0B;GAIT,aAHD,WAChB,KAAK,MAAM,0BAA0B,mBAAmB,EAAG,IAC3D,0BAA0B;EACG;CACnC,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,uBAAuB,eACpB,WAAiC;EACtC,MAAM,EAAE,eAAe;EACvB,MAAM,WAAW,IAAI,MAAM,eAAe;EAC1C,MAAM,YAAY,IAAI,aAAa,aAAa,CAAC;EACjD,MAAM,SAAS,IAAI,aAAa,aAAa,CAAC;EAC9C,MAAM,QAAQ,IAAI,aAAa,UAAU;EAEzC,MAAM,YAAY,iBAAiB,MAAM,QAAQ;EAEjD,IAAI;GACF,UAAU,IAAI,0BAA0B,WAAW;GAEnD,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK;IAEnC,UAAU,IAAI,KAAK;IACnB,UAAU,IAAI,IAAI,KAAK;IACvB,UAAU,IAAI,IAAI,KAAK;IAEvB,OAAO,IAAI,KAAK,UAAU;IAC1B,OAAO,IAAI,IAAI,KAAK,UAAU;IAC9B,OAAO,IAAI,IAAI,KAAK,UAAU;IAE9B,MAAM,WAAW;IACjB,MAAM,kBACJ,0BAA0B,UAAU,OAAO;IAC7C,MAAM,KAAK,WAAW;GACxB;EACF,UAAU;GACR,iBAAiB,MAAM,QAAQ,SAAS;EAC1C;EAEA,SAAS,aAAa,YAAY,IAAI,MAAM,gBAAgB,WAAW,CAAC,CAAC;EACzE,SAAS,aAAa,SAAS,IAAI,MAAM,gBAAgB,QAAQ,CAAC,CAAC;EACnE,SAAS,aAAa,QAAQ,IAAI,MAAM,gBAAgB,OAAO,CAAC,CAAC;EAEjE,MAAM,WAAW,IAAI,MAAM,eAAe;GACxC,MAAM;GACN,cAAc;GACd,aAAa;GACb,SAAS;GACT,UAAU,MAAM;GAChB,YAAY;GACZ,iBAAiB;EACnB,CAAC;EAED,MAAM,SAAS,IAAI,MAAM,OAAO,UAAU,QAAQ;EAClD,OAAO,SAAS,IAAI,GAAG,OAAO,QAAQ;EAEtC,SAA2F,aAAa,EAAE,WAAW,UAAU,MAAM,EAAE;EACvI,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK;GACnC,MAAM,MAAM,KAAK,KAAK,IAAI,KAAK,IAAI,MAAO,UAAU;GACpD,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,KAAK,CAAC,KAAK;GAC7C,SAA2F,WAAW,OAAO,OAAO;GACpH,SAA2F,WAAW,SAAS,OAAO;EACxH;EAEA,OAAO;CACT,GACA,CAAC,cAAc,CACjB;CAEA,MAAM,wBAAwB,eACrB,WAAiC;EACtC,MAAM,EAAE,gBAAgB;EACxB,MAAM,WAAW,IAAI,MAAM,eAAe;EAC1C,MAAM,YAAY,IAAI,aAAa,cAAc,CAAC;EAClD,MAAM,SAAS,IAAI,aAAa,cAAc,CAAC;EAC/C,MAAM,QAAQ,IAAI,aAAa,WAAW;EAE1C,MAAM,YAAY,iBAAiB,MAAM,QAAQ;EAEjD,IAAI;GACF,UAAU,IAAI,0BAA0B,YAAY;GAEpD,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,KAAK;IACpC,MAAM,QAAS,IAAI,cAAe,KAAK,KAAK;IAC5C,UAAU,IAAI,KAAK;IACnB,UAAU,IAAI,IAAI,KAAK;IACvB,UAAU,IAAI,IAAI,KAAK;IAEvB,OAAO,IAAI,KAAK,UAAU;IAC1B,OAAO,IAAI,IAAI,KAAK,UAAU;IAC9B,OAAO,IAAI,IAAI,KAAK,UAAU;IAE9B,MAAM,KAAK;IAEX,SAA+D,SAAS,OAAO;GACjF;EACF,UAAU;GACR,iBAAiB,MAAM,QAAQ,SAAS;EAC1C;EAEA,SAAS,aAAa,YAAY,IAAI,MAAM,gBAAgB,WAAW,CAAC,CAAC;EACzE,SAAS,aAAa,SAAS,IAAI,MAAM,gBAAgB,QAAQ,CAAC,CAAC;EACnE,SAAS,aAAa,QAAQ,IAAI,MAAM,gBAAgB,OAAO,CAAC,CAAC;EAEjE,MAAM,WAAW,IAAI,MAAM,eAAe;GACxC,MAAM;GACN,cAAc;GACd,aAAa;GACb,SAAS;GACT,UAAU,MAAM;GAChB,YAAY;GACZ,iBAAiB;EACnB,CAAC;EAED,MAAM,SAAS,IAAI,MAAM,OAAO,UAAU,QAAQ;EAClD,OAAO,SAAS,IAAI,GAAG,OAAO,QAAQ;EAEtC,OAAO;CACT,GACA,CAAC,cAAc,CACjB;CAEA,gBAAgB;EACd,IAAI,CAAC,SAAS;EAEd,oBAAoB,SAAS;GAC3B,MAAM,UAAU,IAAI,IAAI,IAAI;GAE5B,QAAQ,SAAS,WAAW;IAC1B,IAAI,CAAC,QAAQ,IAAI,OAAO,EAAE,GAAG;KAC3B,MAAM,iBAAiB,qBAAqB,MAAM;KAClD,MAAM,kBAAkB,sBAAsB,MAAM;KACpD,QAAQ,IAAI,OAAO,IAAI;MACrB;MACA;MACA,WAAW,OAAO;MAClB;KACF,CAAC;IACH;GACF,CAAC;GAED,MAAM,aAAa,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,EAAE,CAAC;GACnD,MAAM,KAAK,QAAQ,KAAK,CAAC,CAAC,CAAC,SAAS,OAAO;IACzC,IAAI,CAAC,WAAW,IAAI,EAAE,GAAG;KACvB,MAAM,WAAW,QAAQ,IAAI,EAAE;KAC/B,IAAI,UAAU;MACZ,SAAS,eAAe,SAAS,QAAQ;MACzC,SAAU,eAAe,SAA4B,QAAQ;MAC7D,SAAS,gBAAgB,SAAS,QAAQ;MAC1C,SAAU,gBAAgB,SAA4B,QAAQ;KAChE;KACA,QAAQ,OAAO,EAAE;IACnB;GACF,CAAC;GAED,OAAO;EACT,CAAC;CACH,GAAG;EAAC;EAAS;EAAS;EAAsB;CAAqB,CAAC;CAElE,MAAM,qBAAqB,OAQxB,eAAe;CAClB,gBAAgB;EACd,mBAAmB,UAAU;CAC/B,GAAG,CAAC,eAAe,CAAC;CAEpB,gBAAgB;EACd,aAAa;GACX,mBAAmB,QAAQ,SAAS,aAK9B;IACJ,SAAS,eAAe,SAAS,QAAQ;IACzC,SAAU,eAAe,SAA4B,QAAQ;IAC7D,SAAS,gBAAgB,SAAS,QAAQ;IAC1C,SAAU,gBAAgB,SAA4B,QAAQ;GAChE,CAAC;EACH;CACF,GAAG,CAAC,CAAC;CAEL,eAAe;EACb,IAAI,CAAC,WAAW,gBAAgB,SAAS,GAAG;EAE5C,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,eAAyB,CAAC;EAEhC,gBAAgB,SAAS,UAAU,OAAO;GACxC,MAAM,WAAW,MAAM,SAAS,aAAa;GAC7C,MAAM,EAAE,WAAW;GAEnB,MAAM,gBAAgB,KAAK,IACzB,UAAU,0BAA0B,gBACpC,CACF;GACA,IAAI,gBAAgB,GAAK;IACzB,MAAM,gBAAgB,SAAS,eAAe;IAC5C,MAAM,iBAAiB,cAAc,WAAW,SAC7C;IACH,MAAM,YACJ,0BAA0B,WAAW,OAAO;IAE9C,MAAM,EAAE,eAAe;IACvB,MAAM,aAAc,cAA+F;IACnH,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK;KACnC,MAAM,MAAM,WAAW,OAAO;KAC9B,MAAM,QAAQ,WAAW,SAAS;KAClC,MAAM,SAAS,YAAY;KAE3B,eAAe,IAAI,KAAK,SAAS,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,KAAK;KAC/D,eAAe,IAAI,IAAI,KAAK,SAAS,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,KAAK;KACnE,eAAe,IAAI,IAAI,KAAK,SAAS,KAAK,IAAI,GAAG;IACnD;IAEA,cAAc,WAAW,SAAS,cAAc;IAEhD,MAAM,WAAW,SAAS,eAAe;IACzC,SAAS,UAAU,IAAM,gBAAgB;GAC3C;GAEA,MAAM,iBAAiB,KAAK,IAC1B,UAAU,0BAA0B,iBACpC,CACF;GACA,IAAI,iBAAiB,GAAK;IACxB,MAAM,iBAAiB,SAAS,gBAAgB;IAChD,MAAM,kBAAkB,eAAe,WAAW,SAC/C;IAEH,MAAM,EAAE,gBAAgB;IACxB,MAAM,eAAe,KAAM;IAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,KAAK;KACpC,MAAM,QAAS,eAAoE,SAAS;KAC5F,gBAAgB,IAAI,KAAK,eAAe,KAAK,IAAI,KAAK;KACtD,gBAAgB,IAAI,IAAI,KAAK;KAC7B,gBAAgB,IAAI,IAAI,KAAK,eAAe,KAAK,IAAI,KAAK;IAC5D;IAEA,eAAe,WAAW,SAAS,cAAc;IAEjD,MAAM,WAAW,SAAS,gBACvB;IACH,SAAS,UAAU,IAAM;GAC3B;GAEA,IACE,WAAW,0BAA0B,kBACrC,WAAW,0BAA0B,iBAErC,aAAa,KAAK,EAAE;EAExB,CAAC;EAED,aAAa,SAAS,OAAO;GAC3B,mBAAmB,EAAE;EACvB,CAAC;CACH,CAAC;CAED,OACE,oBAAA,UAAA,EAAA,UACG,MAAM,KAAK,gBAAgB,OAAO,CAAC,CAAC,CAAC,KAAK,aACzC,qBAAC,MAAM,UAAP,EAAA,UAAA,CACE,oBAAC,aAAD,EAAW,QAAQ,SAAS,eAAiB,CAAA,GAC7C,oBAAC,aAAD,EAAW,QAAQ,SAAS,gBAAkB,CAAA,CAChC,EAAA,GAHK,SAAS,OAAO,EAGrB,CACjB,EACD,CAAA;AAEN"}
@@ -1 +1 @@
1
- {"version":3,"file":"TraumaOverlay3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/TraumaOverlay3D.tsx"],"sourcesContent":["/**\n * TraumaOverlay3D - Bruising and injury visualization system\n *\n * Renders progressive bruising, cuts, and bone fracture indicators on 3D character models\n * using shader-based texture overlays with color blending. Bruises darken with repeated\n * hits to the same body region and persist across combat rounds.\n *\n * Features:\n * - Progressive bruising (purple/black gradients)\n * - Cut/laceration marks for sharp strikes\n * - Bone fracture indicators at <30% health\n * - Injury persistence across rounds\n * - Korean-themed injury visualization\n *\n * @module components/combat/TraumaOverlay3D\n * @category Combat Effects\n * @korean 외상오버레이3D\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { InjuryType, Injury } from \"../../../../../types/injury\";\n\n/**\n * Props for TraumaOverlay3D component\n */\nexport interface TraumaOverlay3DProps {\n /** Character ID for injury tracking */\n readonly playerId: string;\n /** Current health (0-100) */\n readonly health: number;\n /** Active injuries to visualize */\n readonly injuries: readonly Injury[];\n /** Character position in world space */\n readonly characterPosition: [number, number, number];\n /** Whether character is mobile (simplified visualization) */\n readonly isMobile?: boolean;\n /** Whether to show fracture indicators */\n readonly showFractures?: boolean;\n}\n\nexport { InjuryType };\nexport type { Injury };\n\n/**\n * Color constants for injury visualization\n */\nconst INJURY_COLORS = {\n BRUISE_FRESH: 0x8B0000, // Dark red (fresh bruise)\n BRUISE_OLD: 0x4B0082, // Indigo (aging bruise)\n BRUISE_SEVERE: 0x000000, // Black (severe bruising)\n CUT_COLOR: 0xFF0000, // Bright red (cut)\n FRACTURE_INDICATOR: KOREAN_COLORS.ACCENT_GOLD, // Gold (bone fracture)\n} as const;\n\n/**\n * Get bruise color based on severity and hit count\n */\nconst getBruiseColor = (severity: number, hitCount: number): number => {\n if (hitCount >= 3 || severity > 0.8) {\n return INJURY_COLORS.BRUISE_SEVERE; // Black for severe/repeated trauma\n } else if (hitCount >= 2 || severity > 0.5) {\n return INJURY_COLORS.BRUISE_OLD; // Indigo for moderate bruising\n } else {\n return INJURY_COLORS.BRUISE_FRESH; // Dark red for fresh bruise\n }\n};\n\n/**\n * Get injury size based on severity\n */\nconst getInjurySize = (severity: number): number => {\n return 0.1 + severity * 0.3; // 0.1 to 0.4 units\n};\n\n/**\n * InjuryMarker - Individual injury visualization\n */\nconst InjuryMarker: React.FC<{\n injury: Injury;\n characterPosition: [number, number, number];\n isMobile: boolean;\n}> = ({ injury, characterPosition, isMobile }) => {\n const worldPosition: [number, number, number] = useMemo(() => {\n return [\n characterPosition[0] + injury.position[0],\n characterPosition[1] + injury.position[1],\n characterPosition[2] + injury.position[2],\n ];\n }, [characterPosition, injury.position]);\n\n const size = useMemo(() => getInjurySize(injury.severity), [injury.severity]);\n\n switch (injury.type) {\n case InjuryType.BRUISE: {\n const color = getBruiseColor(injury.severity, injury.hitCount);\n const opacity = Math.min(0.8, 0.3 + injury.severity * 0.5);\n\n return (\n <mesh position={worldPosition} data-testid={`injury-${injury.id}`}>\n <sphereGeometry args={[size, isMobile ? 8 : 16, isMobile ? 8 : 16]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={opacity}\n depthTest={true}\n />\n </mesh>\n );\n }\n\n case InjuryType.CUT:\n case InjuryType.LACERATION: {\n const cutLength = injury.type === InjuryType.LACERATION ? size * 3 : size * 2;\n return (\n <group position={worldPosition} data-testid={`injury-${injury.id}`}>\n {/* Cut mark - thin red line */}\n <mesh>\n <boxGeometry args={[cutLength, 0.02, 0.02]} />\n <meshBasicMaterial\n color={INJURY_COLORS.CUT_COLOR}\n transparent\n opacity={0.9}\n />\n </mesh>\n {/* Blood trail for lacerations */}\n {injury.type === InjuryType.LACERATION && (\n <mesh position={[0, -size * 0.5, 0]}>\n <boxGeometry args={[0.02, size, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.BLOODLOSS_INDICATOR}\n transparent\n opacity={0.7}\n />\n </mesh>\n )}\n </group>\n );\n }\n\n case InjuryType.FRACTURE: {\n return (\n <group position={worldPosition} data-testid={`injury-${injury.id}`}>\n {/* Fracture indicator - pulsing gold ring */}\n <mesh rotation={[-Math.PI / 2, 0, 0]}>\n <ringGeometry args={[size * 0.8, size, 16]} />\n <meshBasicMaterial\n color={INJURY_COLORS.FRACTURE_INDICATOR}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n {/* Cross indicator */}\n <mesh>\n <boxGeometry args={[size * 2, 0.02, 0.02]} />\n <meshBasicMaterial\n color={INJURY_COLORS.FRACTURE_INDICATOR}\n transparent\n opacity={0.8}\n />\n </mesh>\n <mesh rotation={[0, 0, Math.PI / 2]}>\n <boxGeometry args={[size * 2, 0.02, 0.02]} />\n <meshBasicMaterial\n color={INJURY_COLORS.FRACTURE_INDICATOR}\n transparent\n opacity={0.8}\n />\n </mesh>\n </group>\n );\n }\n\n default:\n return null;\n }\n};\n\n/**\n * FractureWarning - Html overlay warning for critical bone damage\n * \n * Rendered as a styled Html overlay with Korean/English text and alert semantics.\n */\nconst FractureWarning: React.FC<{\n health: number;\n isMobile: boolean;\n}> = ({ health, isMobile }) => {\n const warningOpacity = useMemo(() => {\n const rawOpacity = 0.5 + (30 - health) / 60; // 0.5 at 30% health, 1.0 at 0% health\n return Math.min(1, Math.max(0, rawOpacity));\n }, [health]);\n\n if (health >= 30) return null;\n\n return (\n <Html center position={[0, 2.5, 0]}>\n <div\n style={{\n padding: isMobile ? \"8px 12px\" : \"10px 16px\",\n backgroundColor: hexToRgbaString(INJURY_COLORS.FRACTURE_INDICATOR, warningOpacity * 0.3),\n border: `2px solid ${hexToRgbaString(INJURY_COLORS.FRACTURE_INDICATOR, 1)}`,\n borderRadius: \"6px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(INJURY_COLORS.FRACTURE_INDICATOR, 1),\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n animation: \"fracturePulse 1s ease-in-out infinite\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n data-testid=\"fracture-warning\"\n role=\"alert\"\n aria-live=\"assertive\"\n >\n ⚠️ 골절위험 | Bone Fracture Risk\n </div>\n <style>\n {`\n @keyframes fracturePulse {\n 0%, 100% { opacity: 0.7; transform: scale(1); }\n 50% { opacity: 1; transform: scale(1.05); }\n }\n `}\n </style>\n </Html>\n );\n};\n\n/**\n * TraumaOverlay3D Component\n *\n * Visualizes progressive combat trauma including bruising, cuts, and bone damage\n * on 3D character models. Injuries persist across rounds and darken with repeated\n * hits to the same body region.\n *\n * @example\n * ```tsx\n * const [injuries, setInjuries] = useState<Injury[]>([]);\n *\n * // On hit event\n * const handleHit = (region: BodyRegion, position: [number, number, number], type: InjuryType) => {\n * const existingInjury = injuries.find(i => \n * i.region === region && \n * distance(i.position, position) < 0.2\n * );\n *\n * if (existingInjury) {\n * // Progressive bruising - increase hit count\n * setInjuries(prev => prev.map(i => \n * i.id === existingInjury.id \n * ? { ...i, hitCount: i.hitCount + 1, severity: Math.min(1.0, i.severity + 0.2) }\n * : i\n * ));\n * } else {\n * // New injury\n * setInjuries([...injuries, {\n * id: generateId(),\n * region,\n * type,\n * position,\n * severity: 0.5,\n * hitCount: 1,\n * timestamp: Date.now(),\n * }]);\n * }\n * };\n *\n * <TraumaOverlay3D\n * playerId={playerId}\n * health={playerHealth}\n * injuries={injuries}\n * characterPosition={characterPosition}\n * isMobile={isMobile}\n * showFractures={true}\n * />\n * ```\n */\nexport const TraumaOverlay3D: React.FC<TraumaOverlay3DProps> = ({\n playerId,\n health,\n injuries,\n characterPosition,\n isMobile = false,\n showFractures = true,\n}) => {\n const playerInjuries = useMemo(() => {\n if (playerId === null || playerId === undefined) {\n return injuries;\n }\n\n return injuries.filter((injury) => {\n if (injury.playerId === null || injury.playerId === undefined) {\n return true;\n }\n return injury.playerId === playerId;\n });\n }, [injuries, playerId]);\n\n const { fractures, otherInjuries } = useMemo(() => {\n const frac = playerInjuries.filter((i) => i.type === InjuryType.FRACTURE);\n const other = playerInjuries.filter((i) => i.type !== InjuryType.FRACTURE);\n return { fractures: frac, otherInjuries: other };\n }, [playerInjuries]);\n\n return (\n <group data-testid={`trauma-overlay-${playerId}`}>\n {/* Render non-fracture injuries */}\n {otherInjuries.map((injury) => (\n <InjuryMarker\n key={injury.id}\n injury={injury}\n characterPosition={characterPosition}\n isMobile={isMobile}\n />\n ))}\n\n {/* Render fractures if enabled and health is critical */}\n {showFractures &&\n health < 30 &&\n fractures.map((injury) => (\n <InjuryMarker\n key={injury.id}\n injury={injury}\n characterPosition={characterPosition}\n isMobile={isMobile}\n />\n ))}\n\n {/* Fracture warning overlay */}\n {showFractures && health < 30 && fractures.length > 0 && (\n <FractureWarning health={health} isMobile={isMobile} />\n )}\n </group>\n );\n};\n\nexport default TraumaOverlay3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"TraumaOverlay3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/TraumaOverlay3D.tsx"],"sourcesContent":["/**\n * TraumaOverlay3D - Bruising and injury visualization system\n *\n * Renders progressive bruising, cuts, and bone fracture indicators on 3D character models\n * using shader-based texture overlays with color blending. Bruises darken with repeated\n * hits to the same body region and persist across combat rounds.\n *\n * Features:\n * - Progressive bruising (purple/black gradients)\n * - Cut/laceration marks for sharp strikes\n * - Bone fracture indicators at <30% health\n * - Injury persistence across rounds\n * - Korean-themed injury visualization\n *\n * @module components/combat/TraumaOverlay3D\n * @category Combat Effects\n * @korean 외상오버레이3D\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { InjuryType, Injury } from \"../../../../../types/injury\";\n\n/**\n * Props for TraumaOverlay3D component\n */\nexport interface TraumaOverlay3DProps {\n /** Character ID for injury tracking */\n readonly playerId: string;\n /** Current health (0-100) */\n readonly health: number;\n /** Active injuries to visualize */\n readonly injuries: readonly Injury[];\n /** Character position in world space */\n readonly characterPosition: [number, number, number];\n /** Whether character is mobile (simplified visualization) */\n readonly isMobile?: boolean;\n /** Whether to show fracture indicators */\n readonly showFractures?: boolean;\n}\n\nexport { InjuryType };\nexport type { Injury };\n\n/**\n * Color constants for injury visualization\n */\nconst INJURY_COLORS = {\n BRUISE_FRESH: 0x8B0000, // Dark red (fresh bruise)\n BRUISE_OLD: 0x4B0082, // Indigo (aging bruise)\n BRUISE_SEVERE: 0x000000, // Black (severe bruising)\n CUT_COLOR: 0xFF0000, // Bright red (cut)\n FRACTURE_INDICATOR: KOREAN_COLORS.ACCENT_GOLD, // Gold (bone fracture)\n} as const;\n\n/**\n * Get bruise color based on severity and hit count\n */\nconst getBruiseColor = (severity: number, hitCount: number): number => {\n if (hitCount >= 3 || severity > 0.8) {\n return INJURY_COLORS.BRUISE_SEVERE; // Black for severe/repeated trauma\n } else if (hitCount >= 2 || severity > 0.5) {\n return INJURY_COLORS.BRUISE_OLD; // Indigo for moderate bruising\n } else {\n return INJURY_COLORS.BRUISE_FRESH; // Dark red for fresh bruise\n }\n};\n\n/**\n * Get injury size based on severity\n */\nconst getInjurySize = (severity: number): number => {\n return 0.1 + severity * 0.3; // 0.1 to 0.4 units\n};\n\n/**\n * InjuryMarker - Individual injury visualization\n */\nconst InjuryMarker: React.FC<{\n injury: Injury;\n characterPosition: [number, number, number];\n isMobile: boolean;\n}> = ({ injury, characterPosition, isMobile }) => {\n const worldPosition: [number, number, number] = useMemo(() => {\n return [\n characterPosition[0] + injury.position[0],\n characterPosition[1] + injury.position[1],\n characterPosition[2] + injury.position[2],\n ];\n }, [characterPosition, injury.position]);\n\n const size = useMemo(() => getInjurySize(injury.severity), [injury.severity]);\n\n switch (injury.type) {\n case InjuryType.BRUISE: {\n const color = getBruiseColor(injury.severity, injury.hitCount);\n const opacity = Math.min(0.8, 0.3 + injury.severity * 0.5);\n\n return (\n <mesh position={worldPosition} data-testid={`injury-${injury.id}`}>\n <sphereGeometry args={[size, isMobile ? 8 : 16, isMobile ? 8 : 16]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={opacity}\n depthTest={true}\n />\n </mesh>\n );\n }\n\n case InjuryType.CUT:\n case InjuryType.LACERATION: {\n const cutLength = injury.type === InjuryType.LACERATION ? size * 3 : size * 2;\n return (\n <group position={worldPosition} data-testid={`injury-${injury.id}`}>\n {/* Cut mark - thin red line */}\n <mesh>\n <boxGeometry args={[cutLength, 0.02, 0.02]} />\n <meshBasicMaterial\n color={INJURY_COLORS.CUT_COLOR}\n transparent\n opacity={0.9}\n />\n </mesh>\n {/* Blood trail for lacerations */}\n {injury.type === InjuryType.LACERATION && (\n <mesh position={[0, -size * 0.5, 0]}>\n <boxGeometry args={[0.02, size, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.BLOODLOSS_INDICATOR}\n transparent\n opacity={0.7}\n />\n </mesh>\n )}\n </group>\n );\n }\n\n case InjuryType.FRACTURE: {\n return (\n <group position={worldPosition} data-testid={`injury-${injury.id}`}>\n {/* Fracture indicator - pulsing gold ring */}\n <mesh rotation={[-Math.PI / 2, 0, 0]}>\n <ringGeometry args={[size * 0.8, size, 16]} />\n <meshBasicMaterial\n color={INJURY_COLORS.FRACTURE_INDICATOR}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n {/* Cross indicator */}\n <mesh>\n <boxGeometry args={[size * 2, 0.02, 0.02]} />\n <meshBasicMaterial\n color={INJURY_COLORS.FRACTURE_INDICATOR}\n transparent\n opacity={0.8}\n />\n </mesh>\n <mesh rotation={[0, 0, Math.PI / 2]}>\n <boxGeometry args={[size * 2, 0.02, 0.02]} />\n <meshBasicMaterial\n color={INJURY_COLORS.FRACTURE_INDICATOR}\n transparent\n opacity={0.8}\n />\n </mesh>\n </group>\n );\n }\n\n default:\n return null;\n }\n};\n\n/**\n * FractureWarning - Html overlay warning for critical bone damage\n * \n * Rendered as a styled Html overlay with Korean/English text and alert semantics.\n */\nconst FractureWarning: React.FC<{\n health: number;\n isMobile: boolean;\n}> = ({ health, isMobile }) => {\n const warningOpacity = useMemo(() => {\n const rawOpacity = 0.5 + (30 - health) / 60; // 0.5 at 30% health, 1.0 at 0% health\n return Math.min(1, Math.max(0, rawOpacity));\n }, [health]);\n\n if (health >= 30) return null;\n\n return (\n <Html center position={[0, 2.5, 0]}>\n <div\n style={{\n padding: isMobile ? \"8px 12px\" : \"10px 16px\",\n backgroundColor: hexToRgbaString(INJURY_COLORS.FRACTURE_INDICATOR, warningOpacity * 0.3),\n border: `2px solid ${hexToRgbaString(INJURY_COLORS.FRACTURE_INDICATOR, 1)}`,\n borderRadius: \"6px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: hexToRgbaString(INJURY_COLORS.FRACTURE_INDICATOR, 1),\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n animation: \"fracturePulse 1s ease-in-out infinite\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n data-testid=\"fracture-warning\"\n role=\"alert\"\n aria-live=\"assertive\"\n >\n ⚠️ 골절위험 | Bone Fracture Risk\n </div>\n <style>\n {`\n @keyframes fracturePulse {\n 0%, 100% { opacity: 0.7; transform: scale(1); }\n 50% { opacity: 1; transform: scale(1.05); }\n }\n `}\n </style>\n </Html>\n );\n};\n\n/**\n * TraumaOverlay3D Component\n *\n * Visualizes progressive combat trauma including bruising, cuts, and bone damage\n * on 3D character models. Injuries persist across rounds and darken with repeated\n * hits to the same body region.\n *\n * @example\n * ```tsx\n * const [injuries, setInjuries] = useState<Injury[]>([]);\n *\n * // On hit event\n * const handleHit = (region: BodyRegion, position: [number, number, number], type: InjuryType) => {\n * const existingInjury = injuries.find(i => \n * i.region === region && \n * distance(i.position, position) < 0.2\n * );\n *\n * if (existingInjury) {\n * // Progressive bruising - increase hit count\n * setInjuries(prev => prev.map(i => \n * i.id === existingInjury.id \n * ? { ...i, hitCount: i.hitCount + 1, severity: Math.min(1.0, i.severity + 0.2) }\n * : i\n * ));\n * } else {\n * // New injury\n * setInjuries([...injuries, {\n * id: generateId(),\n * region,\n * type,\n * position,\n * severity: 0.5,\n * hitCount: 1,\n * timestamp: Date.now(),\n * }]);\n * }\n * };\n *\n * <TraumaOverlay3D\n * playerId={playerId}\n * health={playerHealth}\n * injuries={injuries}\n * characterPosition={characterPosition}\n * isMobile={isMobile}\n * showFractures={true}\n * />\n * ```\n */\nexport const TraumaOverlay3D: React.FC<TraumaOverlay3DProps> = ({\n playerId,\n health,\n injuries,\n characterPosition,\n isMobile = false,\n showFractures = true,\n}) => {\n const playerInjuries = useMemo(() => {\n if (playerId === null || playerId === undefined) {\n return injuries;\n }\n\n return injuries.filter((injury) => {\n if (injury.playerId === null || injury.playerId === undefined) {\n return true;\n }\n return injury.playerId === playerId;\n });\n }, [injuries, playerId]);\n\n const { fractures, otherInjuries } = useMemo(() => {\n const frac = playerInjuries.filter((i) => i.type === InjuryType.FRACTURE);\n const other = playerInjuries.filter((i) => i.type !== InjuryType.FRACTURE);\n return { fractures: frac, otherInjuries: other };\n }, [playerInjuries]);\n\n return (\n <group data-testid={`trauma-overlay-${playerId}`}>\n {/* Render non-fracture injuries */}\n {otherInjuries.map((injury) => (\n <InjuryMarker\n key={injury.id}\n injury={injury}\n characterPosition={characterPosition}\n isMobile={isMobile}\n />\n ))}\n\n {/* Render fractures if enabled and health is critical */}\n {showFractures &&\n health < 30 &&\n fractures.map((injury) => (\n <InjuryMarker\n key={injury.id}\n injury={injury}\n characterPosition={characterPosition}\n isMobile={isMobile}\n />\n ))}\n\n {/* Fracture warning overlay */}\n {showFractures && health < 30 && fractures.length > 0 && (\n <FractureWarning health={health} isMobile={isMobile} />\n )}\n </group>\n );\n};\n\nexport default TraumaOverlay3D;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"MatchCountdown.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/feedback/MatchCountdown.tsx"],"sourcesContent":["/**\n * MatchCountdown Component - Displays match start countdown sequence\n *\n * Korean: 매치 시작 카운트다운 (Match Start Countdown)\n *\n * Shows \"Ready?\" → \"3... 2... 1...\" → \"Fight!\" sequence with animations.\n * Implements Korean cyberpunk aesthetic with bilingual text support.\n * Plays audio cues for countdown and fight announcement.\n *\n * Refactored to use useKoreanTheme for consistent styling.\n *\n * @module components/combat/MatchCountdown\n * @category Combat UI\n */\n\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport {\n MatchCountdownState,\n useMatchCountdown,\n} from \"../../../../../hooks/useMatchCountdown\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexColorToCSS } from \"../../../../../utils/colorUtils\";\n\n/**\n * Props for the MatchCountdown component\n */\nexport interface MatchCountdownProps {\n /** Callback when countdown completes */\n readonly onComplete: () => void;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n /** Optional callback for skip button */\n readonly onSkip?: () => void;\n /** Whether skip button should be shown */\n readonly showSkip?: boolean;\n}\n\n/**\n * Get display text for current countdown state\n */\nfunction getDisplayText(\n state: MatchCountdownState,\n currentNumber: number\n): {\n ko: string;\n en: string;\n} | null {\n switch (state) {\n case \"ready\":\n return { ko: \"준비?\", en: \"Ready?\" };\n case \"counting\":\n return { ko: String(currentNumber), en: String(currentNumber) };\n case \"fight\":\n return { ko: \"전투!\", en: \"Fight!\" };\n default:\n return null;\n }\n}\n\n/**\n * MatchCountdown Component\n *\n * Displays match start countdown with:\n * - \"Ready?\" announcement (1 second)\n * - Countdown from 3 to 1 (1 second intervals)\n * - \"Fight!\" announcement (1 second)\n * - Pulse/scale animations for emphasis\n * - Bilingual Korean-English text\n * - Audio cues for countdown and fight\n * - Optional skip button\n * - Responsive sizing for mobile/tablet/desktop\n *\n * Korean: 매치 시작 카운트다운 컴포넌트\n */\nconst COUNTDOWN_CONFIG = {\n readyDuration: 1,\n countdownInterval: 1,\n fightDuration: 1,\n startNumber: 3,\n} as const;\n\nexport const MatchCountdown: React.FC<MatchCountdownProps> = ({\n onComplete,\n isMobile,\n onSkip,\n showSkip = false,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"xlarge\", isMobile });\n\n const { state, currentNumber, startCountdown, skipCountdown, isActive } =\n useMatchCountdown(COUNTDOWN_CONFIG, onComplete);\n\n const hasStartedRef = useRef(false);\n\n useEffect(() => {\n if (!hasStartedRef.current) {\n hasStartedRef.current = true;\n startCountdown();\n }\n }, [startCountdown]);\n\n useEffect(() => {\n if (!audio.isAudioReady) return;\n\n if (state === \"counting\" && currentNumber > 0) {\n audio.playSFX(\"attack_light\"); // Using placeholder - will be countdown_beep\n } else if (state === \"fight\") {\n audio.playSFX(\"attack_heavy\"); // Using placeholder - will be fight_start\n }\n }, [state, currentNumber, audio]);\n\n const handleSkip = () => {\n skipCountdown();\n onSkip?.();\n };\n\n const displayText = getDisplayText(state, currentNumber);\n\n const getMainFontSize = (): string => {\n if (isMobile) {\n return state === \"fight\" ? \"72px\" : \"64px\";\n }\n return state === \"fight\" ? \"120px\" : \"96px\";\n };\n\n const getSubFontSize = (): string => {\n if (isMobile) {\n return state === \"fight\" ? \"48px\" : \"40px\";\n }\n return state === \"fight\" ? \"72px\" : \"56px\";\n };\n\n const mainFontSize = getMainFontSize();\n const subFontSize = getSubFontSize();\n\n const goldColor = useMemo(() => hexColorToCSS(theme.colors.ACCENT_GOLD), [theme.colors.ACCENT_GOLD]);\n const cyanColor = useMemo(\n () => hexColorToCSS(theme.colors.PRIMARY_CYAN),\n [theme.colors.PRIMARY_CYAN]\n );\n const darkBg = useMemo(\n () => hexColorToCSS(theme.colors.UI_BACKGROUND_DARK),\n [theme.colors.UI_BACKGROUND_DARK]\n );\n\n if (!isActive || !displayText) {\n return null;\n }\n\n return (\n <>\n <div\n data-testid=\"match-countdown\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Match countdown in progress\"\n aria-live=\"assertive\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: state === \"fight\" ? `${darkBg}cc` : `${darkBg}ee`,\n zIndex: 1000,\n animation: state === \"ready\" ? \"fadeIn 0.3s ease-in\" : \"none\",\n }}\n >\n <div\n style={{\n fontSize: mainFontSize,\n fontWeight: \"bold\",\n color: state === \"fight\" ? goldColor : cyanColor,\n fontFamily: theme.fontFamily.KOREAN,\n textShadow: `0 0 ${state === \"fight\" ? \"40px\" : \"30px\"} ${\n state === \"fight\" ? goldColor : cyanColor\n }`,\n animation:\n state === \"ready\"\n ? \"pulse 0.8s ease-out\"\n : state === \"counting\"\n ? \"countdownPulse 0.8s ease-out\"\n : state === \"fight\"\n ? \"flash 0.3s ease-out\"\n : \"none\",\n textAlign: \"center\",\n userSelect: \"none\",\n }}\n data-testid=\"countdown-text\"\n >\n {displayText.ko}\n <br />\n <span\n style={{\n fontSize: subFontSize,\n }}\n >\n {displayText.en}\n </span>\n </div>\n\n {/* Optional Skip Button */}\n {showSkip && state !== \"fight\" && (\n <button\n onClick={handleSkip}\n data-testid=\"skip-countdown-button\"\n aria-label=\"Skip countdown\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"40px\" : \"60px\",\n padding: isMobile ? \"10px 24px\" : \"12px 32px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: cyanColor,\n color: darkBg,\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${cyanColor}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n }}\n onFocus={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${cyanColor}`;\n e.currentTarget.style.outline = `2px solid ${cyanColor}`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n e.currentTarget.style.outline = \"none\";\n }}\n >\n 건너뛰기 | Skip\n </button>\n )}\n </div>\n\n {/* CSS Animations */}\n <style>\n {`\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n @keyframes pulse {\n 0% {\n opacity: 0;\n transform: scale(0.9);\n }\n 50% {\n opacity: 1;\n transform: scale(1.05);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n @keyframes countdownPulse {\n 0% {\n opacity: 0;\n transform: scale(1.2);\n }\n 50% {\n opacity: 1;\n transform: scale(1.1);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n @keyframes flash {\n 0% {\n opacity: 0;\n transform: scale(1.5);\n }\n 30% {\n opacity: 1;\n transform: scale(1.3);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n `}\n </style>\n </>\n );\n};\n\nexport default MatchCountdown;\n"],"mappings":"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"}
1
+ {"version":3,"file":"MatchCountdown.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/feedback/MatchCountdown.tsx"],"sourcesContent":["/**\n * MatchCountdown Component - Displays match start countdown sequence\n *\n * Korean: 매치 시작 카운트다운 (Match Start Countdown)\n *\n * Shows \"Ready?\" → \"3... 2... 1...\" → \"Fight!\" sequence with animations.\n * Implements Korean cyberpunk aesthetic with bilingual text support.\n * Plays audio cues for countdown and fight announcement.\n *\n * Refactored to use useKoreanTheme for consistent styling.\n *\n * @module components/combat/MatchCountdown\n * @category Combat UI\n */\n\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport {\n MatchCountdownState,\n useMatchCountdown,\n} from \"../../../../../hooks/useMatchCountdown\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexColorToCSS } from \"../../../../../utils/colorUtils\";\n\n/**\n * Props for the MatchCountdown component\n */\nexport interface MatchCountdownProps {\n /** Callback when countdown completes */\n readonly onComplete: () => void;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n /** Optional callback for skip button */\n readonly onSkip?: () => void;\n /** Whether skip button should be shown */\n readonly showSkip?: boolean;\n}\n\n/**\n * Get display text for current countdown state\n */\nfunction getDisplayText(\n state: MatchCountdownState,\n currentNumber: number\n): {\n ko: string;\n en: string;\n} | null {\n switch (state) {\n case \"ready\":\n return { ko: \"준비?\", en: \"Ready?\" };\n case \"counting\":\n return { ko: String(currentNumber), en: String(currentNumber) };\n case \"fight\":\n return { ko: \"전투!\", en: \"Fight!\" };\n default:\n return null;\n }\n}\n\n/**\n * MatchCountdown Component\n *\n * Displays match start countdown with:\n * - \"Ready?\" announcement (1 second)\n * - Countdown from 3 to 1 (1 second intervals)\n * - \"Fight!\" announcement (1 second)\n * - Pulse/scale animations for emphasis\n * - Bilingual Korean-English text\n * - Audio cues for countdown and fight\n * - Optional skip button\n * - Responsive sizing for mobile/tablet/desktop\n *\n * Korean: 매치 시작 카운트다운 컴포넌트\n */\nconst COUNTDOWN_CONFIG = {\n readyDuration: 1,\n countdownInterval: 1,\n fightDuration: 1,\n startNumber: 3,\n} as const;\n\nexport const MatchCountdown: React.FC<MatchCountdownProps> = ({\n onComplete,\n isMobile,\n onSkip,\n showSkip = false,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"xlarge\", isMobile });\n\n const { state, currentNumber, startCountdown, skipCountdown, isActive } =\n useMatchCountdown(COUNTDOWN_CONFIG, onComplete);\n\n const hasStartedRef = useRef(false);\n\n useEffect(() => {\n if (!hasStartedRef.current) {\n hasStartedRef.current = true;\n startCountdown();\n }\n }, [startCountdown]);\n\n useEffect(() => {\n if (!audio.isAudioReady) return;\n\n if (state === \"counting\" && currentNumber > 0) {\n audio.playSFX(\"attack_light\"); // Using placeholder - will be countdown_beep\n } else if (state === \"fight\") {\n audio.playSFX(\"attack_heavy\"); // Using placeholder - will be fight_start\n }\n }, [state, currentNumber, audio]);\n\n const handleSkip = () => {\n skipCountdown();\n onSkip?.();\n };\n\n const displayText = getDisplayText(state, currentNumber);\n\n const getMainFontSize = (): string => {\n if (isMobile) {\n return state === \"fight\" ? \"72px\" : \"64px\";\n }\n return state === \"fight\" ? \"120px\" : \"96px\";\n };\n\n const getSubFontSize = (): string => {\n if (isMobile) {\n return state === \"fight\" ? \"48px\" : \"40px\";\n }\n return state === \"fight\" ? \"72px\" : \"56px\";\n };\n\n const mainFontSize = getMainFontSize();\n const subFontSize = getSubFontSize();\n\n const goldColor = useMemo(() => hexColorToCSS(theme.colors.ACCENT_GOLD), [theme.colors.ACCENT_GOLD]);\n const cyanColor = useMemo(\n () => hexColorToCSS(theme.colors.PRIMARY_CYAN),\n [theme.colors.PRIMARY_CYAN]\n );\n const darkBg = useMemo(\n () => hexColorToCSS(theme.colors.UI_BACKGROUND_DARK),\n [theme.colors.UI_BACKGROUND_DARK]\n );\n\n if (!isActive || !displayText) {\n return null;\n }\n\n return (\n <>\n <div\n data-testid=\"match-countdown\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Match countdown in progress\"\n aria-live=\"assertive\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: state === \"fight\" ? `${darkBg}cc` : `${darkBg}ee`,\n zIndex: 1000,\n animation: state === \"ready\" ? \"fadeIn 0.3s ease-in\" : \"none\",\n }}\n >\n <div\n style={{\n fontSize: mainFontSize,\n fontWeight: \"bold\",\n color: state === \"fight\" ? goldColor : cyanColor,\n fontFamily: theme.fontFamily.KOREAN,\n textShadow: `0 0 ${state === \"fight\" ? \"40px\" : \"30px\"} ${\n state === \"fight\" ? goldColor : cyanColor\n }`,\n animation:\n state === \"ready\"\n ? \"pulse 0.8s ease-out\"\n : state === \"counting\"\n ? \"countdownPulse 0.8s ease-out\"\n : state === \"fight\"\n ? \"flash 0.3s ease-out\"\n : \"none\",\n textAlign: \"center\",\n userSelect: \"none\",\n }}\n data-testid=\"countdown-text\"\n >\n {displayText.ko}\n <br />\n <span\n style={{\n fontSize: subFontSize,\n }}\n >\n {displayText.en}\n </span>\n </div>\n\n {/* Optional Skip Button */}\n {showSkip && state !== \"fight\" && (\n <button\n onClick={handleSkip}\n data-testid=\"skip-countdown-button\"\n aria-label=\"Skip countdown\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"40px\" : \"60px\",\n padding: isMobile ? \"10px 24px\" : \"12px 32px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: cyanColor,\n color: darkBg,\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${cyanColor}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n }}\n onFocus={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 0 20px ${cyanColor}`;\n e.currentTarget.style.outline = `2px solid ${cyanColor}`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = \"none\";\n e.currentTarget.style.outline = \"none\";\n }}\n >\n 건너뛰기 | Skip\n </button>\n )}\n </div>\n\n {/* CSS Animations */}\n <style>\n {`\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n @keyframes pulse {\n 0% {\n opacity: 0;\n transform: scale(0.9);\n }\n 50% {\n opacity: 1;\n transform: scale(1.05);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n @keyframes countdownPulse {\n 0% {\n opacity: 0;\n transform: scale(1.2);\n }\n 50% {\n opacity: 1;\n transform: scale(1.1);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n @keyframes flash {\n 0% {\n opacity: 0;\n transform: scale(1.5);\n }\n 30% {\n opacity: 1;\n transform: scale(1.3);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n `}\n </style>\n </>\n );\n};\n\nexport default MatchCountdown;\n"],"mappings":"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"}
@@ -74,23 +74,24 @@ var RoundAnnouncement = ({ roundNumber, roundWinner, currentScore, roundStats, o
74
74
  const cyanColor = useMemo(() => hexColorToCSS(theme.colors.PRIMARY_CYAN), [theme.colors.PRIMARY_CYAN]);
75
75
  const darkBg = useMemo(() => hexColorToCSS(theme.colors.UI_BACKGROUND_DARK), [theme.colors.UI_BACKGROUND_DARK]);
76
76
  const textSecondary = useMemo(() => hexColorToCSS(theme.colors.TEXT_SECONDARY), [theme.colors.TEXT_SECONDARY]);
77
+ const containerStyle = useMemo(() => ({
78
+ position: "fixed",
79
+ top: 0,
80
+ left: 0,
81
+ width: "100%",
82
+ height: "100%",
83
+ display: "flex",
84
+ flexDirection: "column",
85
+ alignItems: "center",
86
+ justifyContent: "center",
87
+ backgroundColor: `${darkBg}dd`,
88
+ zIndex: 1e3,
89
+ opacity: isVisible ? 1 : 0,
90
+ transition: "opacity 0.3s ease-in-out"
91
+ }), [darkBg, isVisible]);
77
92
  return /* @__PURE__ */ jsxs("div", {
78
93
  "data-testid": "round-announcement",
79
- style: useMemo(() => ({
80
- position: "fixed",
81
- top: 0,
82
- left: 0,
83
- width: "100%",
84
- height: "100%",
85
- display: "flex",
86
- flexDirection: "column",
87
- alignItems: "center",
88
- justifyContent: "center",
89
- backgroundColor: `${darkBg}dd`,
90
- zIndex: 1e3,
91
- opacity: isVisible ? 1 : 0,
92
- transition: "opacity 0.3s ease-in-out"
93
- }), [darkBg, isVisible]),
94
+ style: containerStyle,
94
95
  children: [
95
96
  isMatchPoint && /* @__PURE__ */ jsx("div", {
96
97
  style: {
@@ -1 +1 @@
1
- {"version":3,"file":"RoundAnnouncementOverlayHtml.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.tsx"],"sourcesContent":["/**\n * RoundAnnouncement Component - Displays round completion and transition UI\n *\n * Korean: 라운드 발표 (Round Announcement)\n *\n * Shows round winner, current score, statistics, and countdown to next round.\n * Implements Korean cyberpunk aesthetic with bilingual text support.\n *\n * Refactored to use useKoreanTheme for consistent styling.\n *\n * @module components/combat/RoundAnnouncement\n * @category Combat UI\n */\n\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { PlayerState } from \"../../../../../systems\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexColorToCSS } from \"../../../../../utils/colorUtils\";\n\n/**\n * Round statistics displayed between rounds\n *\n * Korean: 라운드 통계 (Round Statistics)\n */\nexport interface RoundStats {\n /** Total damage dealt by player */\n readonly damageDealt: number;\n /** Number of hits successfully landed */\n readonly hitsLanded: number;\n /** Number of vital points struck */\n readonly vitalPointsHit: number;\n /** Combat accuracy percentage */\n readonly accuracy: number;\n}\n\n/**\n * Props for the RoundAnnouncement component\n */\nexport interface RoundAnnouncementProps {\n /** Current round number (1-based) */\n readonly roundNumber: number;\n /** Round winner, or null if round was a draw */\n readonly roundWinner: PlayerState | null;\n /** Current match score (player1 wins, player2 wins) */\n readonly currentScore: { readonly player1: number; readonly player2: number };\n /** Round statistics to display */\n readonly roundStats?: RoundStats;\n /** Callback when countdown completes */\n readonly onCountdownComplete: () => void;\n /** Callback when skip button is pressed */\n readonly onSkip: () => void;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n /** Total number of rounds in match (for match point detection) */\n readonly totalRounds?: number;\n /** Countdown duration in seconds */\n readonly countdownDuration?: number;\n}\n\n/**\n * RoundAnnouncement Component\n *\n * Displays round completion announcement with:\n * - Bilingual round completion title\n * - Round winner display\n * - Current match score\n * - Round statistics (damage, hits, accuracy)\n * - Countdown to next round\n * - Skip button for quick play\n * - Match point indicator for final rounds\n * - Uses useKoreanTheme for consistent styling\n *\n * Korean: 라운드 종료 발표 컴포넌트\n */\nexport const RoundAnnouncement: React.FC<RoundAnnouncementProps> = ({\n roundNumber,\n roundWinner,\n currentScore,\n roundStats,\n onCountdownComplete,\n onSkip,\n isMobile,\n totalRounds = 3,\n countdownDuration = 3,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"large\", isMobile });\n const [countdown, setCountdown] = useState(countdownDuration);\n const [isVisible, setIsVisible] = useState(false);\n const hasCompletedRef = useRef(false);\n\n const onCountdownCompleteRef = useRef(onCountdownComplete);\n useEffect(() => {\n onCountdownCompleteRef.current = onCountdownComplete;\n }, [onCountdownComplete]);\n\n const handleCountdownComplete = useCallback(() => {\n onCountdownCompleteRef.current();\n }, []);\n\n useEffect(() => {\n const timer = setTimeout(() => setIsVisible(true), 50);\n return () => clearTimeout(timer);\n }, []);\n\n useEffect(() => {\n if (countdown <= 0) {\n if (!hasCompletedRef.current) {\n hasCompletedRef.current = true;\n handleCountdownComplete();\n }\n return;\n }\n\n const timer = setInterval(() => {\n setCountdown((prev) => {\n if (prev <= 1) {\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [countdown, handleCountdownComplete]);\n\n const isMatchPoint = useMemo(() => {\n const maxScore = Math.max(currentScore.player1, currentScore.player2);\n const roundsToWin = Math.ceil(totalRounds / 2);\n return maxScore === roundsToWin - 1;\n }, [currentScore, totalRounds]);\n\n const goldColor = useMemo(\n () => hexColorToCSS(theme.colors.ACCENT_GOLD),\n [theme.colors.ACCENT_GOLD]\n );\n const cyanColor = useMemo(\n () => hexColorToCSS(theme.colors.PRIMARY_CYAN),\n [theme.colors.PRIMARY_CYAN]\n );\n const darkBg = useMemo(\n () => hexColorToCSS(theme.colors.UI_BACKGROUND_DARK),\n [theme.colors.UI_BACKGROUND_DARK]\n );\n const textSecondary = useMemo(\n () => hexColorToCSS(theme.colors.TEXT_SECONDARY),\n [theme.colors.TEXT_SECONDARY]\n );\n\n const containerStyle = useMemo(\n () => ({\n position: \"fixed\" as const,\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\" as const,\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: `${darkBg}dd`,\n zIndex: 1000,\n opacity: isVisible ? 1 : 0,\n transition: \"opacity 0.3s ease-in-out\",\n }),\n [darkBg, isVisible]\n );\n\n return (\n <div data-testid=\"round-announcement\" style={containerStyle}>\n {/* Match Point Indicator */}\n {isMatchPoint && (\n <div\n style={{\n fontSize: isMobile ? \"20px\" : \"28px\",\n color: goldColor,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n marginBottom: isMobile ? \"10px\" : \"20px\",\n textShadow: `0 0 20px ${goldColor}`,\n animation: \"pulse 1s infinite\",\n }}\n data-testid=\"match-point-indicator\"\n >\n 매치 포인트! | Match Point!\n </div>\n )}\n\n {/* Round Complete Title */}\n <h1\n style={{\n fontSize: isMobile ? \"36px\" : \"56px\",\n color: goldColor,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"20px\" : \"30px\"} 0`,\n textShadow: `0 0 30px ${goldColor}`,\n textAlign: \"center\",\n }}\n data-testid=\"round-complete-title\"\n >\n 라운드 {roundNumber} 완료!\n <br />\n Round {roundNumber} Complete!\n </h1>\n\n {/* Round Winner */}\n {roundWinner && (\n <div\n style={{\n fontSize: isMobile ? \"24px\" : \"36px\",\n color: cyanColor,\n fontFamily: theme.fontFamily.KOREAN,\n margin: `0 0 ${isMobile ? \"20px\" : \"30px\"} 0`,\n textAlign: \"center\",\n }}\n data-testid=\"round-winner\"\n >\n 승자 | Winner: {roundWinner.name.korean} | {roundWinner.name.english}\n </div>\n )}\n\n {/* Current Score */}\n <div\n style={{\n display: \"flex\",\n gap: isMobile ? \"40px\" : \"80px\",\n marginBottom: isMobile ? \"20px\" : \"30px\",\n fontSize: isMobile ? \"32px\" : \"48px\",\n fontWeight: \"bold\",\n }}\n data-testid=\"current-score\"\n >\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"18px\",\n color: cyanColor,\n marginBottom: \"8px\",\n }}\n >\n Player 1\n </div>\n <div style={{ color: goldColor }}>{currentScore.player1}</div>\n </div>\n <div style={{ color: cyanColor }}>-</div>\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"18px\",\n color: cyanColor,\n marginBottom: \"8px\",\n }}\n >\n Player 2\n </div>\n <div style={{ color: goldColor }}>{currentScore.player2}</div>\n </div>\n </div>\n\n {/* Round Statistics */}\n {roundStats && (\n <div\n style={{\n display: \"flex\",\n flexDirection: isMobile ? \"column\" : \"row\",\n gap: isMobile ? \"12px\" : \"24px\",\n marginBottom: isMobile ? \"20px\" : \"30px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n }}\n data-testid=\"round-stats\"\n >\n <div>피해량 | Damage: {roundStats.damageDealt.toFixed(0)}</div>\n <div>명중 | Hits: {roundStats.hitsLanded}</div>\n <div>급소타격 | Vital Points: {roundStats.vitalPointsHit}</div>\n <div>정확도 | Accuracy: {roundStats.accuracy.toFixed(1)}%</div>\n </div>\n )}\n\n {/* Countdown */}\n <div\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: goldColor,\n fontWeight: \"bold\",\n marginBottom: isMobile ? \"20px\" : \"30px\",\n textShadow: `0 0 40px ${goldColor}`,\n }}\n data-testid=\"countdown-display\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n aria-label={`Countdown: ${countdown} seconds remaining`}\n >\n {countdown}\n </div>\n\n {/* Skip Button */}\n <button\n onClick={onSkip}\n data-testid=\"skip-countdown-button\"\n className=\"skip-countdown-button\"\n style={{\n padding: isMobile ? \"10px 24px\" : \"12px 32px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: cyanColor,\n color: darkBg,\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n }}\n >\n 건너뛰기 | Skip\n </button>\n\n {/* CSS Animation for pulse effect and button hover */}\n <style>\n {`\n @keyframes pulse {\n 0%, 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.7;\n transform: scale(1.05);\n }\n }\n \n .skip-countdown-button {\n transition: all 0.2s ease;\n }\n \n .skip-countdown-button:hover {\n transform: scale(1.05);\n box-shadow: 0 0 20px ${cyanColor};\n }\n \n .skip-countdown-button:focus {\n outline: 2px solid ${cyanColor};\n outline-offset: 2px;\n }\n `}\n </style>\n </div>\n );\n};\n\nexport default RoundAnnouncement;\n"],"mappings":"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"}
1
+ {"version":3,"file":"RoundAnnouncementOverlayHtml.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.tsx"],"sourcesContent":["/**\n * RoundAnnouncement Component - Displays round completion and transition UI\n *\n * Korean: 라운드 발표 (Round Announcement)\n *\n * Shows round winner, current score, statistics, and countdown to next round.\n * Implements Korean cyberpunk aesthetic with bilingual text support.\n *\n * Refactored to use useKoreanTheme for consistent styling.\n *\n * @module components/combat/RoundAnnouncement\n * @category Combat UI\n */\n\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { PlayerState } from \"../../../../../systems\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexColorToCSS } from \"../../../../../utils/colorUtils\";\n\n/**\n * Round statistics displayed between rounds\n *\n * Korean: 라운드 통계 (Round Statistics)\n */\nexport interface RoundStats {\n /** Total damage dealt by player */\n readonly damageDealt: number;\n /** Number of hits successfully landed */\n readonly hitsLanded: number;\n /** Number of vital points struck */\n readonly vitalPointsHit: number;\n /** Combat accuracy percentage */\n readonly accuracy: number;\n}\n\n/**\n * Props for the RoundAnnouncement component\n */\nexport interface RoundAnnouncementProps {\n /** Current round number (1-based) */\n readonly roundNumber: number;\n /** Round winner, or null if round was a draw */\n readonly roundWinner: PlayerState | null;\n /** Current match score (player1 wins, player2 wins) */\n readonly currentScore: { readonly player1: number; readonly player2: number };\n /** Round statistics to display */\n readonly roundStats?: RoundStats;\n /** Callback when countdown completes */\n readonly onCountdownComplete: () => void;\n /** Callback when skip button is pressed */\n readonly onSkip: () => void;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n /** Total number of rounds in match (for match point detection) */\n readonly totalRounds?: number;\n /** Countdown duration in seconds */\n readonly countdownDuration?: number;\n}\n\n/**\n * RoundAnnouncement Component\n *\n * Displays round completion announcement with:\n * - Bilingual round completion title\n * - Round winner display\n * - Current match score\n * - Round statistics (damage, hits, accuracy)\n * - Countdown to next round\n * - Skip button for quick play\n * - Match point indicator for final rounds\n * - Uses useKoreanTheme for consistent styling\n *\n * Korean: 라운드 종료 발표 컴포넌트\n */\nexport const RoundAnnouncement: React.FC<RoundAnnouncementProps> = ({\n roundNumber,\n roundWinner,\n currentScore,\n roundStats,\n onCountdownComplete,\n onSkip,\n isMobile,\n totalRounds = 3,\n countdownDuration = 3,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"large\", isMobile });\n const [countdown, setCountdown] = useState(countdownDuration);\n const [isVisible, setIsVisible] = useState(false);\n const hasCompletedRef = useRef(false);\n\n const onCountdownCompleteRef = useRef(onCountdownComplete);\n useEffect(() => {\n onCountdownCompleteRef.current = onCountdownComplete;\n }, [onCountdownComplete]);\n\n const handleCountdownComplete = useCallback(() => {\n onCountdownCompleteRef.current();\n }, []);\n\n useEffect(() => {\n const timer = setTimeout(() => setIsVisible(true), 50);\n return () => clearTimeout(timer);\n }, []);\n\n useEffect(() => {\n if (countdown <= 0) {\n if (!hasCompletedRef.current) {\n hasCompletedRef.current = true;\n handleCountdownComplete();\n }\n return;\n }\n\n const timer = setInterval(() => {\n setCountdown((prev) => {\n if (prev <= 1) {\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [countdown, handleCountdownComplete]);\n\n const isMatchPoint = useMemo(() => {\n const maxScore = Math.max(currentScore.player1, currentScore.player2);\n const roundsToWin = Math.ceil(totalRounds / 2);\n return maxScore === roundsToWin - 1;\n }, [currentScore, totalRounds]);\n\n const goldColor = useMemo(\n () => hexColorToCSS(theme.colors.ACCENT_GOLD),\n [theme.colors.ACCENT_GOLD]\n );\n const cyanColor = useMemo(\n () => hexColorToCSS(theme.colors.PRIMARY_CYAN),\n [theme.colors.PRIMARY_CYAN]\n );\n const darkBg = useMemo(\n () => hexColorToCSS(theme.colors.UI_BACKGROUND_DARK),\n [theme.colors.UI_BACKGROUND_DARK]\n );\n const textSecondary = useMemo(\n () => hexColorToCSS(theme.colors.TEXT_SECONDARY),\n [theme.colors.TEXT_SECONDARY]\n );\n\n const containerStyle = useMemo(\n () => ({\n position: \"fixed\" as const,\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\" as const,\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: `${darkBg}dd`,\n zIndex: 1000,\n opacity: isVisible ? 1 : 0,\n transition: \"opacity 0.3s ease-in-out\",\n }),\n [darkBg, isVisible]\n );\n\n return (\n <div data-testid=\"round-announcement\" style={containerStyle}>\n {/* Match Point Indicator */}\n {isMatchPoint && (\n <div\n style={{\n fontSize: isMobile ? \"20px\" : \"28px\",\n color: goldColor,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n marginBottom: isMobile ? \"10px\" : \"20px\",\n textShadow: `0 0 20px ${goldColor}`,\n animation: \"pulse 1s infinite\",\n }}\n data-testid=\"match-point-indicator\"\n >\n 매치 포인트! | Match Point!\n </div>\n )}\n\n {/* Round Complete Title */}\n <h1\n style={{\n fontSize: isMobile ? \"36px\" : \"56px\",\n color: goldColor,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"20px\" : \"30px\"} 0`,\n textShadow: `0 0 30px ${goldColor}`,\n textAlign: \"center\",\n }}\n data-testid=\"round-complete-title\"\n >\n 라운드 {roundNumber} 완료!\n <br />\n Round {roundNumber} Complete!\n </h1>\n\n {/* Round Winner */}\n {roundWinner && (\n <div\n style={{\n fontSize: isMobile ? \"24px\" : \"36px\",\n color: cyanColor,\n fontFamily: theme.fontFamily.KOREAN,\n margin: `0 0 ${isMobile ? \"20px\" : \"30px\"} 0`,\n textAlign: \"center\",\n }}\n data-testid=\"round-winner\"\n >\n 승자 | Winner: {roundWinner.name.korean} | {roundWinner.name.english}\n </div>\n )}\n\n {/* Current Score */}\n <div\n style={{\n display: \"flex\",\n gap: isMobile ? \"40px\" : \"80px\",\n marginBottom: isMobile ? \"20px\" : \"30px\",\n fontSize: isMobile ? \"32px\" : \"48px\",\n fontWeight: \"bold\",\n }}\n data-testid=\"current-score\"\n >\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"18px\",\n color: cyanColor,\n marginBottom: \"8px\",\n }}\n >\n Player 1\n </div>\n <div style={{ color: goldColor }}>{currentScore.player1}</div>\n </div>\n <div style={{ color: cyanColor }}>-</div>\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"18px\",\n color: cyanColor,\n marginBottom: \"8px\",\n }}\n >\n Player 2\n </div>\n <div style={{ color: goldColor }}>{currentScore.player2}</div>\n </div>\n </div>\n\n {/* Round Statistics */}\n {roundStats && (\n <div\n style={{\n display: \"flex\",\n flexDirection: isMobile ? \"column\" : \"row\",\n gap: isMobile ? \"12px\" : \"24px\",\n marginBottom: isMobile ? \"20px\" : \"30px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n }}\n data-testid=\"round-stats\"\n >\n <div>피해량 | Damage: {roundStats.damageDealt.toFixed(0)}</div>\n <div>명중 | Hits: {roundStats.hitsLanded}</div>\n <div>급소타격 | Vital Points: {roundStats.vitalPointsHit}</div>\n <div>정확도 | Accuracy: {roundStats.accuracy.toFixed(1)}%</div>\n </div>\n )}\n\n {/* Countdown */}\n <div\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: goldColor,\n fontWeight: \"bold\",\n marginBottom: isMobile ? \"20px\" : \"30px\",\n textShadow: `0 0 40px ${goldColor}`,\n }}\n data-testid=\"countdown-display\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n aria-label={`Countdown: ${countdown} seconds remaining`}\n >\n {countdown}\n </div>\n\n {/* Skip Button */}\n <button\n onClick={onSkip}\n data-testid=\"skip-countdown-button\"\n className=\"skip-countdown-button\"\n style={{\n padding: isMobile ? \"10px 24px\" : \"12px 32px\",\n fontSize: isMobile ? \"14px\" : \"16px\",\n backgroundColor: cyanColor,\n color: darkBg,\n border: \"none\",\n borderRadius: \"6px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n }}\n >\n 건너뛰기 | Skip\n </button>\n\n {/* CSS Animation for pulse effect and button hover */}\n <style>\n {`\n @keyframes pulse {\n 0%, 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.7;\n transform: scale(1.05);\n }\n }\n \n .skip-countdown-button {\n transition: all 0.2s ease;\n }\n \n .skip-countdown-button:hover {\n transform: scale(1.05);\n box-shadow: 0 0 20px ${cyanColor};\n }\n \n .skip-countdown-button:focus {\n outline: 2px solid ${cyanColor};\n outline-offset: 2px;\n }\n `}\n </style>\n </div>\n );\n};\n\nexport default RoundAnnouncement;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"RoundDisplayStatus.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/feedback/RoundDisplayStatus.tsx"],"sourcesContent":["/**\n * RoundDisplayStatus - Brief round status messages\n * \n * Displays quick status messages like \"라운드 시작!\", \"전투!\", \"라운드 종료\", \"K.O.!\"\n * Rendered as a fullscreen overlay outside the Canvas.\n * \n * @korean 라운드 상태 표시 - 간단한 라운드 상태 메시지\n */\n\nimport React from \"react\";\nimport { Z_INDEX } from \"../../../../../types/LayoutTypes\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\n\nexport type RoundDisplayStatus = \"start\" | \"fight\" | \"end\" | \"ko\" | null;\n\nexport interface RoundDisplayStatusProps {\n /** Current round status to display */\n readonly status: RoundDisplayStatus;\n /** Whether mobile layout is active */\n readonly isMobile?: boolean;\n}\n\n/**\n * RoundDisplayStatus Component\n * \n * Large centered text overlay showing brief round status messages.\n * Auto-fades based on combatState.roundDisplayStatus timing.\n */\nexport const RoundDisplayStatus: React.FC<RoundDisplayStatusProps> = ({\n status,\n isMobile = false,\n}) => {\n const theme = useKoreanTheme({\n variant: \"primary\",\n size: \"md\",\n isMobile,\n });\n\n if (!status) {\n return null;\n }\n\n const statusMessages: Record<Exclude<RoundDisplayStatus, null>, string> = {\n start: \"라운드 시작!\",\n fight: \"전투!\",\n end: \"라운드 종료\",\n ko: \"K.O.!\",\n };\n\n const fontSize = isMobile ? \"48px\" : \"72px\";\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n justifyContent: \"center\",\n alignItems: \"center\",\n pointerEvents: \"none\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"round-display-status\"\n >\n <div\n style={{\n fontSize,\n fontWeight: \"bold\",\n fontFamily: theme.koreanTypography.fontFamily,\n lineHeight: theme.koreanTypography.lineHeight,\n letterSpacing: theme.koreanTypography.letterSpacing,\n wordBreak: theme.koreanTypography.wordBreak,\n textAlign: \"center\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n textShadow: \"0 0 20px rgba(255, 215, 0, 0.8)\",\n }}\n data-testid={`round-display-status-${status}`}\n >\n {statusMessages[status]}\n </div>\n </div>\n );\n};\n\nexport default RoundDisplayStatus;\n"],"mappings":";;;;;;;;;;;;AA6BA,IAAa,sBAAyD,EACpE,QACA,WAAW,YACP;CACJ,MAAM,QAAQ,eAAe;EAC3B,SAAS;EACT,MAAM;EACN;CACF,CAAC;CAED,IAAI,CAAC,QACH,OAAO;CAGT,MAAM,iBAAoE;EACxE,OAAO;EACP,OAAO;EACP,KAAK;EACL,IAAI;CACN;CAEA,MAAM,WAAW,WAAW,SAAS;CAErC,OACE,oBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,gBAAgB;GAChB,YAAY;GACZ,eAAe;GACf,QAAQ,QAAQ;EAClB;EACA,eAAY;YAEZ,oBAAC,OAAD;GACE,OAAO;IACL;IACA,YAAY;IACZ,YAAY,MAAM,iBAAiB;IACnC,YAAY,MAAM,iBAAiB;IACnC,eAAe,MAAM,iBAAiB;IACtC,WAAW,MAAM,iBAAiB;IAClC,WAAW;IACX,OAAO,gBAAgB,MAAM,OAAO,aAAa,CAAC;IAClD,YAAY;GACd;GACA,eAAa,wBAAwB;aAEpC,eAAe;EACb,CAAA;CACF,CAAA;AAET"}
1
+ {"version":3,"file":"RoundDisplayStatus.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/feedback/RoundDisplayStatus.tsx"],"sourcesContent":["/**\n * RoundDisplayStatus - Brief round status messages\n * \n * Displays quick status messages like \"라운드 시작!\", \"전투!\", \"라운드 종료\", \"K.O.!\"\n * Rendered as a fullscreen overlay outside the Canvas.\n * \n * @korean 라운드 상태 표시 - 간단한 라운드 상태 메시지\n */\n\nimport React from \"react\";\nimport { Z_INDEX } from \"../../../../../types/LayoutTypes\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\n\nexport type RoundDisplayStatus = \"start\" | \"fight\" | \"end\" | \"ko\" | null;\n\nexport interface RoundDisplayStatusProps {\n /** Current round status to display */\n readonly status: RoundDisplayStatus;\n /** Whether mobile layout is active */\n readonly isMobile?: boolean;\n}\n\n/**\n * RoundDisplayStatus Component\n * \n * Large centered text overlay showing brief round status messages.\n * Auto-fades based on combatState.roundDisplayStatus timing.\n */\nexport const RoundDisplayStatus: React.FC<RoundDisplayStatusProps> = ({\n status,\n isMobile = false,\n}) => {\n const theme = useKoreanTheme({\n variant: \"primary\",\n size: \"md\",\n isMobile,\n });\n\n if (!status) {\n return null;\n }\n\n const statusMessages: Record<Exclude<RoundDisplayStatus, null>, string> = {\n start: \"라운드 시작!\",\n fight: \"전투!\",\n end: \"라운드 종료\",\n ko: \"K.O.!\",\n };\n\n const fontSize = isMobile ? \"48px\" : \"72px\";\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n justifyContent: \"center\",\n alignItems: \"center\",\n pointerEvents: \"none\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"round-display-status\"\n >\n <div\n style={{\n fontSize,\n fontWeight: \"bold\",\n fontFamily: theme.koreanTypography.fontFamily,\n lineHeight: theme.koreanTypography.lineHeight,\n letterSpacing: theme.koreanTypography.letterSpacing,\n wordBreak: theme.koreanTypography.wordBreak,\n textAlign: \"center\",\n color: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n textShadow: \"0 0 20px rgba(255, 215, 0, 0.8)\",\n }}\n data-testid={`round-display-status-${status}`}\n >\n {statusMessages[status]}\n </div>\n </div>\n );\n};\n\nexport default RoundDisplayStatus;\n"],"mappings":";;;;;;;;;;;;AA6BA,IAAa,sBAAyD,EACpE,QACA,WAAW,YACP;CACJ,MAAM,QAAQ,eAAe;EAC3B,SAAS;EACT,MAAM;EACN;CACF,CAAC;CAED,IAAI,CAAC,QACH,OAAO;CAGT,MAAM,iBAAoE;EACxE,OAAO;EACP,OAAO;EACP,KAAK;EACL,IAAI;CACN;CAEA,MAAM,WAAW,WAAW,SAAS;CAErC,OACE,oBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,gBAAgB;GAChB,YAAY;GACZ,eAAe;GACf,QAAQ,QAAQ;EAClB;EACA,eAAY;EAEZ,UAAA,oBAAC,OAAD;GACE,OAAO;IACL;IACA,YAAY;IACZ,YAAY,MAAM,iBAAiB;IACnC,YAAY,MAAM,iBAAiB;IACnC,eAAe,MAAM,iBAAiB;IACtC,WAAW,MAAM,iBAAiB;IAClC,WAAW;IACX,OAAO,gBAAgB,MAAM,OAAO,aAAa,CAAC;IAClD,YAAY;GACd;GACA,eAAa,wBAAwB;GAEpC,UAAA,eAAe;EACb,CAAA;CACF,CAAA;AAET"}