blacktrigram 0.7.131 → 0.7.133

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 (160) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/types.d.ts +1 -1
  3. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  4. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  5. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  6. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  7. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  8. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  9. package/lib/components/screens/combat/components/effects/index.d.ts +1 -1
  10. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  11. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  12. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  13. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  14. package/lib/components/screens/combat/hooks/useAICombat.d.ts +4 -4
  15. package/lib/components/screens/combat/hooks/useCombatActions.d.ts +8 -8
  16. package/lib/components/screens/combat/hooks/useCombatAttackMovement.d.ts +2 -2
  17. package/lib/components/screens/combat/hooks/useCombatState.d.ts +2 -2
  18. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  19. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  20. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  21. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  22. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  23. package/lib/components/screens/training/hooks/useAttackMovement.d.ts +2 -2
  24. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  25. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  26. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  27. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  28. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  29. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  30. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  31. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  32. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  33. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  34. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  35. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  36. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  37. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  38. package/lib/components/shared/ui/SplashScreen.js +2 -2
  39. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  40. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.d.ts +1 -1
  41. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  42. package/lib/data/archetypeClothing.d.ts +2 -2
  43. package/lib/data/archetypePhysicalAttributes.d.ts +1 -1
  44. package/lib/data/techniques.js.map +1 -1
  45. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  46. package/lib/systems/ai/AIPersonality.d.ts +1 -1
  47. package/lib/systems/ai/ArchetypeEnforcer.d.ts +1 -1
  48. package/lib/systems/ai/ComboSystem.d.ts +3 -3
  49. package/lib/systems/ai/TrainingAI.d.ts +1 -1
  50. package/lib/systems/ai/types.d.ts +3 -3
  51. package/lib/systems/animation/builders/AnimationBuilder.d.ts +2 -2
  52. package/lib/systems/animation/builders/HandPoseApplicator.d.ts +1 -1
  53. package/lib/systems/animation/builders/HandPoses.d.ts +2 -2
  54. package/lib/systems/animation/builders/KeyframeConfig.d.ts +2 -2
  55. package/lib/systems/animation/builders/KeyframeInterpolation.d.ts +1 -1
  56. package/lib/systems/animation/builders/KoreanGuardPositions.d.ts +1 -1
  57. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.d.ts +2 -2
  58. package/lib/systems/animation/builders/MartialPoseApplicator.d.ts +1 -1
  59. package/lib/systems/animation/builders/SkeletonRig.d.ts +3 -3
  60. package/lib/systems/animation/builders/TrigramGuardApplicator.d.ts +2 -2
  61. package/lib/systems/animation/catalogs/AttackAnimations.d.ts +1 -1
  62. package/lib/systems/animation/catalogs/BasicAnimations.d.ts +1 -1
  63. package/lib/systems/animation/catalogs/ComboAnimations.d.ts +1 -1
  64. package/lib/systems/animation/catalogs/DarkOpsAnimations.d.ts +1 -1
  65. package/lib/systems/animation/catalogs/DefensiveAnimations.d.ts +2 -2
  66. package/lib/systems/animation/catalogs/ElbowKneeAnimations.d.ts +1 -1
  67. package/lib/systems/animation/catalogs/EnhancedAttackAnimations.d.ts +1 -1
  68. package/lib/systems/animation/catalogs/EnhancedElbowKneeAnimations.d.ts +1 -1
  69. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.d.ts +1 -1
  70. package/lib/systems/animation/catalogs/GamRedirectionAnimations.d.ts +1 -1
  71. package/lib/systems/animation/catalogs/GamStanceAnimations.d.ts +1 -1
  72. package/lib/systems/animation/catalogs/GamTechniqueAnimations.d.ts +1 -1
  73. package/lib/systems/animation/catalogs/GanStanceAnimations.d.ts +1 -1
  74. package/lib/systems/animation/catalogs/GanTechniqueAnimations.d.ts +1 -1
  75. package/lib/systems/animation/catalogs/GeonStanceAnimations.d.ts +1 -1
  76. package/lib/systems/animation/catalogs/GonGuardPoses.d.ts +1 -1
  77. package/lib/systems/animation/catalogs/GonStanceAnimations.d.ts +1 -1
  78. package/lib/systems/animation/catalogs/GonTechniqueAnimations.d.ts +1 -1
  79. package/lib/systems/animation/catalogs/GrapplingAnimations.d.ts +1 -1
  80. package/lib/systems/animation/catalogs/JinStanceAnimations.d.ts +1 -1
  81. package/lib/systems/animation/catalogs/JinTechniqueAnimations.d.ts +1 -1
  82. package/lib/systems/animation/catalogs/KickAnimations.d.ts +1 -1
  83. package/lib/systems/animation/catalogs/LiStanceAnimations.d.ts +1 -1
  84. package/lib/systems/animation/catalogs/LiTechniqueAnimations.d.ts +1 -1
  85. package/lib/systems/animation/catalogs/MovementAnimations.d.ts +1 -1
  86. package/lib/systems/animation/catalogs/PunchAnimations.d.ts +1 -1
  87. package/lib/systems/animation/catalogs/SonStanceAnimations.d.ts +1 -1
  88. package/lib/systems/animation/catalogs/SonTechniqueAnimations.d.ts +1 -1
  89. package/lib/systems/animation/catalogs/SpecializedPunchAnimations.d.ts +1 -1
  90. package/lib/systems/animation/catalogs/StanceAnimations.d.ts +1 -1
  91. package/lib/systems/animation/catalogs/StanceAttackAnimations.d.ts +2 -2
  92. package/lib/systems/animation/catalogs/StanceGuardPoses.d.ts +2 -2
  93. package/lib/systems/animation/catalogs/StanceIdleAnimations.d.ts +2 -2
  94. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.d.ts +1 -1
  95. package/lib/systems/animation/catalogs/StepSkeletalAnimations.d.ts +1 -1
  96. package/lib/systems/animation/catalogs/TaeJointLockAnimations.d.ts +1 -1
  97. package/lib/systems/animation/catalogs/TaeStanceAnimations.d.ts +1 -1
  98. package/lib/systems/animation/core/AnimationOptimizations.d.ts +1 -1
  99. package/lib/systems/animation/core/AnimationRegistry.d.ts +1 -1
  100. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  101. package/lib/systems/animation/core/AnimationStateMachine.d.ts +2 -2
  102. package/lib/systems/animation/core/AnimationTransitions.d.ts +2 -2
  103. package/lib/systems/animation/core/LateralityTransform.d.ts +1 -1
  104. package/lib/systems/animation/core/RecoveryPhaseEnhancer.d.ts +1 -1
  105. package/lib/systems/animation/core/TechniqueAnimationMapper.d.ts +1 -1
  106. package/lib/systems/animation/core/TrigramAnimationMapping.d.ts +2 -2
  107. package/lib/systems/animation/core/TrigramStanceTransitions.d.ts +2 -2
  108. package/lib/systems/animation/systems/BodyFacingSystem.d.ts +1 -1
  109. package/lib/systems/animation/systems/FacialExpressions.d.ts +1 -1
  110. package/lib/systems/animation/systems/FallAnimations.d.ts +1 -1
  111. package/lib/systems/animation/systems/HeadMovements.d.ts +1 -1
  112. package/lib/systems/animation/systems/MuscleActivation.d.ts +2 -2
  113. package/lib/systems/animation/systems/RecoveryVisualization.d.ts +1 -1
  114. package/lib/systems/bodypart/BodyPartDamageIntegration.d.ts +1 -1
  115. package/lib/systems/bodypart/BodyPartHealthSystem.d.ts +1 -1
  116. package/lib/systems/bodypart/CombatInjuryIntegration.d.ts +1 -1
  117. package/lib/systems/bodypart/PlayerInjuryTrackingManager.d.ts +1 -1
  118. package/lib/systems/bodypart/types.d.ts +1 -1
  119. package/lib/systems/breathing/BreathingDisruptionSystem.d.ts +3 -3
  120. package/lib/systems/breathing/feedback.d.ts +1 -1
  121. package/lib/systems/combat/BalanceSystem.d.ts +2 -2
  122. package/lib/systems/combat/ConsciousnessSystem.d.ts +2 -2
  123. package/lib/systems/combat/GrappleSystem.d.ts +2 -2
  124. package/lib/systems/combat/PainResponseSystem.d.ts +1 -1
  125. package/lib/systems/combat/painConsciousnessUtils.d.ts +1 -1
  126. package/lib/systems/combat/types.d.ts +1 -1
  127. package/lib/systems/game.d.ts +1 -1
  128. package/lib/systems/movement/InjuryMovementModifier.d.ts +1 -1
  129. package/lib/systems/movement/integration.d.ts +1 -1
  130. package/lib/systems/physics/AttackMovementPhysics.d.ts +2 -2
  131. package/lib/systems/physics/CoordinateMapper.d.ts +2 -2
  132. package/lib/systems/physics/KnockbackPhysics.d.ts +1 -1
  133. package/lib/systems/physics/MovementPhysics.d.ts +2 -2
  134. package/lib/systems/physics/PhysicalReachCalculator.d.ts +2 -2
  135. package/lib/systems/physics/SpeedModifierSystem.d.ts +1 -1
  136. package/lib/systems/player.d.ts +8 -8
  137. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  138. package/lib/systems/trigram/techniques/DarkOpsTechniques.d.ts +1 -1
  139. package/lib/systems/trigram/techniques/GamTechniques.d.ts +1 -1
  140. package/lib/systems/trigram/techniques/GanTechniques.d.ts +1 -1
  141. package/lib/systems/trigram/techniques/GeonTechniques.d.ts +1 -1
  142. package/lib/systems/trigram/techniques/JinTechniques.d.ts +1 -1
  143. package/lib/systems/trigram/techniques/LiTechniques.d.ts +1 -1
  144. package/lib/systems/trigram/techniques/SonTechniques.d.ts +1 -1
  145. package/lib/systems/trigram/techniques/TaeTechniques.d.ts +1 -1
  146. package/lib/systems/trigram/types/GonTechniqueExtensions.d.ts +1 -1
  147. package/lib/systems/types.d.ts +3 -3
  148. package/lib/systems/vitalpoint/KoreanAnatomy.d.ts +1 -1
  149. package/lib/systems/vitalpoint/VitalPointsData.d.ts +1 -1
  150. package/lib/test/test-utils.d.ts +1 -1
  151. package/lib/types/physicsConstants.d.ts +1 -1
  152. package/lib/utils/characterScaling.d.ts +1 -1
  153. package/lib/utils/clothingMaterials.d.ts +1 -1
  154. package/lib/utils/inputSystem.d.ts +1 -1
  155. package/lib/utils/particlePool.d.ts +1 -1
  156. package/lib/utils/physicalAttributeValidation.d.ts +1 -1
  157. package/lib/utils/sharedPhysicsConfig.d.ts +1 -1
  158. package/lib/utils/skeletonScaling.d.ts +1 -1
  159. package/lib/utils/skeletonScaling.js.map +1 -1
  160. package/package.json +13 -13
@@ -1 +1 @@
1
- {"version":3,"file":"PauseMenu.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenu.tsx"],"sourcesContent":["/**\n * PauseMenu Component - Main pause menu overlay\n *\n * Features:\n * - Resume combat\n * - Restart match\n * - View controls\n * - Adjust settings\n * - Return to menu (with confirmation)\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - WCAG 2.1 Level AA compliant\n * - Keyboard navigation (Arrow keys, Enter, Escape)\n * - Focus indicators with high contrast\n * - ARIA labels for screen readers\n * \n * Refactored to use useKoreanTheme for consistent theming\n */\n\nimport React, { useCallback, useMemo, useState, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport ConfirmDialog from \"../../../../shared/ui/shared/ConfirmDialog\";\nimport ControlsGuide from \"./ControlsGuide\";\nimport QuickSettings from \"./QuickSettings\";\nimport { handleKeyboardNav } from \"../../../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../../../types/AccessibilityTypes\";\nimport { PauseMenuButton } from \"./PauseMenuButton\";\n\nexport interface PauseMenuProps {\n readonly onResume: () => void;\n readonly onRestart: () => void;\n readonly onReturnToMenu: () => void;\n readonly isMobile: boolean;\n}\n\ninterface MenuItem {\n readonly key: string;\n readonly labelKorean: string;\n readonly labelEnglish: string;\n readonly testId: string;\n readonly onClick: () => void;\n readonly icon?: string;\n}\n\n/**\n * PauseMenu - Main pause menu with options and submenus\n */\nexport const PauseMenu: React.FC<PauseMenuProps> = ({\n onResume,\n onRestart,\n onReturnToMenu,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n const themeColors = useMemo(() => ({\n overlayBg: hexToRgbaString(theme.colors.BLACK_SOLID, 0.85),\n accentGold: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n accentGoldGlow: hexToRgbaString(theme.colors.ACCENT_GOLD, 0.6),\n textSecondary: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.8),\n }), [theme.colors.BLACK_SOLID, theme.colors.ACCENT_GOLD, theme.colors.TEXT_SECONDARY]);\n const [activeSubmenu, setActiveSubmenu] = useState<\n \"controls\" | \"settings\" | null\n >(null);\n const [showConfirm, setShowConfirm] = useState<\n \"restart\" | \"menu\" | null\n >(null);\n const [focusedIndex, setFocusedIndex] = useState<number>(0);\n const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const menuItems: MenuItem[] = React.useMemo(\n () => [\n {\n key: \"resume\",\n labelKorean: \"계속\",\n labelEnglish: \"Resume\",\n testId: \"pause-resume-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n onResume();\n },\n icon: \"▶️\",\n },\n {\n key: \"restart\",\n labelKorean: \"재시작\",\n labelEnglish: \"Restart Match\",\n testId: \"pause-restart-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"restart\");\n },\n icon: \"🔄\",\n },\n {\n key: \"controls\",\n labelKorean: \"조작법\",\n labelEnglish: \"Controls\",\n testId: \"pause-controls-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"controls\");\n },\n icon: \"🎮\",\n },\n {\n key: \"settings\",\n labelKorean: \"설정\",\n labelEnglish: \"Settings\",\n testId: \"pause-settings-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"settings\");\n },\n icon: \"⚙️\",\n },\n {\n key: \"menu\",\n labelKorean: \"메인 메뉴\",\n labelEnglish: \"Return to Menu\",\n testId: \"pause-menu-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"menu\");\n },\n icon: \"🏠\",\n },\n ],\n [audio, onResume]\n );\n\n useEffect(() => {\n if (buttonRefs.current[0]) {\n buttonRefs.current[0].focus();\n }\n }, [menuItems]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent, index: number) => {\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n menuItems[index].onClick();\n },\n onCancel: () => {\n onResume();\n },\n onNavigate: (direction) => {\n if (direction === 'up') {\n const newIndex = (index - 1 + menuItems.length) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n } else if (direction === 'down') {\n const newIndex = (index + 1) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n }\n },\n });\n },\n [menuItems, onResume]\n );\n\n\n return (\n <>\n {/* Main Pause Menu */}\n <div\n data-testid=\"pause-menu\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pause-title\"\n aria-describedby=\"pause-hint\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: themeColors.overlayBg,\n backdropFilter: \"blur(8px)\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n }}\n >\n {/* Pause Title */}\n <h1\n id=\"pause-title\"\n data-testid=\"pause-title\"\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: themeColors.accentGold,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"40px\" : \"60px\"} 0`,\n textShadow: `0 0 30px ${themeColors.accentGoldGlow}`,\n textAlign: \"center\",\n }}\n >\n 일시정지 | Paused\n </h1>\n\n {/* Menu Buttons */}\n <div\n role=\"menu\"\n aria-label={createBilingualLabel('일시정지 메뉴', 'Pause Menu').label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n }}\n >\n {menuItems.map((item, index) => (\n <PauseMenuButton\n key={item.key}\n ref={(el) => { buttonRefs.current[index] = el; }}\n labelKorean={item.labelKorean}\n labelEnglish={item.labelEnglish}\n icon={item.icon}\n onClick={item.onClick}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n onKeyDown={(e) => handleKeyDown(e, index)}\n onFocus={() => setFocusedIndex(index)}\n isFocused={focusedIndex === index}\n isMobile={isMobile}\n testId={item.testId}\n />\n ))}\n </div>\n\n {/* ESC hint */}\n <div\n id=\"pause-hint\"\n data-testid=\"pause-hint\"\n style={{\n marginTop: isMobile ? \"40px\" : \"60px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: themeColors.textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n ESC 키를 눌러 계속 | Press ESC to resume\n <br />\n ↑↓ 키로 이동 | Use ↑↓ to navigate\n </div>\n </div>\n\n {/* Controls Guide Submenu */}\n {activeSubmenu === \"controls\" && (\n <ControlsGuide\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Settings Submenu */}\n {activeSubmenu === \"settings\" && (\n <QuickSettings\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Restart Confirmation Dialog */}\n {showConfirm === \"restart\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Restart Match?\"\n titleKorean=\"경기를 재시작하시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 초기화됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onRestart();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Return to Menu Confirmation Dialog */}\n {showConfirm === \"menu\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Return to Menu?\"\n titleKorean=\"메인 메뉴로 돌아가시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 손실됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onReturnToMenu();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n </>\n );\n};\n\nexport default PauseMenu;\n"],"mappings":"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"}
1
+ {"version":3,"file":"PauseMenu.js","names":["_Fragment"],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenu.tsx"],"sourcesContent":["/**\n * PauseMenu Component - Main pause menu overlay\n *\n * Features:\n * - Resume combat\n * - Restart match\n * - View controls\n * - Adjust settings\n * - Return to menu (with confirmation)\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - WCAG 2.1 Level AA compliant\n * - Keyboard navigation (Arrow keys, Enter, Escape)\n * - Focus indicators with high contrast\n * - ARIA labels for screen readers\n * \n * Refactored to use useKoreanTheme for consistent theming\n */\n\nimport React, { useCallback, useMemo, useState, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport ConfirmDialog from \"../../../../shared/ui/shared/ConfirmDialog\";\nimport ControlsGuide from \"./ControlsGuide\";\nimport QuickSettings from \"./QuickSettings\";\nimport { handleKeyboardNav } from \"../../../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../../../types/AccessibilityTypes\";\nimport { PauseMenuButton } from \"./PauseMenuButton\";\n\nexport interface PauseMenuProps {\n readonly onResume: () => void;\n readonly onRestart: () => void;\n readonly onReturnToMenu: () => void;\n readonly isMobile: boolean;\n}\n\ninterface MenuItem {\n readonly key: string;\n readonly labelKorean: string;\n readonly labelEnglish: string;\n readonly testId: string;\n readonly onClick: () => void;\n readonly icon?: string;\n}\n\n/**\n * PauseMenu - Main pause menu with options and submenus\n */\nexport const PauseMenu: React.FC<PauseMenuProps> = ({\n onResume,\n onRestart,\n onReturnToMenu,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n const themeColors = useMemo(() => ({\n overlayBg: hexToRgbaString(theme.colors.BLACK_SOLID, 0.85),\n accentGold: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n accentGoldGlow: hexToRgbaString(theme.colors.ACCENT_GOLD, 0.6),\n textSecondary: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.8),\n }), [theme.colors.BLACK_SOLID, theme.colors.ACCENT_GOLD, theme.colors.TEXT_SECONDARY]);\n const [activeSubmenu, setActiveSubmenu] = useState<\n \"controls\" | \"settings\" | null\n >(null);\n const [showConfirm, setShowConfirm] = useState<\n \"restart\" | \"menu\" | null\n >(null);\n const [focusedIndex, setFocusedIndex] = useState<number>(0);\n const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const menuItems: MenuItem[] = React.useMemo(\n () => [\n {\n key: \"resume\",\n labelKorean: \"계속\",\n labelEnglish: \"Resume\",\n testId: \"pause-resume-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n onResume();\n },\n icon: \"▶️\",\n },\n {\n key: \"restart\",\n labelKorean: \"재시작\",\n labelEnglish: \"Restart Match\",\n testId: \"pause-restart-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"restart\");\n },\n icon: \"🔄\",\n },\n {\n key: \"controls\",\n labelKorean: \"조작법\",\n labelEnglish: \"Controls\",\n testId: \"pause-controls-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"controls\");\n },\n icon: \"🎮\",\n },\n {\n key: \"settings\",\n labelKorean: \"설정\",\n labelEnglish: \"Settings\",\n testId: \"pause-settings-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"settings\");\n },\n icon: \"⚙️\",\n },\n {\n key: \"menu\",\n labelKorean: \"메인 메뉴\",\n labelEnglish: \"Return to Menu\",\n testId: \"pause-menu-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"menu\");\n },\n icon: \"🏠\",\n },\n ],\n [audio, onResume]\n );\n\n useEffect(() => {\n if (buttonRefs.current[0]) {\n buttonRefs.current[0].focus();\n }\n }, [menuItems]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent, index: number) => {\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n menuItems[index].onClick();\n },\n onCancel: () => {\n onResume();\n },\n onNavigate: (direction) => {\n if (direction === 'up') {\n const newIndex = (index - 1 + menuItems.length) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n } else if (direction === 'down') {\n const newIndex = (index + 1) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n }\n },\n });\n },\n [menuItems, onResume]\n );\n\n\n return (\n <>\n {/* Main Pause Menu */}\n <div\n data-testid=\"pause-menu\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pause-title\"\n aria-describedby=\"pause-hint\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: themeColors.overlayBg,\n backdropFilter: \"blur(8px)\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n }}\n >\n {/* Pause Title */}\n <h1\n id=\"pause-title\"\n data-testid=\"pause-title\"\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: themeColors.accentGold,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"40px\" : \"60px\"} 0`,\n textShadow: `0 0 30px ${themeColors.accentGoldGlow}`,\n textAlign: \"center\",\n }}\n >\n 일시정지 | Paused\n </h1>\n\n {/* Menu Buttons */}\n <div\n role=\"menu\"\n aria-label={createBilingualLabel('일시정지 메뉴', 'Pause Menu').label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n }}\n >\n {menuItems.map((item, index) => (\n <PauseMenuButton\n key={item.key}\n ref={(el) => { buttonRefs.current[index] = el; }}\n labelKorean={item.labelKorean}\n labelEnglish={item.labelEnglish}\n icon={item.icon}\n onClick={item.onClick}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n onKeyDown={(e) => handleKeyDown(e, index)}\n onFocus={() => setFocusedIndex(index)}\n isFocused={focusedIndex === index}\n isMobile={isMobile}\n testId={item.testId}\n />\n ))}\n </div>\n\n {/* ESC hint */}\n <div\n id=\"pause-hint\"\n data-testid=\"pause-hint\"\n style={{\n marginTop: isMobile ? \"40px\" : \"60px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: themeColors.textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n ESC 키를 눌러 계속 | Press ESC to resume\n <br />\n ↑↓ 키로 이동 | Use ↑↓ to navigate\n </div>\n </div>\n\n {/* Controls Guide Submenu */}\n {activeSubmenu === \"controls\" && (\n <ControlsGuide\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Settings Submenu */}\n {activeSubmenu === \"settings\" && (\n <QuickSettings\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Restart Confirmation Dialog */}\n {showConfirm === \"restart\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Restart Match?\"\n titleKorean=\"경기를 재시작하시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 초기화됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onRestart();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Return to Menu Confirmation Dialog */}\n {showConfirm === \"menu\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Return to Menu?\"\n titleKorean=\"메인 메뉴로 돌아가시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 손실됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onReturnToMenu();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n </>\n );\n};\n\nexport default PauseMenu;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BloodLossOverlayHtml.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodLossOverlayHtml.tsx"],"sourcesContent":["/**\n * BloodLossOverlayHtml Component - Visual warning for blood loss\n *\n * Displays a pulsing red overlay when blood loss exceeds critical threshold (50%).\n * Uses CSS animation for smooth pulsing effect.\n *\n * NOTE: This component is rendered OUTSIDE the Canvas as part of the HTML overlay.\n * It does NOT use Html from drei - it's a standard React component.\n *\n * @module components/combat/BloodLossOverlayHtml\n * @category Combat UI\n * @korean 출혈오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\nexport interface BloodLossOverlayProps {\n /**\n * Current blood loss amount (0-100)\n * @korean 출혈량\n */\n readonly bloodLoss: number;\n\n /**\n * Mobile responsive mode (reduced pulse intensity)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Multiplier applied to the effect's maximum opacity (0.0-1.0).\n *\n * Use this to soften the fullscreen effect when the 3D arena is already\n * visually compressed (e.g. portrait mobile). Default is `1.0`.\n *\n * @korean 효과강도배수\n */\n readonly intensityScale?: number;\n}\n\n/**\n * BloodLossOverlayHtml - Pulsing red warning for critical blood loss\n *\n * Renders a fullscreen pulsing red overlay when blood loss is 50 or higher.\n * Only visible when bloodLoss is 50 or above; does not render for values below 50.\n * Uses CSS keyframe animation for smooth pulsing effect at 60fps.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when blood loss hasn't changed significantly\n * - Memoized style calculations\n * \n * Accessibility notes:\n * - Purely decorative visual effect; no ARIA role or aria-live region is defined here\n * - Typically rendered within an aria-hidden container so it is ignored by screen readers\n * - Critical status announcements should be provided by separate, semantic HUD components\n *\n * @example\n * ```tsx\n * // Overlay is rendered (bloodLoss >= 50)\n * <BloodLossOverlayHtml bloodLoss={75} isMobile={false} />\n *\n * // Overlay is not rendered (bloodLoss < 50)\n * <BloodLossOverlayHtml bloodLoss={30} isMobile={false} />\n * ```\n */\nexport const BloodLossOverlayHtml = React.memo<BloodLossOverlayProps>(\n ({ bloodLoss, isMobile, intensityScale = 1 }) => {\n const overlayStyle = useMemo(() => {\n const criticalThreshold = 50;\n if (bloodLoss < criticalThreshold) {\n return null;\n }\n\n const clampedBloodLoss = Math.max(\n criticalThreshold,\n Math.min(100, bloodLoss)\n );\n\n const intensity =\n (clampedBloodLoss - criticalThreshold) / (100 - criticalThreshold);\n\n const safeScale = Math.max(0, Math.min(1, intensityScale));\n const maxOpacity = (isMobile ? 0.15 : 0.25) * safeScale;\n const baseOpacity = intensity * maxOpacity;\n\n const rgb = KOREAN_COLORS.BLOODLOSS_INDICATOR;\n const bloodColor = `rgb(${(rgb >> 16) & 255}, ${(rgb >> 8) & 255}, ${\n rgb & 255\n })`;\n\n return {\n position: \"fixed\" as const,\n inset: 0,\n pointerEvents: \"none\" as const,\n backgroundColor: bloodColor,\n [\"--base-opacity\"]: baseOpacity.toString(),\n animation: \"bloodLossPulse 1.5s ease-in-out infinite\",\n transition: \"opacity 0.5s ease-out\",\n zIndex: 55, // Between pain vignette and consciousness blur\n } as React.CSSProperties;\n }, [bloodLoss, isMobile, intensityScale]);\n\n if (bloodLoss < 50 || !overlayStyle) {\n return null;\n }\n\n return (\n <>\n {/* CSS keyframe animation for pulsing with CSS variables */}\n <style>\n {`\n @keyframes bloodLossPulse {\n 0%, 100% {\n opacity: var(--base-opacity);\n }\n 50% {\n opacity: calc(var(--base-opacity) * 1.5);\n }\n }\n `}\n </style>\n <div\n data-testid=\"bloodloss-overlay\"\n style={overlayStyle}\n aria-hidden=\"true\"\n />\n </>\n );\n },\n (prevProps, nextProps) => {\n const wasCritical = prevProps.bloodLoss >= 50;\n const isCritical = nextProps.bloodLoss >= 50;\n \n if (!wasCritical && !isCritical) return true;\n \n if (wasCritical !== isCritical) return false;\n \n return (\n Math.abs(prevProps.bloodLoss - nextProps.bloodLoss) < 5 &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.intensityScale === nextProps.intensityScale\n );\n },\n);\n\nBloodLossOverlayHtml.displayName = \"BloodLossOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,uBAAuB,MAAM,MACvC,EAAE,WAAW,UAAU,iBAAiB,QAAQ;CACjD,MAAM,eAAe,cAAc;EACjC,MAAM,oBAAoB;EAC1B,IAAI,YAAY,mBACd,OAAO;EAaT,MAAM,eAVmB,KAAK,IAC5B,mBACA,KAAK,IAAI,KAAK,SAAS,CAItB,IAAmB,qBAAsB,OAGxB,WAAW,MAAO,OADpB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,cAAc,CACV;EAG9C,MAAM,MAAM,cAAc;EAK1B,OAAO;GACL,UAAU;GACV,OAAO;GACP,eAAe;GACf,iBAAiB,OARQ,OAAO,KAAM,IAAI,IAAK,OAAO,IAAK,IAAI,IAC/D,MAAM,IACP;IAOE,mBAAmB,YAAY,SAAS;GACzC,WAAW;GACX,YAAY;GACZ,QAAQ;EACV;CACF,GAAG;EAAC;EAAW;EAAU;CAAc,CAAC;CAExC,IAAI,YAAY,MAAM,CAAC,cACrB,OAAO;CAGT,OACE,qBAAA,UAAA,EAAA,UAAA,CAEE,oBAAC,SAAD,EAAA,UACG;;;;;;;;;UAUI,CAAA,GACP,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;EACP,eAAY;CACb,CAAA,CACD,EAAA,CAAA;AAEJ,IACC,WAAW,cAAc;CACxB,MAAM,cAAc,UAAU,aAAa;CAC3C,MAAM,aAAa,UAAU,aAAa;CAE1C,IAAI,CAAC,eAAe,CAAC,YAAY,OAAO;CAExC,IAAI,gBAAgB,YAAY,OAAO;CAEvC,OACE,KAAK,IAAI,UAAU,YAAY,UAAU,SAAS,IAAI,KACtD,UAAU,aAAa,UAAU,YACjC,UAAU,mBAAmB,UAAU;AAE3C,CACF;AAEA,qBAAqB,cAAc"}
1
+ {"version":3,"file":"BloodLossOverlayHtml.js","names":["_Fragment"],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodLossOverlayHtml.tsx"],"sourcesContent":["/**\n * BloodLossOverlayHtml Component - Visual warning for blood loss\n *\n * Displays a pulsing red overlay when blood loss exceeds critical threshold (50%).\n * Uses CSS animation for smooth pulsing effect.\n *\n * NOTE: This component is rendered OUTSIDE the Canvas as part of the HTML overlay.\n * It does NOT use Html from drei - it's a standard React component.\n *\n * @module components/combat/BloodLossOverlayHtml\n * @category Combat UI\n * @korean 출혈오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\nexport interface BloodLossOverlayProps {\n /**\n * Current blood loss amount (0-100)\n * @korean 출혈량\n */\n readonly bloodLoss: number;\n\n /**\n * Mobile responsive mode (reduced pulse intensity)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Multiplier applied to the effect's maximum opacity (0.0-1.0).\n *\n * Use this to soften the fullscreen effect when the 3D arena is already\n * visually compressed (e.g. portrait mobile). Default is `1.0`.\n *\n * @korean 효과강도배수\n */\n readonly intensityScale?: number;\n}\n\n/**\n * BloodLossOverlayHtml - Pulsing red warning for critical blood loss\n *\n * Renders a fullscreen pulsing red overlay when blood loss is 50 or higher.\n * Only visible when bloodLoss is 50 or above; does not render for values below 50.\n * Uses CSS keyframe animation for smooth pulsing effect at 60fps.\n *\n * Optimized with React.memo for 60fps performance:\n * - Prevents re-renders when blood loss hasn't changed significantly\n * - Memoized style calculations\n * \n * Accessibility notes:\n * - Purely decorative visual effect; no ARIA role or aria-live region is defined here\n * - Typically rendered within an aria-hidden container so it is ignored by screen readers\n * - Critical status announcements should be provided by separate, semantic HUD components\n *\n * @example\n * ```tsx\n * // Overlay is rendered (bloodLoss >= 50)\n * <BloodLossOverlayHtml bloodLoss={75} isMobile={false} />\n *\n * // Overlay is not rendered (bloodLoss < 50)\n * <BloodLossOverlayHtml bloodLoss={30} isMobile={false} />\n * ```\n */\nexport const BloodLossOverlayHtml = React.memo<BloodLossOverlayProps>(\n ({ bloodLoss, isMobile, intensityScale = 1 }) => {\n const overlayStyle = useMemo(() => {\n const criticalThreshold = 50;\n if (bloodLoss < criticalThreshold) {\n return null;\n }\n\n const clampedBloodLoss = Math.max(\n criticalThreshold,\n Math.min(100, bloodLoss)\n );\n\n const intensity =\n (clampedBloodLoss - criticalThreshold) / (100 - criticalThreshold);\n\n const safeScale = Math.max(0, Math.min(1, intensityScale));\n const maxOpacity = (isMobile ? 0.15 : 0.25) * safeScale;\n const baseOpacity = intensity * maxOpacity;\n\n const rgb = KOREAN_COLORS.BLOODLOSS_INDICATOR;\n const bloodColor = `rgb(${(rgb >> 16) & 255}, ${(rgb >> 8) & 255}, ${\n rgb & 255\n })`;\n\n return {\n position: \"fixed\" as const,\n inset: 0,\n pointerEvents: \"none\" as const,\n backgroundColor: bloodColor,\n [\"--base-opacity\"]: baseOpacity.toString(),\n animation: \"bloodLossPulse 1.5s ease-in-out infinite\",\n transition: \"opacity 0.5s ease-out\",\n zIndex: 55, // Between pain vignette and consciousness blur\n } as React.CSSProperties;\n }, [bloodLoss, isMobile, intensityScale]);\n\n if (bloodLoss < 50 || !overlayStyle) {\n return null;\n }\n\n return (\n <>\n {/* CSS keyframe animation for pulsing with CSS variables */}\n <style>\n {`\n @keyframes bloodLossPulse {\n 0%, 100% {\n opacity: var(--base-opacity);\n }\n 50% {\n opacity: calc(var(--base-opacity) * 1.5);\n }\n }\n `}\n </style>\n <div\n data-testid=\"bloodloss-overlay\"\n style={overlayStyle}\n aria-hidden=\"true\"\n />\n </>\n );\n },\n (prevProps, nextProps) => {\n const wasCritical = prevProps.bloodLoss >= 50;\n const isCritical = nextProps.bloodLoss >= 50;\n \n if (!wasCritical && !isCritical) return true;\n \n if (wasCritical !== isCritical) return false;\n \n return (\n Math.abs(prevProps.bloodLoss - nextProps.bloodLoss) < 5 &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.intensityScale === nextProps.intensityScale\n );\n },\n);\n\nBloodLossOverlayHtml.displayName = \"BloodLossOverlayHtml\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAa,uBAAuB,MAAM,MACvC,EAAE,WAAW,UAAU,iBAAiB,QAAQ;CACjD,MAAM,eAAe,cAAc;EACjC,MAAM,oBAAoB;EAC1B,IAAI,YAAY,mBACd,OAAO;EAaT,MAAM,eAVmB,KAAK,IAC5B,mBACA,KAAK,IAAI,KAAK,SAAS,CAItB,IAAmB,qBAAsB,OAGxB,WAAW,MAAO,OADpB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,cAAc,CACV;EAG9C,MAAM,MAAM,cAAc;EAK1B,OAAO;GACL,UAAU;GACV,OAAO;GACP,eAAe;GACf,iBAAiB,OARQ,OAAO,KAAM,IAAI,IAAK,OAAO,IAAK,IAAI,IAC/D,MAAM,IACP;IAOE,mBAAmB,YAAY,SAAS;GACzC,WAAW;GACX,YAAY;GACZ,QAAQ;EACV;CACF,GAAG;EAAC;EAAW;EAAU;CAAc,CAAC;CAExC,IAAI,YAAY,MAAM,CAAC,cACrB,OAAO;CAGT,OACE,qBAAAA,UAAA,EAAA,UAAA,CAEE,oBAAC,SAAD,EAAA,UACG;;;;;;;;;UAUI,CAAA,GACP,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;EACP,eAAY;CACb,CAAA,CACD,EAAA,CAAA;AAEJ,IACC,WAAW,cAAc;CACxB,MAAM,cAAc,UAAU,aAAa;CAC3C,MAAM,aAAa,UAAU,aAAa;CAE1C,IAAI,CAAC,eAAe,CAAC,YAAY,OAAO;CAExC,IAAI,gBAAgB,YAAY,OAAO;CAEvC,OACE,KAAK,IAAI,UAAU,YAAY,UAAU,SAAS,IAAI,KACtD,UAAU,aAAa,UAAU,YACjC,UAAU,mBAAmB,UAAU;AAE3C,CACF;AAEA,qBAAqB,cAAc"}
@@ -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":["_Fragment"],"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 +1 @@
1
- {"version":3,"file":"CombatParticleEffects3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/CombatParticleEffects3D.tsx"],"sourcesContent":["/**\n * CombatParticleEffects3D - Particle effects coordinator for combat\n * 전투 입자 효과 통합 관리\n *\n * Maps HitEffect events to advanced particle effects:\n * - BloodViscosity3D for blood physics on hits\n * - InternalDamage3D for organ damage visualization on vital point strikes\n * - ParticleAudio3D for synchronized combat audio\n *\n * @module components/effects\n * @category Combat Effects\n * @korean 전투입자효과\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HitEffect } from \"../../../../../systems\";\nimport { HitEffectType } from \"../../../../../systems/effects\";\nimport {\n BloodViscosity3D,\n type BloodViscosityEffect,\n type ViscosityType,\n} from \"./BloodViscosity3D\";\nimport {\n InternalDamage3D,\n type InternalDamageEffect,\n type OrganType,\n type PenetrationDepth,\n} from \"./InternalDamage3D\";\nimport {\n ParticleAudio3D,\n type ParticleAudioTrigger,\n type ParticleEffectType,\n} from \"./ParticleAudio3D\";\n\n/**\n * Props for CombatParticleEffects3D\n */\nexport interface CombatParticleEffects3DProps {\n /** Active hit effects from the combat state */\n readonly hitEffects: readonly HitEffect[];\n /** Whether effects are enabled */\n readonly enabled?: boolean;\n /** Mobile optimization flag */\n readonly isMobile?: boolean;\n}\n\n/**\n * Map HitEffectType → blood viscosity type\n * 타격 유형 → 혈액 점도 매핑\n */\nfunction getViscosityForHitType(type: HitEffectType): ViscosityType | null {\n switch (type) {\n case HitEffectType.HIT:\n return \"thin\";\n case HitEffectType.COUNTER:\n return \"medium\";\n case HitEffectType.CRITICAL_HIT:\n return \"thick\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"gout\";\n default:\n return null;\n }\n}\n\n/**\n * Map HitEffectType → organ type based on strike position\n * 급소 유형 → 장기 매핑 (타격 위치 기반)\n */\nfunction getOrganForPosition(\n position: { x: number; y: number } | undefined,\n): OrganType {\n if (!position) return \"stomach\";\n const y = position.y;\n if (y > 1.4) return \"heart\"; // 심장 - upper chest\n if (y > 1.1) return \"stomach\"; // 명치 - solar plexus\n if (y > 0.8) return \"liver\"; // 간 - right side mid-torso\n if (y > 0.5) return \"spleen\"; // 비장 - left side mid-torso\n return \"kidney\"; // 신장 - lower back\n}\n\n/**\n * Map HitEffect intensity → penetration depth\n * 타격 강도 → 관통 깊이 매핑\n */\nfunction getPenetrationDepth(intensity: number): PenetrationDepth {\n if (intensity >= 1.5) return \"critical\";\n if (intensity >= 1.0) return \"deep\";\n if (intensity >= 0.5) return \"shallow\";\n return \"surface\";\n}\n\n/**\n * Map HitEffectType → audio effect type\n * 타격 유형 → 오디오 효과 매핑\n */\nfunction getAudioEffectType(type: HitEffectType): ParticleEffectType | null {\n switch (type) {\n case HitEffectType.HIT:\n case HitEffectType.COUNTER:\n return \"viscosity\";\n case HitEffectType.CRITICAL_HIT:\n return \"bone\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"organ\";\n case HitEffectType.BLOCK:\n case HitEffectType.PARRY:\n return \"bone\";\n default:\n return null;\n }\n}\n\n/** Maximum concurrent effects to prevent performance issues */\nconst MAX_BLOOD_EFFECTS = 8;\nconst MAX_ORGAN_EFFECTS = 4;\n\n/**\n * Simple deterministic hash from string → [0, 1)\n * Used for deterministic direction calculations inside useMemo\n */\nfunction hashToFloat(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 31 + str.charCodeAt(i)) | 0;\n }\n return Math.abs(hash % 10000) / 10000;\n}\n\n/**\n * CombatParticleEffects3D - Coordinates particle effects for combat\n *\n * Converts HitEffect events into:\n * - Blood viscosity particles (thin/medium/thick/gout based on strike type)\n * - Internal organ damage pulses (for vital point strikes)\n * - Synchronized audio triggers\n *\n * @korean 전투 입자 효과 통합 (타격→혈액물리+장기손상+오디오)\n */\nexport const CombatParticleEffects3D: React.FC<\n CombatParticleEffects3DProps\n> = ({ hitEffects, enabled = true, isMobile = false }) => {\n const processedIds = useRef<Set<string>>(new Set());\n\n const [bloodEffects, setBloodEffects] = useState<BloodViscosityEffect[]>([]);\n const [organEffects, setOrganEffects] = useState<InternalDamageEffect[]>([]);\n const [audioTriggers, setAudioTriggers] = useState<ParticleAudioTrigger[]>(\n [],\n );\n\n useEffect(() => {\n const newBlood: BloodViscosityEffect[] = [];\n const newOrgan: InternalDamageEffect[] = [];\n const newAudio: ParticleAudioTrigger[] = [];\n\n for (const hit of hitEffects) {\n if (processedIds.current.has(hit.id)) continue;\n processedIds.current.add(hit.id);\n\n const pos: [number, number, number] = [\n hit.position?.x ?? 0,\n hit.position?.y ?? 1.0,\n 0,\n ];\n\n const viscosity = getViscosityForHitType(hit.type);\n if (viscosity) {\n const angle = hashToFloat(hit.id) * Math.PI * 2;\n const ySpread = 0.3 + hashToFloat(hit.id + \"_y\") * 0.4;\n const dir: [number, number, number] = [\n Math.cos(angle) * 0.5,\n ySpread,\n Math.sin(angle) * 0.5,\n ];\n\n newBlood.push({\n id: `blood_${hit.id}`,\n position: pos,\n direction: dir,\n viscosityType: viscosity,\n intensity: Math.min(1, hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n if (hit.type === HitEffectType.VITAL_POINT_STRIKE) {\n newOrgan.push({\n id: `organ_${hit.id}`,\n position: pos,\n organType: getOrganForPosition(hit.position),\n penetrationDepth: getPenetrationDepth(hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n const audioType = getAudioEffectType(hit.type);\n if (audioType) {\n newAudio.push({\n effectType: audioType,\n intensity: Math.min(1, hit.intensity),\n timestamp: hit.timestamp,\n });\n }\n }\n\n if (newBlood.length > 0) {\n setBloodEffects((prev) =>\n [...prev, ...newBlood].slice(-MAX_BLOOD_EFFECTS),\n );\n }\n if (newOrgan.length > 0) {\n setOrganEffects((prev) =>\n [...prev, ...newOrgan].slice(-MAX_ORGAN_EFFECTS),\n );\n }\n if (newAudio.length > 0) {\n setAudioTriggers((prev) => [...prev, ...newAudio]);\n }\n\n if (processedIds.current.size > 500) {\n const arr = Array.from(processedIds.current);\n processedIds.current = new Set(arr.slice(-250));\n }\n }, [hitEffects]);\n\n const handleBloodComplete = useCallback((id: string) => {\n setBloodEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleOrganComplete = useCallback((id: string) => {\n setOrganEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleAudioProcessed = useCallback((timestamp: number) => {\n setAudioTriggers((prev) => prev.filter((t) => t.timestamp !== timestamp));\n }, []);\n\n if (!enabled) return null;\n\n return (\n <>\n {/* Blood Viscosity Particles - 혈액 점도 입자 */}\n <BloodViscosity3D\n effects={bloodEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleBloodComplete}\n />\n\n {/* Internal Organ Damage Pulses - 장기 손상 시각화 */}\n <InternalDamage3D\n effects={organEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleOrganComplete}\n />\n\n {/* Audio Coordination - 입자 효과 오디오 동기화 */}\n <ParticleAudio3D\n triggers={audioTriggers}\n enabled={enabled}\n onTriggerProcessed={handleAudioProcessed}\n />\n </>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CombatParticleEffects3D.js","names":["_Fragment"],"sources":["../../../../../../src/components/screens/combat/components/effects/CombatParticleEffects3D.tsx"],"sourcesContent":["/**\n * CombatParticleEffects3D - Particle effects coordinator for combat\n * 전투 입자 효과 통합 관리\n *\n * Maps HitEffect events to advanced particle effects:\n * - BloodViscosity3D for blood physics on hits\n * - InternalDamage3D for organ damage visualization on vital point strikes\n * - ParticleAudio3D for synchronized combat audio\n *\n * @module components/effects\n * @category Combat Effects\n * @korean 전투입자효과\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HitEffect } from \"../../../../../systems\";\nimport { HitEffectType } from \"../../../../../systems/effects\";\nimport {\n BloodViscosity3D,\n type BloodViscosityEffect,\n type ViscosityType,\n} from \"./BloodViscosity3D\";\nimport {\n InternalDamage3D,\n type InternalDamageEffect,\n type OrganType,\n type PenetrationDepth,\n} from \"./InternalDamage3D\";\nimport {\n ParticleAudio3D,\n type ParticleAudioTrigger,\n type ParticleEffectType,\n} from \"./ParticleAudio3D\";\n\n/**\n * Props for CombatParticleEffects3D\n */\nexport interface CombatParticleEffects3DProps {\n /** Active hit effects from the combat state */\n readonly hitEffects: readonly HitEffect[];\n /** Whether effects are enabled */\n readonly enabled?: boolean;\n /** Mobile optimization flag */\n readonly isMobile?: boolean;\n}\n\n/**\n * Map HitEffectType → blood viscosity type\n * 타격 유형 → 혈액 점도 매핑\n */\nfunction getViscosityForHitType(type: HitEffectType): ViscosityType | null {\n switch (type) {\n case HitEffectType.HIT:\n return \"thin\";\n case HitEffectType.COUNTER:\n return \"medium\";\n case HitEffectType.CRITICAL_HIT:\n return \"thick\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"gout\";\n default:\n return null;\n }\n}\n\n/**\n * Map HitEffectType → organ type based on strike position\n * 급소 유형 → 장기 매핑 (타격 위치 기반)\n */\nfunction getOrganForPosition(\n position: { x: number; y: number } | undefined,\n): OrganType {\n if (!position) return \"stomach\";\n const y = position.y;\n if (y > 1.4) return \"heart\"; // 심장 - upper chest\n if (y > 1.1) return \"stomach\"; // 명치 - solar plexus\n if (y > 0.8) return \"liver\"; // 간 - right side mid-torso\n if (y > 0.5) return \"spleen\"; // 비장 - left side mid-torso\n return \"kidney\"; // 신장 - lower back\n}\n\n/**\n * Map HitEffect intensity → penetration depth\n * 타격 강도 → 관통 깊이 매핑\n */\nfunction getPenetrationDepth(intensity: number): PenetrationDepth {\n if (intensity >= 1.5) return \"critical\";\n if (intensity >= 1.0) return \"deep\";\n if (intensity >= 0.5) return \"shallow\";\n return \"surface\";\n}\n\n/**\n * Map HitEffectType → audio effect type\n * 타격 유형 → 오디오 효과 매핑\n */\nfunction getAudioEffectType(type: HitEffectType): ParticleEffectType | null {\n switch (type) {\n case HitEffectType.HIT:\n case HitEffectType.COUNTER:\n return \"viscosity\";\n case HitEffectType.CRITICAL_HIT:\n return \"bone\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"organ\";\n case HitEffectType.BLOCK:\n case HitEffectType.PARRY:\n return \"bone\";\n default:\n return null;\n }\n}\n\n/** Maximum concurrent effects to prevent performance issues */\nconst MAX_BLOOD_EFFECTS = 8;\nconst MAX_ORGAN_EFFECTS = 4;\n\n/**\n * Simple deterministic hash from string → [0, 1)\n * Used for deterministic direction calculations inside useMemo\n */\nfunction hashToFloat(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 31 + str.charCodeAt(i)) | 0;\n }\n return Math.abs(hash % 10000) / 10000;\n}\n\n/**\n * CombatParticleEffects3D - Coordinates particle effects for combat\n *\n * Converts HitEffect events into:\n * - Blood viscosity particles (thin/medium/thick/gout based on strike type)\n * - Internal organ damage pulses (for vital point strikes)\n * - Synchronized audio triggers\n *\n * @korean 전투 입자 효과 통합 (타격→혈액물리+장기손상+오디오)\n */\nexport const CombatParticleEffects3D: React.FC<\n CombatParticleEffects3DProps\n> = ({ hitEffects, enabled = true, isMobile = false }) => {\n const processedIds = useRef<Set<string>>(new Set());\n\n const [bloodEffects, setBloodEffects] = useState<BloodViscosityEffect[]>([]);\n const [organEffects, setOrganEffects] = useState<InternalDamageEffect[]>([]);\n const [audioTriggers, setAudioTriggers] = useState<ParticleAudioTrigger[]>(\n [],\n );\n\n useEffect(() => {\n const newBlood: BloodViscosityEffect[] = [];\n const newOrgan: InternalDamageEffect[] = [];\n const newAudio: ParticleAudioTrigger[] = [];\n\n for (const hit of hitEffects) {\n if (processedIds.current.has(hit.id)) continue;\n processedIds.current.add(hit.id);\n\n const pos: [number, number, number] = [\n hit.position?.x ?? 0,\n hit.position?.y ?? 1.0,\n 0,\n ];\n\n const viscosity = getViscosityForHitType(hit.type);\n if (viscosity) {\n const angle = hashToFloat(hit.id) * Math.PI * 2;\n const ySpread = 0.3 + hashToFloat(hit.id + \"_y\") * 0.4;\n const dir: [number, number, number] = [\n Math.cos(angle) * 0.5,\n ySpread,\n Math.sin(angle) * 0.5,\n ];\n\n newBlood.push({\n id: `blood_${hit.id}`,\n position: pos,\n direction: dir,\n viscosityType: viscosity,\n intensity: Math.min(1, hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n if (hit.type === HitEffectType.VITAL_POINT_STRIKE) {\n newOrgan.push({\n id: `organ_${hit.id}`,\n position: pos,\n organType: getOrganForPosition(hit.position),\n penetrationDepth: getPenetrationDepth(hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n const audioType = getAudioEffectType(hit.type);\n if (audioType) {\n newAudio.push({\n effectType: audioType,\n intensity: Math.min(1, hit.intensity),\n timestamp: hit.timestamp,\n });\n }\n }\n\n if (newBlood.length > 0) {\n setBloodEffects((prev) =>\n [...prev, ...newBlood].slice(-MAX_BLOOD_EFFECTS),\n );\n }\n if (newOrgan.length > 0) {\n setOrganEffects((prev) =>\n [...prev, ...newOrgan].slice(-MAX_ORGAN_EFFECTS),\n );\n }\n if (newAudio.length > 0) {\n setAudioTriggers((prev) => [...prev, ...newAudio]);\n }\n\n if (processedIds.current.size > 500) {\n const arr = Array.from(processedIds.current);\n processedIds.current = new Set(arr.slice(-250));\n }\n }, [hitEffects]);\n\n const handleBloodComplete = useCallback((id: string) => {\n setBloodEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleOrganComplete = useCallback((id: string) => {\n setOrganEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleAudioProcessed = useCallback((timestamp: number) => {\n setAudioTriggers((prev) => prev.filter((t) => t.timestamp !== timestamp));\n }, []);\n\n if (!enabled) return null;\n\n return (\n <>\n {/* Blood Viscosity Particles - 혈액 점도 입자 */}\n <BloodViscosity3D\n effects={bloodEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleBloodComplete}\n />\n\n {/* Internal Organ Damage Pulses - 장기 손상 시각화 */}\n <InternalDamage3D\n effects={organEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleOrganComplete}\n />\n\n {/* Audio Coordination - 입자 효과 오디오 동기화 */}\n <ParticleAudio3D\n triggers={audioTriggers}\n enabled={enabled}\n onTriggerProcessed={handleAudioProcessed}\n />\n </>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAS,uBAAuB,MAA2C;CACzE,QAAQ,MAAR;EACE,KAAK,cAAc,KACjB,OAAO;EACT,KAAK,cAAc,SACjB,OAAO;EACT,KAAK,cAAc,cACjB,OAAO;EACT,KAAK,cAAc,oBACjB,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;;AAMA,SAAS,oBACP,UACW;CACX,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,IAAI,SAAS;CACnB,IAAI,IAAI,KAAK,OAAO;CACpB,IAAI,IAAI,KAAK,OAAO;CACpB,IAAI,IAAI,IAAK,OAAO;CACpB,IAAI,IAAI,IAAK,OAAO;CACpB,OAAO;AACT;;;;;AAMA,SAAS,oBAAoB,WAAqC;CAChE,IAAI,aAAa,KAAK,OAAO;CAC7B,IAAI,aAAa,GAAK,OAAO;CAC7B,IAAI,aAAa,IAAK,OAAO;CAC7B,OAAO;AACT;;;;;AAMA,SAAS,mBAAmB,MAAgD;CAC1E,QAAQ,MAAR;EACE,KAAK,cAAc;EACnB,KAAK,cAAc,SACjB,OAAO;EACT,KAAK,cAAc,cACjB,OAAO;EACT,KAAK,cAAc,oBACjB,OAAO;EACT,KAAK,cAAc;EACnB,KAAK,cAAc,OACjB,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;;AAUA,SAAS,YAAY,KAAqB;CACxC,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAC9B,OAAQ,OAAO,KAAK,IAAI,WAAW,CAAC,IAAK;CAE3C,OAAO,KAAK,IAAI,OAAO,GAAK,IAAI;AAClC;;;;;;;;;;;AAYA,IAAa,2BAER,EAAE,YAAY,UAAU,MAAM,WAAW,YAAY;CACxD,MAAM,eAAe,uBAAoB,IAAI,IAAI,CAAC;CAElD,MAAM,CAAC,cAAc,mBAAmB,SAAiC,CAAC,CAAC;CAC3E,MAAM,CAAC,cAAc,mBAAmB,SAAiC,CAAC,CAAC;CAC3E,MAAM,CAAC,eAAe,oBAAoB,SACxC,CAAC,CACH;CAEA,gBAAgB;EACd,MAAM,WAAmC,CAAC;EAC1C,MAAM,WAAmC,CAAC;EAC1C,MAAM,WAAmC,CAAC;EAE1C,KAAK,MAAM,OAAO,YAAY;GAC5B,IAAI,aAAa,QAAQ,IAAI,IAAI,EAAE,GAAG;GACtC,aAAa,QAAQ,IAAI,IAAI,EAAE;GAE/B,MAAM,MAAgC;IACpC,IAAI,UAAU,KAAK;IACnB,IAAI,UAAU,KAAK;IACnB;GACF;GAEA,MAAM,YAAY,uBAAuB,IAAI,IAAI;GACjD,IAAI,WAAW;IACb,MAAM,QAAQ,YAAY,IAAI,EAAE,IAAI,KAAK,KAAK;IAC9C,MAAM,UAAU,KAAM,YAAY,IAAI,KAAK,IAAI,IAAI;IACnD,MAAM,MAAgC;KACpC,KAAK,IAAI,KAAK,IAAI;KAClB;KACA,KAAK,IAAI,KAAK,IAAI;IACpB;IAEA,SAAS,KAAK;KACZ,IAAI,SAAS,IAAI;KACjB,UAAU;KACV,WAAW;KACX,eAAe;KACf,WAAW,KAAK,IAAI,GAAG,IAAI,SAAS;KACpC,WAAW,IAAI;IACjB,CAAC;GACH;GAEA,IAAI,IAAI,SAAS,cAAc,oBAC7B,SAAS,KAAK;IACZ,IAAI,SAAS,IAAI;IACjB,UAAU;IACV,WAAW,oBAAoB,IAAI,QAAQ;IAC3C,kBAAkB,oBAAoB,IAAI,SAAS;IACnD,WAAW,IAAI;GACjB,CAAC;GAGH,MAAM,YAAY,mBAAmB,IAAI,IAAI;GAC7C,IAAI,WACF,SAAS,KAAK;IACZ,YAAY;IACZ,WAAW,KAAK,IAAI,GAAG,IAAI,SAAS;IACpC,WAAW,IAAI;GACjB,CAAC;EAEL;EAEA,IAAI,SAAS,SAAS,GACpB,iBAAiB,SACf,CAAC,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,MAAM,EAAkB,CACjD;EAEF,IAAI,SAAS,SAAS,GACpB,iBAAiB,SACf,CAAC,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,MAAM,EAAkB,CACjD;EAEF,IAAI,SAAS,SAAS,GACpB,kBAAkB,SAAS,CAAC,GAAG,MAAM,GAAG,QAAQ,CAAC;EAGnD,IAAI,aAAa,QAAQ,OAAO,KAAK;GACnC,MAAM,MAAM,MAAM,KAAK,aAAa,OAAO;GAC3C,aAAa,UAAU,IAAI,IAAI,IAAI,MAAM,IAAI,CAAC;EAChD;CACF,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,sBAAsB,aAAa,OAAe;EACtD,iBAAiB,SAAS,KAAK,QAAQ,MAAM,EAAE,OAAO,EAAE,CAAC;CAC3D,GAAG,CAAC,CAAC;CAEL,MAAM,sBAAsB,aAAa,OAAe;EACtD,iBAAiB,SAAS,KAAK,QAAQ,MAAM,EAAE,OAAO,EAAE,CAAC;CAC3D,GAAG,CAAC,CAAC;CAEL,MAAM,uBAAuB,aAAa,cAAsB;EAC9D,kBAAkB,SAAS,KAAK,QAAQ,MAAM,EAAE,cAAc,SAAS,CAAC;CAC1E,GAAG,CAAC,CAAC;CAEL,IAAI,CAAC,SAAS,OAAO;CAErB,OACE,qBAAAA,UAAA,EAAA,UAAA;EAEE,oBAAC,kBAAD;GACE,SAAS;GACA;GACC;GACV,kBAAkB;EACnB,CAAA;EAGD,oBAAC,kBAAD;GACE,SAAS;GACA;GACC;GACV,kBAAkB;EACnB,CAAA;EAGD,oBAAC,iBAAD;GACE,UAAU;GACD;GACT,oBAAoB;EACrB,CAAA;CACD,EAAA,CAAA;AAEN"}
@@ -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":["_Fragment"],"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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"}
@@ -82,7 +82,7 @@ export type { HitEffects3DProps } from "../../../../shared/three/effects/HitEffe
82
82
  * DustClouds3D,
83
83
  * type ImpactSparkEffect,
84
84
  * type DustCloudEffect,
85
- * } from '../../../../effects/index.d.ts';
85
+ * } from '../../../../effects/index.js';
86
86
  *
87
87
  * // Manage effect state
88
88
  * const [sparkEffects, setSparkEffects] = useState<ImpactSparkEffect[]>([]);
@@ -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":["_Fragment"],"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":";;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAS,eACP,OACA,eAIO;CACP,QAAQ,OAAR;EACE,KAAK,SACH,OAAO;GAAE,IAAI;GAAO,IAAI;EAAS;EACnC,KAAK,YACH,OAAO;GAAE,IAAI,OAAO,aAAa;GAAG,IAAI,OAAO,aAAa;EAAE;EAChE,KAAK,SACH,OAAO;GAAE,IAAI;GAAO,IAAI;EAAS;EACnC,SACE,OAAO;CACX;AACF;;;;;;;;;;;;;;;;AAiBA,IAAM,mBAAmB;CACvB,eAAe;CACf,mBAAmB;CACnB,eAAe;CACf,aAAa;AACf;AAEA,IAAa,kBAAiD,EAC5D,YACA,UACA,QACA,WAAW,YACP;CACJ,MAAM,QAAQ,SAAS;CACvB,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAU;CAAS,CAAC;CAE7E,MAAM,EAAE,OAAO,eAAe,gBAAgB,eAAe,aAC3D,kBAAkB,kBAAkB,UAAU;CAEhD,MAAM,gBAAgB,OAAO,KAAK;CAElC,gBAAgB;EACd,IAAI,CAAC,cAAc,SAAS;GAC1B,cAAc,UAAU;GACxB,eAAe;EACjB;CACF,GAAG,CAAC,cAAc,CAAC;CAEnB,gBAAgB;EACd,IAAI,CAAC,MAAM,cAAc;EAEzB,IAAI,UAAU,cAAc,gBAAgB,GAC1C,MAAM,QAAQ,cAAc;OACvB,IAAI,UAAU,SACnB,MAAM,QAAQ,cAAc;CAEhC,GAAG;EAAC;EAAO;EAAe;CAAK,CAAC;CAEhC,MAAM,mBAAmB;EACvB,cAAc;EACd,SAAS;CACX;CAEA,MAAM,cAAc,eAAe,OAAO,aAAa;CAEvD,MAAM,wBAAgC;EACpC,IAAI,UACF,OAAO,UAAU,UAAU,SAAS;EAEtC,OAAO,UAAU,UAAU,UAAU;CACvC;CAEA,MAAM,uBAA+B;EACnC,IAAI,UACF,OAAO,UAAU,UAAU,SAAS;EAEtC,OAAO,UAAU,UAAU,SAAS;CACtC;CAEA,MAAM,eAAe,gBAAgB;CACrC,MAAM,cAAc,eAAe;CAEnC,MAAM,YAAY,cAAc,cAAc,MAAM,OAAO,WAAW,GAAG,CAAC,MAAM,OAAO,WAAW,CAAC;CACnG,MAAM,YAAY,cACV,cAAc,MAAM,OAAO,YAAY,GAC7C,CAAC,MAAM,OAAO,YAAY,CAC5B;CACA,MAAM,SAAS,cACP,cAAc,MAAM,OAAO,kBAAkB,GACnD,CAAC,MAAM,OAAO,kBAAkB,CAClC;CAEA,IAAI,CAAC,YAAY,CAAC,aAChB,OAAO;CAGT,OACE,qBAAAA,UAAA,EAAA,UAAA,CACE,qBAAC,OAAD;EACE,eAAY;EACZ,MAAK;EACL,cAAW;EACX,cAAW;EACX,aAAU;EACV,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,iBAAiB,UAAU,UAAU,GAAG,OAAO,MAAM,GAAG,OAAO;GAC/D,QAAQ;GACR,WAAW,UAAU,UAAU,wBAAwB;EACzD;EAlBF,UAAA,CAoBE,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,YAAY;IACZ,OAAO,UAAU,UAAU,YAAY;IACvC,YAAY,MAAM,WAAW;IAC7B,YAAY,OAAO,UAAU,UAAU,SAAS,OAAO,GACrD,UAAU,UAAU,YAAY;IAElC,WACE,UAAU,UACN,wBACA,UAAU,aACV,iCACA,UAAU,UACV,wBACA;IACN,WAAW;IACX,YAAY;GACd;GACA,eAAY;GApBd,UAAA;IAsBG,YAAY;IACb,oBAAC,MAAD,CAAK,CAAA;IACL,oBAAC,QAAD;KACE,OAAO,EACL,UAAU,YACZ;KAEC,UAAA,YAAY;IACT,CAAA;GACH;EAGJ,CAAA,GAAA,YAAY,UAAU,WACrB,oBAAC,UAAD;GACE,SAAS;GACT,eAAY;GACZ,cAAW;GACX,OAAO;IACL,UAAU;IACV,QAAQ,WAAW,SAAS;IAC5B,SAAS,WAAW,cAAc;IAClC,UAAU,WAAW,SAAS;IAC9B,iBAAiB;IACjB,OAAO;IACP,QAAQ;IACR,cAAc;IACd,YAAY,MAAM,WAAW;IAC7B,YAAY;IACZ,QAAQ;IACR,YAAY;GACd;GACA,eAAe,MAAM;IACnB,EAAE,cAAc,MAAM,YAAY;IAClC,EAAE,cAAc,MAAM,YAAY,YAAY;GAChD;GACA,eAAe,MAAM;IACnB,EAAE,cAAc,MAAM,YAAY;IAClC,EAAE,cAAc,MAAM,YAAY;GACpC;GACA,UAAU,MAAM;IACd,EAAE,cAAc,MAAM,YAAY;IAClC,EAAE,cAAc,MAAM,YAAY,YAAY;IAC9C,EAAE,cAAc,MAAM,UAAU,aAAa;GAC/C;GACA,SAAS,MAAM;IACb,EAAE,cAAc,MAAM,YAAY;IAClC,EAAE,cAAc,MAAM,YAAY;IAClC,EAAE,cAAc,MAAM,UAAU;GAClC;GACD,UAAA;EAEO,CAAA,CAEP;CAGL,CAAA,GAAA,oBAAC,SAAD,EAAA,UACG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAuDI,CAAA,CACP,EAAA,CAAA;AAEN"}
@@ -1 +1 @@
1
- {"version":3,"file":"RoundStartAnnouncementOverlayHtml.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.tsx"],"sourcesContent":["/**\n * RoundStartAnnouncement Component - Displays \"Round X Begin!\" announcement\n *\n * Korean: 라운드 시작 발표 (Round Start Announcement)\n *\n * Shows \"Round X Begin!\" for subsequent rounds (not the first round).\n * Implements Korean cyberpunk aesthetic with bilingual text support.\n *\n * Refactored to use useKoreanTheme hook for consistent styling.\n *\n * @module components/combat/RoundStartAnnouncement\n * @category Combat UI\n */\n\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexColorToCSS } from \"../../../../../utils/colorUtils\";\n\n/**\n * Props for the RoundStartAnnouncement component\n */\nexport interface RoundStartAnnouncementProps {\n /** Round number (1-based) */\n readonly roundNumber: number;\n /** Duration to display announcement in seconds */\n readonly duration?: number;\n /** Callback when announcement completes */\n readonly onComplete: () => void;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n}\n\n/**\n * RoundStartAnnouncement Component\n *\n * Displays \"Round X Begin!\" announcement with:\n * - Bilingual round number and \"Begin!\" text\n * - Flash/pulse animation for impact\n * - Auto-dismiss after configured duration\n * - Audio cue for round start\n * - Responsive sizing for mobile/tablet/desktop\n * - Uses useKoreanTheme for consistent styling\n *\n * Korean: 라운드 시작 발표 컴포넌트\n */\nexport const RoundStartAnnouncement: React.FC<RoundStartAnnouncementProps> = ({\n roundNumber,\n duration = 2,\n onComplete,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"xlarge\", isMobile });\n const [isVisible, setIsVisible] = useState(false);\n\n const onCompleteRef = useRef(onComplete);\n useEffect(() => {\n onCompleteRef.current = onComplete;\n }, [onComplete]);\n\n const handleComplete = useCallback(() => {\n onCompleteRef.current();\n }, []);\n\n useEffect(() => {\n const timer = setTimeout(() => setIsVisible(true), 50);\n return () => clearTimeout(timer);\n }, []);\n\n useEffect(() => {\n if (audio.isAudioReady) {\n audio.playSFX(\"attack_medium\"); // Using placeholder - will be round_start\n }\n }, [audio]);\n\n useEffect(() => {\n let isMounted = true;\n let innerTimer: ReturnType<typeof setTimeout> | null = null;\n\n const outerTimer = setTimeout(() => {\n setIsVisible(false);\n innerTimer = setTimeout(() => {\n if (isMounted) {\n handleComplete();\n }\n }, 300); // Wait for fade out\n }, duration * 1000);\n\n return () => {\n isMounted = false;\n clearTimeout(outerTimer);\n if (innerTimer) {\n clearTimeout(innerTimer);\n }\n };\n }, [duration, handleComplete]);\n\n const goldColor = useMemo(() => hexColorToCSS(theme.colors.ACCENT_GOLD), [theme.colors.ACCENT_GOLD]);\n const darkBg = useMemo(\n () => hexColorToCSS(theme.colors.UI_BACKGROUND_DARK),\n [theme.colors.UI_BACKGROUND_DARK]\n );\n\n return (\n <>\n <div\n data-testid=\"round-start-announcement\"\n role=\"alert\"\n aria-live=\"assertive\"\n aria-label={`Round ${roundNumber} starting`}\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: `${darkBg}88`,\n zIndex: 900,\n opacity: isVisible ? 1 : 0,\n transition: \"opacity 0.3s ease-in-out\",\n pointerEvents: \"none\",\n }}\n >\n <div\n style={{\n fontSize: isMobile ? \"56px\" : \"96px\",\n fontWeight: \"bold\",\n color: goldColor,\n fontFamily: theme.fontFamily.KOREAN,\n textShadow: `0 0 40px ${goldColor}`,\n animation: \"roundStartFlash 0.5s ease-out\",\n textAlign: \"center\",\n userSelect: \"none\",\n }}\n data-testid=\"round-start-text\"\n >\n 라운드 {roundNumber} 시작!\n <br />\n <span\n style={{\n fontSize: isMobile ? \"40px\" : \"64px\",\n }}\n >\n Round {roundNumber} Begin!\n </span>\n </div>\n </div>\n\n {/* CSS Animation */}\n <style>\n {`\n @keyframes roundStartFlash {\n 0% {\n opacity: 0;\n transform: scale(1.5);\n }\n 30% {\n opacity: 1;\n transform: scale(1.2);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n `}\n </style>\n </>\n );\n};\n\nexport default RoundStartAnnouncement;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,IAAa,0BAAiE,EAC5E,aACA,WAAW,GACX,YACA,eACI;CACJ,MAAM,QAAQ,SAAS;CACvB,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAU;CAAS,CAAC;CAC7E,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAEhD,MAAM,gBAAgB,OAAO,UAAU;CACvC,gBAAgB;EACd,cAAc,UAAU;CAC1B,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,iBAAiB,kBAAkB;EACvC,cAAc,QAAQ;CACxB,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,MAAM,QAAQ,iBAAiB,aAAa,IAAI,GAAG,EAAE;EACrD,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,IAAI,MAAM,cACR,MAAM,QAAQ,eAAe;CAEjC,GAAG,CAAC,KAAK,CAAC;CAEV,gBAAgB;EACd,IAAI,YAAY;EAChB,IAAI,aAAmD;EAEvD,MAAM,aAAa,iBAAiB;GAClC,aAAa,KAAK;GAClB,aAAa,iBAAiB;IAC5B,IAAI,WACF,eAAe;GAEnB,GAAG,GAAG;EACR,GAAG,WAAW,GAAI;EAElB,aAAa;GACX,YAAY;GACZ,aAAa,UAAU;GACvB,IAAI,YACF,aAAa,UAAU;EAE3B;CACF,GAAG,CAAC,UAAU,cAAc,CAAC;CAE7B,MAAM,YAAY,cAAc,cAAc,MAAM,OAAO,WAAW,GAAG,CAAC,MAAM,OAAO,WAAW,CAAC;CACnG,MAAM,SAAS,cACP,cAAc,MAAM,OAAO,kBAAkB,GACnD,CAAC,MAAM,OAAO,kBAAkB,CAClC;CAEA,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,OAAD;EACE,eAAY;EACZ,MAAK;EACL,aAAU;EACV,cAAY,SAAS,YAAY;EACjC,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,iBAAiB,GAAG,OAAO;GAC3B,QAAQ;GACR,SAAS,YAAY,IAAI;GACzB,YAAY;GACZ,eAAe;EACjB;EAEA,UAAA,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,WAAW,SAAS;IAC9B,YAAY;IACZ,OAAO;IACP,YAAY,MAAM,WAAW;IAC7B,YAAY,YAAY;IACxB,WAAW;IACX,WAAW;IACX,YAAY;GACd;GACA,eAAY;GAXd,UAAA;IAYC;IACM;IAAY;IACjB,oBAAC,MAAD,CAAK,CAAA;IACL,qBAAC,QAAD;KACE,OAAO,EACL,UAAU,WAAW,SAAS,OAChC;KAHF,UAAA;MAIC;MACQ;MAAY;KACf;;GACH;;CACF,CAAA,GAGL,oBAAC,SAAD,EAAA,UACG;;;;;;;;;;;;;;;UAgBI,CAAA,CACP,EAAA,CAAA;AAEN"}
1
+ {"version":3,"file":"RoundStartAnnouncementOverlayHtml.js","names":["_Fragment"],"sources":["../../../../../../src/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.tsx"],"sourcesContent":["/**\n * RoundStartAnnouncement Component - Displays \"Round X Begin!\" announcement\n *\n * Korean: 라운드 시작 발표 (Round Start Announcement)\n *\n * Shows \"Round X Begin!\" for subsequent rounds (not the first round).\n * Implements Korean cyberpunk aesthetic with bilingual text support.\n *\n * Refactored to use useKoreanTheme hook for consistent styling.\n *\n * @module components/combat/RoundStartAnnouncement\n * @category Combat UI\n */\n\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexColorToCSS } from \"../../../../../utils/colorUtils\";\n\n/**\n * Props for the RoundStartAnnouncement component\n */\nexport interface RoundStartAnnouncementProps {\n /** Round number (1-based) */\n readonly roundNumber: number;\n /** Duration to display announcement in seconds */\n readonly duration?: number;\n /** Callback when announcement completes */\n readonly onComplete: () => void;\n /** Whether layout should adapt for mobile screens */\n readonly isMobile: boolean;\n}\n\n/**\n * RoundStartAnnouncement Component\n *\n * Displays \"Round X Begin!\" announcement with:\n * - Bilingual round number and \"Begin!\" text\n * - Flash/pulse animation for impact\n * - Auto-dismiss after configured duration\n * - Audio cue for round start\n * - Responsive sizing for mobile/tablet/desktop\n * - Uses useKoreanTheme for consistent styling\n *\n * Korean: 라운드 시작 발표 컴포넌트\n */\nexport const RoundStartAnnouncement: React.FC<RoundStartAnnouncementProps> = ({\n roundNumber,\n duration = 2,\n onComplete,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"xlarge\", isMobile });\n const [isVisible, setIsVisible] = useState(false);\n\n const onCompleteRef = useRef(onComplete);\n useEffect(() => {\n onCompleteRef.current = onComplete;\n }, [onComplete]);\n\n const handleComplete = useCallback(() => {\n onCompleteRef.current();\n }, []);\n\n useEffect(() => {\n const timer = setTimeout(() => setIsVisible(true), 50);\n return () => clearTimeout(timer);\n }, []);\n\n useEffect(() => {\n if (audio.isAudioReady) {\n audio.playSFX(\"attack_medium\"); // Using placeholder - will be round_start\n }\n }, [audio]);\n\n useEffect(() => {\n let isMounted = true;\n let innerTimer: ReturnType<typeof setTimeout> | null = null;\n\n const outerTimer = setTimeout(() => {\n setIsVisible(false);\n innerTimer = setTimeout(() => {\n if (isMounted) {\n handleComplete();\n }\n }, 300); // Wait for fade out\n }, duration * 1000);\n\n return () => {\n isMounted = false;\n clearTimeout(outerTimer);\n if (innerTimer) {\n clearTimeout(innerTimer);\n }\n };\n }, [duration, handleComplete]);\n\n const goldColor = useMemo(() => hexColorToCSS(theme.colors.ACCENT_GOLD), [theme.colors.ACCENT_GOLD]);\n const darkBg = useMemo(\n () => hexColorToCSS(theme.colors.UI_BACKGROUND_DARK),\n [theme.colors.UI_BACKGROUND_DARK]\n );\n\n return (\n <>\n <div\n data-testid=\"round-start-announcement\"\n role=\"alert\"\n aria-live=\"assertive\"\n aria-label={`Round ${roundNumber} starting`}\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: `${darkBg}88`,\n zIndex: 900,\n opacity: isVisible ? 1 : 0,\n transition: \"opacity 0.3s ease-in-out\",\n pointerEvents: \"none\",\n }}\n >\n <div\n style={{\n fontSize: isMobile ? \"56px\" : \"96px\",\n fontWeight: \"bold\",\n color: goldColor,\n fontFamily: theme.fontFamily.KOREAN,\n textShadow: `0 0 40px ${goldColor}`,\n animation: \"roundStartFlash 0.5s ease-out\",\n textAlign: \"center\",\n userSelect: \"none\",\n }}\n data-testid=\"round-start-text\"\n >\n 라운드 {roundNumber} 시작!\n <br />\n <span\n style={{\n fontSize: isMobile ? \"40px\" : \"64px\",\n }}\n >\n Round {roundNumber} Begin!\n </span>\n </div>\n </div>\n\n {/* CSS Animation */}\n <style>\n {`\n @keyframes roundStartFlash {\n 0% {\n opacity: 0;\n transform: scale(1.5);\n }\n 30% {\n opacity: 1;\n transform: scale(1.2);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n }\n `}\n </style>\n </>\n );\n};\n\nexport default RoundStartAnnouncement;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,IAAa,0BAAiE,EAC5E,aACA,WAAW,GACX,YACA,eACI;CACJ,MAAM,QAAQ,SAAS;CACvB,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAU;CAAS,CAAC;CAC7E,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAEhD,MAAM,gBAAgB,OAAO,UAAU;CACvC,gBAAgB;EACd,cAAc,UAAU;CAC1B,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,iBAAiB,kBAAkB;EACvC,cAAc,QAAQ;CACxB,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,MAAM,QAAQ,iBAAiB,aAAa,IAAI,GAAG,EAAE;EACrD,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,IAAI,MAAM,cACR,MAAM,QAAQ,eAAe;CAEjC,GAAG,CAAC,KAAK,CAAC;CAEV,gBAAgB;EACd,IAAI,YAAY;EAChB,IAAI,aAAmD;EAEvD,MAAM,aAAa,iBAAiB;GAClC,aAAa,KAAK;GAClB,aAAa,iBAAiB;IAC5B,IAAI,WACF,eAAe;GAEnB,GAAG,GAAG;EACR,GAAG,WAAW,GAAI;EAElB,aAAa;GACX,YAAY;GACZ,aAAa,UAAU;GACvB,IAAI,YACF,aAAa,UAAU;EAE3B;CACF,GAAG,CAAC,UAAU,cAAc,CAAC;CAE7B,MAAM,YAAY,cAAc,cAAc,MAAM,OAAO,WAAW,GAAG,CAAC,MAAM,OAAO,WAAW,CAAC;CACnG,MAAM,SAAS,cACP,cAAc,MAAM,OAAO,kBAAkB,GACnD,CAAC,MAAM,OAAO,kBAAkB,CAClC;CAEA,OACE,qBAAAA,UAAA,EAAA,UAAA,CACE,oBAAC,OAAD;EACE,eAAY;EACZ,MAAK;EACL,aAAU;EACV,cAAY,SAAS,YAAY;EACjC,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,iBAAiB,GAAG,OAAO;GAC3B,QAAQ;GACR,SAAS,YAAY,IAAI;GACzB,YAAY;GACZ,eAAe;EACjB;EAEA,UAAA,qBAAC,OAAD;GACE,OAAO;IACL,UAAU,WAAW,SAAS;IAC9B,YAAY;IACZ,OAAO;IACP,YAAY,MAAM,WAAW;IAC7B,YAAY,YAAY;IACxB,WAAW;IACX,WAAW;IACX,YAAY;GACd;GACA,eAAY;GAXd,UAAA;IAYC;IACM;IAAY;IACjB,oBAAC,MAAD,CAAK,CAAA;IACL,qBAAC,QAAD;KACE,OAAO,EACL,UAAU,WAAW,SAAS,OAChC;KAHF,UAAA;MAIC;MACQ;MAAY;KACf;;GACH;;CACF,CAAA,GAGL,oBAAC,SAAD,EAAA,UACG;;;;;;;;;;;;;;;UAgBI,CAAA,CACP,EAAA,CAAA;AAEN"}
@@ -1 +1 @@
1
- {"version":3,"file":"MobileControlsWrapper.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/hud/MobileControlsWrapper.tsx"],"sourcesContent":["/**\n * MobileControlsWrapper - Mobile touch controls container\n *\n * Wraps all mobile-specific touch controls including:\n * - Virtual D-Pad for movement\n * - Action buttons (attack/block)\n * - Stance wheel for trigram stance selection\n * - Gesture recognizer for swipe actions\n *\n * PRIORITY SUPPORT: Optimized for high-end mobile devices (Super HD, 2K+)\n * - Enhanced touch targets (56px on Super HD vs 48px standard)\n * - Larger fonts and better spacing on high-resolution displays\n * - Full feature parity with desktop experience\n *\n * Integrated with:\n * - CombatScreen3D: Full combat gameplay with mobile controls\n * - TrainingScreen3D: Training mode with mobile controls\n * - deviceDetection.ts: Robust mobile detection (handles 2712x1220 Super HD)\n * - mobileUIUtils.ts: Touch target and font sizing utilities\n *\n * Only rendered on mobile devices (isMobile=true)\n *\n * @module components/combat/components/MobileControlsWrapper\n * @category Combat UI\n * @korean 모바일컨트롤래퍼\n */\n\nimport React from \"react\";\nimport {\n ActionButtons,\n GestureRecognizerPure,\n StanceWheelPure,\n VirtualDPad,\n} from \"../../../../shared/mobile\";\nimport { ButtonEventType } from \"../../../../shared/mobile/ActionButtons\";\nimport { Direction, DPadEventType } from \"../../../../shared/mobile/VirtualDPad\";\nimport { GestureEvent } from \"../../../../../hooks/useTouchControls\";\nimport { getMobileControlsBottom } from \"../../../../../types/constants/layout\";\n\nexport interface MobileControlsWrapperProps {\n /** Whether mobile controls are enabled */\n readonly enabled: boolean;\n /** Current stance index (0-7) */\n readonly currentStanceIndex: number;\n /** Whether stance wheel is expanded */\n readonly stanceWheelExpanded: boolean;\n /** D-Pad movement handler */\n readonly onMove: (direction: Direction | null, eventType: DPadEventType) => void;\n /** Attack button handler */\n readonly onAttack: () => void;\n /** Block button handler */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Stance change handler */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Stance wheel toggle handler */\n readonly onStanceWheelToggle: () => void;\n /** Gesture handler */\n readonly onGesture: (gesture: GestureEvent) => void;\n /**\n * Current viewport height in pixels.\n *\n * Forwarded to `getMobileControlsBottom()` so the D-Pad / ActionButtons\n * drop closer to the bottom edge on short viewports (e.g. landscape\n * phones &lt; 500 px tall). When omitted, the default 200 px band is used.\n */\n readonly viewportHeight?: number;\n}\n\n/**\n * MobileControlsWrapper - Container for all mobile touch controls\n *\n * Provides virtual controls for mobile combat gameplay:\n * - Virtual D-Pad (bottom-left) for 8-direction movement\n * - Action Buttons (bottom-right) for attack/block\n * - Stance Wheel (right-center) for trigram stance selection\n * - Gesture Recognition for swipe-based actions\n *\n * Positioning Strategy:\n * - All controls positioned using centralized layout constants\n * - D-Pad and ActionButtons use getMobileControlsBottom() for consistency\n * - Z-index: MOBILE_CONTROLS (50) to ensure visibility over HUD elements\n *\n * @example\n * ```tsx\n * <MobileControlsWrapper\n * enabled={mobileControlsEnabled}\n * currentStanceIndex={currentStanceIndex}\n * stanceWheelExpanded={stanceWheelExpanded}\n * onMove={handleMobileMove}\n * onAttack={handleMobileAttack}\n * onBlock={handleMobileBlock}\n * onStanceChange={handleMobileStanceChange}\n * onStanceWheelToggle={() => setStanceWheelExpanded(!stanceWheelExpanded)}\n * onGesture={handleMobileGesture}\n * />\n * ```\n */\nexport const MobileControlsWrapper: React.FC<MobileControlsWrapperProps> = ({\n enabled,\n currentStanceIndex,\n stanceWheelExpanded,\n onMove,\n onAttack,\n onBlock,\n onStanceChange,\n onStanceWheelToggle,\n onGesture,\n viewportHeight,\n}) => {\n const controlsBottom = getMobileControlsBottom(viewportHeight);\n return (\n <>\n {/* Virtual D-Pad - Bottom-left for movement */}\n {/* Positioned using centralized constant for consistency */}\n <VirtualDPad\n onMove={onMove}\n disabled={!enabled}\n opacity={0.8}\n bottom={controlsBottom}\n />\n\n {/* Action Buttons - Bottom-right for attack/block */}\n {/* Positioned using centralized constant for consistency */}\n <ActionButtons\n onAttack={onAttack}\n onBlock={onBlock}\n disabled={!enabled}\n opacity={0.8}\n bottom={controlsBottom}\n />\n\n {/* Stance Wheel - Right-center for trigram stance selection */}\n <StanceWheelPure\n currentStance={currentStanceIndex}\n onStanceChange={onStanceChange}\n expanded={stanceWheelExpanded}\n onToggle={onStanceWheelToggle}\n disabled={!enabled}\n opacity={0.8}\n />\n\n {/* Gesture Recognizer - Full-screen swipe detection */}\n <GestureRecognizerPure\n onGesture={onGesture}\n enabled={enabled}\n showFeedback={true}\n minSwipeDistance={50}\n />\n </>\n );\n};\n\nexport default MobileControlsWrapper;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,IAAa,yBAA+D,EAC1E,SACA,oBACA,qBACA,QACA,UACA,SACA,gBACA,qBACA,WACA,qBACI;CACJ,MAAM,iBAAiB,wBAAwB,cAAc;CAC7D,OACE,qBAAA,UAAA,EAAA,UAAA;EAGE,oBAAC,aAAD;GACU;GACR,UAAU,CAAC;GACX,SAAS;GACT,QAAQ;EACT,CAAA;EAID,oBAAC,eAAD;GACY;GACD;GACT,UAAU,CAAC;GACX,SAAS;GACT,QAAQ;EACT,CAAA;EAGD,oBAAC,iBAAD;GACE,eAAe;GACC;GAChB,UAAU;GACV,UAAU;GACV,UAAU,CAAC;GACX,SAAS;EACV,CAAA;EAGD,oBAAC,uBAAD;GACa;GACF;GACT,cAAc;GACd,kBAAkB;EACnB,CAAA;CACD,EAAA,CAAA;AAEN"}
1
+ {"version":3,"file":"MobileControlsWrapper.js","names":["_Fragment"],"sources":["../../../../../../src/components/screens/combat/components/hud/MobileControlsWrapper.tsx"],"sourcesContent":["/**\n * MobileControlsWrapper - Mobile touch controls container\n *\n * Wraps all mobile-specific touch controls including:\n * - Virtual D-Pad for movement\n * - Action buttons (attack/block)\n * - Stance wheel for trigram stance selection\n * - Gesture recognizer for swipe actions\n *\n * PRIORITY SUPPORT: Optimized for high-end mobile devices (Super HD, 2K+)\n * - Enhanced touch targets (56px on Super HD vs 48px standard)\n * - Larger fonts and better spacing on high-resolution displays\n * - Full feature parity with desktop experience\n *\n * Integrated with:\n * - CombatScreen3D: Full combat gameplay with mobile controls\n * - TrainingScreen3D: Training mode with mobile controls\n * - deviceDetection.ts: Robust mobile detection (handles 2712x1220 Super HD)\n * - mobileUIUtils.ts: Touch target and font sizing utilities\n *\n * Only rendered on mobile devices (isMobile=true)\n *\n * @module components/combat/components/MobileControlsWrapper\n * @category Combat UI\n * @korean 모바일컨트롤래퍼\n */\n\nimport React from \"react\";\nimport {\n ActionButtons,\n GestureRecognizerPure,\n StanceWheelPure,\n VirtualDPad,\n} from \"../../../../shared/mobile\";\nimport { ButtonEventType } from \"../../../../shared/mobile/ActionButtons\";\nimport { Direction, DPadEventType } from \"../../../../shared/mobile/VirtualDPad\";\nimport { GestureEvent } from \"../../../../../hooks/useTouchControls\";\nimport { getMobileControlsBottom } from \"../../../../../types/constants/layout\";\n\nexport interface MobileControlsWrapperProps {\n /** Whether mobile controls are enabled */\n readonly enabled: boolean;\n /** Current stance index (0-7) */\n readonly currentStanceIndex: number;\n /** Whether stance wheel is expanded */\n readonly stanceWheelExpanded: boolean;\n /** D-Pad movement handler */\n readonly onMove: (direction: Direction | null, eventType: DPadEventType) => void;\n /** Attack button handler */\n readonly onAttack: () => void;\n /** Block button handler */\n readonly onBlock: (eventType: ButtonEventType) => void;\n /** Stance change handler */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Stance wheel toggle handler */\n readonly onStanceWheelToggle: () => void;\n /** Gesture handler */\n readonly onGesture: (gesture: GestureEvent) => void;\n /**\n * Current viewport height in pixels.\n *\n * Forwarded to `getMobileControlsBottom()` so the D-Pad / ActionButtons\n * drop closer to the bottom edge on short viewports (e.g. landscape\n * phones &lt; 500 px tall). When omitted, the default 200 px band is used.\n */\n readonly viewportHeight?: number;\n}\n\n/**\n * MobileControlsWrapper - Container for all mobile touch controls\n *\n * Provides virtual controls for mobile combat gameplay:\n * - Virtual D-Pad (bottom-left) for 8-direction movement\n * - Action Buttons (bottom-right) for attack/block\n * - Stance Wheel (right-center) for trigram stance selection\n * - Gesture Recognition for swipe-based actions\n *\n * Positioning Strategy:\n * - All controls positioned using centralized layout constants\n * - D-Pad and ActionButtons use getMobileControlsBottom() for consistency\n * - Z-index: MOBILE_CONTROLS (50) to ensure visibility over HUD elements\n *\n * @example\n * ```tsx\n * <MobileControlsWrapper\n * enabled={mobileControlsEnabled}\n * currentStanceIndex={currentStanceIndex}\n * stanceWheelExpanded={stanceWheelExpanded}\n * onMove={handleMobileMove}\n * onAttack={handleMobileAttack}\n * onBlock={handleMobileBlock}\n * onStanceChange={handleMobileStanceChange}\n * onStanceWheelToggle={() => setStanceWheelExpanded(!stanceWheelExpanded)}\n * onGesture={handleMobileGesture}\n * />\n * ```\n */\nexport const MobileControlsWrapper: React.FC<MobileControlsWrapperProps> = ({\n enabled,\n currentStanceIndex,\n stanceWheelExpanded,\n onMove,\n onAttack,\n onBlock,\n onStanceChange,\n onStanceWheelToggle,\n onGesture,\n viewportHeight,\n}) => {\n const controlsBottom = getMobileControlsBottom(viewportHeight);\n return (\n <>\n {/* Virtual D-Pad - Bottom-left for movement */}\n {/* Positioned using centralized constant for consistency */}\n <VirtualDPad\n onMove={onMove}\n disabled={!enabled}\n opacity={0.8}\n bottom={controlsBottom}\n />\n\n {/* Action Buttons - Bottom-right for attack/block */}\n {/* Positioned using centralized constant for consistency */}\n <ActionButtons\n onAttack={onAttack}\n onBlock={onBlock}\n disabled={!enabled}\n opacity={0.8}\n bottom={controlsBottom}\n />\n\n {/* Stance Wheel - Right-center for trigram stance selection */}\n <StanceWheelPure\n currentStance={currentStanceIndex}\n onStanceChange={onStanceChange}\n expanded={stanceWheelExpanded}\n onToggle={onStanceWheelToggle}\n disabled={!enabled}\n opacity={0.8}\n />\n\n {/* Gesture Recognizer - Full-screen swipe detection */}\n <GestureRecognizerPure\n onGesture={onGesture}\n enabled={enabled}\n showFeedback={true}\n minSwipeDistance={50}\n />\n </>\n );\n};\n\nexport default MobileControlsWrapper;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,IAAa,yBAA+D,EAC1E,SACA,oBACA,qBACA,QACA,UACA,SACA,gBACA,qBACA,WACA,qBACI;CACJ,MAAM,iBAAiB,wBAAwB,cAAc;CAC7D,OACE,qBAAAA,UAAA,EAAA,UAAA;EAGE,oBAAC,aAAD;GACU;GACR,UAAU,CAAC;GACX,SAAS;GACT,QAAQ;EACT,CAAA;EAID,oBAAC,eAAD;GACY;GACD;GACT,UAAU,CAAC;GACX,SAAS;GACT,QAAQ;EACT,CAAA;EAGD,oBAAC,iBAAD;GACE,eAAe;GACC;GAChB,UAAU;GACV,UAAU;GACV,UAAU,CAAC;GACX,SAAS;EACV,CAAA;EAGD,oBAAC,uBAAD;GACa;GACF;GACT,cAAc;GACd,kBAAkB;EACnB,CAAA;CACD,EAAA,CAAA;AAEN"}
@@ -1 +1 @@
1
- {"version":3,"file":"StaminaWarning.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/indicators/StaminaWarning.tsx"],"sourcesContent":["/**\n * StaminaWarning Component - Visual warning for low stamina\n *\n * Displays a yellow flashing indicator when stamina drops below critical threshold (20%).\n * Uses CSS animation for attention-grabbing flash effect.\n *\n * NOTE: This component is rendered OUTSIDE the Canvas as part of the HTML overlay.\n * It does NOT use Html from drei - it's a standard React component.\n *\n * Refactored to use useKoreanTheme for consistent styling.\n *\n * @module components/combat/StaminaWarning\n * @category Combat UI\n * @korean 체력경고\n */\n\nimport React, { useMemo } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\nexport interface StaminaWarningProps {\n /**\n * Current stamina amount (0-100)\n * @korean 체력량\n */\n readonly stamina: number;\n\n /**\n * Mobile responsive mode (reduced flash intensity)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Multiplier applied to the warning's visual weight (0.0-1.0).\n *\n * Used to soften the fullscreen flash when the 3D arena is already\n * visually compressed (e.g. portrait mobile). Default is `1.0`.\n *\n * @korean 효과강도배수\n */\n readonly intensityScale?: number;\n}\n\n/**\n * StaminaWarning - Yellow flash warning for critical stamina depletion\n *\n * Renders a fullscreen flashing yellow border when stamina drops below 20%.\n * Only visible when stamina is below 20. If stamina is 20 or higher, the component does not render.\n * Uses CSS keyframe animation for fast attention-grabbing flash at 60fps.\n * Uses useKoreanTheme for consistent color scheme.\n *\n * @example\n * ```tsx\n * <StaminaWarning stamina={15} isMobile={false} /> // Renders warning\n * <StaminaWarning stamina={25} isMobile={false} /> // Does not render\n * ```\n */\nexport const StaminaWarning: React.FC<StaminaWarningProps> = ({\n stamina,\n isMobile,\n intensityScale = 1,\n}) => {\n const theme = useKoreanTheme({ variant: \"danger\", size: \"md\", isMobile });\n \n const warningStyle = useMemo(() => {\n const criticalThreshold = 20;\n if (stamina >= criticalThreshold) {\n return null;\n }\n\n const clampedStamina = Math.max(0, Math.min(criticalThreshold, stamina));\n\n const urgency = (criticalThreshold - clampedStamina) / criticalThreshold;\n\n const safeScale = Math.max(0, Math.min(1, intensityScale));\n\n const borderWidth = isMobile ? \"4px\" : \"6px\";\n\n const borderColor = hexToRgbaString(theme.colors.WARNING_YELLOW, 1);\n const glowColor = hexToRgbaString(theme.colors.WARNING_YELLOW, safeScale);\n const animationDuration = Math.max(0.6, 1.2 - urgency * 0.6);\n\n return {\n position: \"fixed\" as const,\n inset: borderWidth,\n pointerEvents: \"none\" as const,\n border: `${borderWidth} solid ${borderColor}`,\n boxShadow: `0 0 ${Math.round(20 * safeScale)}px ${glowColor}`,\n animation: `staminaFlash ${animationDuration}s ease-in-out infinite`,\n transition: \"border-color 0.3s ease-out\",\n zIndex: 85, // Below balance indicator but above game content\n };\n }, [stamina, isMobile, intensityScale, theme.colors.WARNING_YELLOW]);\n\n if (stamina >= 20 || !warningStyle) {\n return null;\n }\n\n return (\n <>\n {/* CSS keyframe animation for flashing */}\n <style>\n {`\n @keyframes staminaFlash {\n 0%, 100% {\n opacity: 0.3;\n }\n 50% {\n opacity: 1;\n }\n }\n `}\n </style>\n <div\n data-testid=\"stamina-warning\"\n style={warningStyle}\n aria-hidden=\"true\"\n />\n </>\n );\n};\n\nStaminaWarning.displayName = \"StaminaWarning\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,IAAa,kBAAiD,EAC5D,SACA,UACA,iBAAiB,QACb;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAU,MAAM;EAAM;CAAS,CAAC;CAExE,MAAM,eAAe,cAAc;EACjC,MAAM,oBAAoB;EAC1B,IAAI,WAAW,mBACb,OAAO;EAKT,MAAM,WAAW,oBAFM,KAAK,IAAI,GAAG,KAAK,IAAI,mBAAmB,OAAO,CAEjC,KAAkB;EAEvD,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,cAAc,CAAC;EAEzD,MAAM,cAAc,WAAW,QAAQ;EAEvC,MAAM,cAAc,gBAAgB,MAAM,OAAO,gBAAgB,CAAC;EAClE,MAAM,YAAY,gBAAgB,MAAM,OAAO,gBAAgB,SAAS;EACxE,MAAM,oBAAoB,KAAK,IAAI,IAAK,MAAM,UAAU,EAAG;EAE3D,OAAO;GACL,UAAU;GACV,OAAO;GACP,eAAe;GACf,QAAQ,GAAG,YAAY,SAAS;GAChC,WAAW,OAAO,KAAK,MAAM,KAAK,SAAS,EAAE,KAAK;GAClD,WAAW,gBAAgB,kBAAkB;GAC7C,YAAY;GACZ,QAAQ;EACV;CACF,GAAG;EAAC;EAAS;EAAU;EAAgB,MAAM,OAAO;CAAc,CAAC;CAEnE,IAAI,WAAW,MAAM,CAAC,cACpB,OAAO;CAGT,OACE,qBAAA,UAAA,EAAA,UAAA,CAEE,oBAAC,SAAD,EAAA,UACG;;;;;;;;;UAUI,CAAA,GACP,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;EACP,eAAY;CACb,CAAA,CACD,EAAA,CAAA;AAEN;AAEA,eAAe,cAAc"}
1
+ {"version":3,"file":"StaminaWarning.js","names":["_Fragment"],"sources":["../../../../../../src/components/screens/combat/components/indicators/StaminaWarning.tsx"],"sourcesContent":["/**\n * StaminaWarning Component - Visual warning for low stamina\n *\n * Displays a yellow flashing indicator when stamina drops below critical threshold (20%).\n * Uses CSS animation for attention-grabbing flash effect.\n *\n * NOTE: This component is rendered OUTSIDE the Canvas as part of the HTML overlay.\n * It does NOT use Html from drei - it's a standard React component.\n *\n * Refactored to use useKoreanTheme for consistent styling.\n *\n * @module components/combat/StaminaWarning\n * @category Combat UI\n * @korean 체력경고\n */\n\nimport React, { useMemo } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\nexport interface StaminaWarningProps {\n /**\n * Current stamina amount (0-100)\n * @korean 체력량\n */\n readonly stamina: number;\n\n /**\n * Mobile responsive mode (reduced flash intensity)\n * @korean 모바일여부\n */\n readonly isMobile: boolean;\n\n /**\n * Multiplier applied to the warning's visual weight (0.0-1.0).\n *\n * Used to soften the fullscreen flash when the 3D arena is already\n * visually compressed (e.g. portrait mobile). Default is `1.0`.\n *\n * @korean 효과강도배수\n */\n readonly intensityScale?: number;\n}\n\n/**\n * StaminaWarning - Yellow flash warning for critical stamina depletion\n *\n * Renders a fullscreen flashing yellow border when stamina drops below 20%.\n * Only visible when stamina is below 20. If stamina is 20 or higher, the component does not render.\n * Uses CSS keyframe animation for fast attention-grabbing flash at 60fps.\n * Uses useKoreanTheme for consistent color scheme.\n *\n * @example\n * ```tsx\n * <StaminaWarning stamina={15} isMobile={false} /> // Renders warning\n * <StaminaWarning stamina={25} isMobile={false} /> // Does not render\n * ```\n */\nexport const StaminaWarning: React.FC<StaminaWarningProps> = ({\n stamina,\n isMobile,\n intensityScale = 1,\n}) => {\n const theme = useKoreanTheme({ variant: \"danger\", size: \"md\", isMobile });\n \n const warningStyle = useMemo(() => {\n const criticalThreshold = 20;\n if (stamina >= criticalThreshold) {\n return null;\n }\n\n const clampedStamina = Math.max(0, Math.min(criticalThreshold, stamina));\n\n const urgency = (criticalThreshold - clampedStamina) / criticalThreshold;\n\n const safeScale = Math.max(0, Math.min(1, intensityScale));\n\n const borderWidth = isMobile ? \"4px\" : \"6px\";\n\n const borderColor = hexToRgbaString(theme.colors.WARNING_YELLOW, 1);\n const glowColor = hexToRgbaString(theme.colors.WARNING_YELLOW, safeScale);\n const animationDuration = Math.max(0.6, 1.2 - urgency * 0.6);\n\n return {\n position: \"fixed\" as const,\n inset: borderWidth,\n pointerEvents: \"none\" as const,\n border: `${borderWidth} solid ${borderColor}`,\n boxShadow: `0 0 ${Math.round(20 * safeScale)}px ${glowColor}`,\n animation: `staminaFlash ${animationDuration}s ease-in-out infinite`,\n transition: \"border-color 0.3s ease-out\",\n zIndex: 85, // Below balance indicator but above game content\n };\n }, [stamina, isMobile, intensityScale, theme.colors.WARNING_YELLOW]);\n\n if (stamina >= 20 || !warningStyle) {\n return null;\n }\n\n return (\n <>\n {/* CSS keyframe animation for flashing */}\n <style>\n {`\n @keyframes staminaFlash {\n 0%, 100% {\n opacity: 0.3;\n }\n 50% {\n opacity: 1;\n }\n }\n `}\n </style>\n <div\n data-testid=\"stamina-warning\"\n style={warningStyle}\n aria-hidden=\"true\"\n />\n </>\n );\n};\n\nStaminaWarning.displayName = \"StaminaWarning\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,IAAa,kBAAiD,EAC5D,SACA,UACA,iBAAiB,QACb;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAU,MAAM;EAAM;CAAS,CAAC;CAExE,MAAM,eAAe,cAAc;EACjC,MAAM,oBAAoB;EAC1B,IAAI,WAAW,mBACb,OAAO;EAKT,MAAM,WAAW,oBAFM,KAAK,IAAI,GAAG,KAAK,IAAI,mBAAmB,OAAO,CAEjC,KAAkB;EAEvD,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,cAAc,CAAC;EAEzD,MAAM,cAAc,WAAW,QAAQ;EAEvC,MAAM,cAAc,gBAAgB,MAAM,OAAO,gBAAgB,CAAC;EAClE,MAAM,YAAY,gBAAgB,MAAM,OAAO,gBAAgB,SAAS;EACxE,MAAM,oBAAoB,KAAK,IAAI,IAAK,MAAM,UAAU,EAAG;EAE3D,OAAO;GACL,UAAU;GACV,OAAO;GACP,eAAe;GACf,QAAQ,GAAG,YAAY,SAAS;GAChC,WAAW,OAAO,KAAK,MAAM,KAAK,SAAS,EAAE,KAAK;GAClD,WAAW,gBAAgB,kBAAkB;GAC7C,YAAY;GACZ,QAAQ;EACV;CACF,GAAG;EAAC;EAAS;EAAU;EAAgB,MAAM,OAAO;CAAc,CAAC;CAEnE,IAAI,WAAW,MAAM,CAAC,cACpB,OAAO;CAGT,OACE,qBAAAA,UAAA,EAAA,UAAA,CAEE,oBAAC,SAAD,EAAA,UACG;;;;;;;;;UAUI,CAAA,GACP,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;EACP,eAAY;CACb,CAAA,CACD,EAAA,CAAA;AAEN;AAEA,eAAe,cAAc"}
@@ -44,10 +44,10 @@
44
44
  * });
45
45
  * ```
46
46
  */
47
- import { AdaptiveDifficulty, AIComboSystem, AIDecisionTree, AIPersonality, DifficultyParameters } from "../../../../systems/ai/index.d.ts";
48
- import { PlayerState } from "../../../../systems/player.d.ts";
49
- import { KoreanTechnique } from "../../../../systems/vitalpoint/types.d.ts";
50
- import { Position, TrigramStance } from "../../../../types/index.d.ts";
47
+ import { AdaptiveDifficulty, AIComboSystem, AIDecisionTree, AIPersonality, DifficultyParameters } from "../../../../systems/ai/index.js";
48
+ import { PlayerState } from "../../../../systems/player.js";
49
+ import { KoreanTechnique } from "../../../../systems/vitalpoint/types.js";
50
+ import { Position, TrigramStance } from "../../../../types/index.js";
51
51
  /**
52
52
  * AI state management
53
53
  */
@@ -36,14 +36,14 @@
36
36
  * });
37
37
  * ```
38
38
  */
39
- import { PlayerState } from "../../../../systems/index.d.ts";
40
- import { type AnimationState } from "../../../../systems/animation/index.d.ts";
41
- import { type PhysicsArenaBounds } from "../../../../types/PhysicsTypes.d.ts";
42
- import { CombatSystem } from "../../../../systems/CombatSystem.d.ts";
43
- import { HitEffectType } from "../../../../systems/effects.d.ts";
44
- import { KoreanTechnique } from "../../../../systems/vitalpoint/types.d.ts";
45
- import { Position, Technique, TrigramStance } from "../../../../types/index.d.ts";
46
- import { Injury } from "../../../../types/injury.d.ts";
39
+ import { PlayerState } from "../../../../systems/index.js";
40
+ import { type AnimationState } from "../../../../systems/animation/index.js";
41
+ import { type PhysicsArenaBounds } from "../../../../types/PhysicsTypes.js";
42
+ import { CombatSystem } from "../../../../systems/CombatSystem.js";
43
+ import { HitEffectType } from "../../../../systems/effects.js";
44
+ import { KoreanTechnique } from "../../../../systems/vitalpoint/types.js";
45
+ import { Position, Technique, TrigramStance } from "../../../../types/index.js";
46
+ import { Injury } from "../../../../types/injury.js";
47
47
  import { AttackIntensity } from "./useCombatAudio";
48
48
  import { CombatActions, CombatScreenState } from "./useCombatState";
49
49
  export declare const SCREEN_SHAKE_FRAME_INTERVAL_MS = 50;
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * @korean 전투공격이동훅 - 전투 공격 이동 추적
8
8
  */
9
- import { AnimationType } from "../../../../systems/animation/index.d.ts";
10
- import { TrigramStance } from "../../../../types/common.d.ts";
9
+ import { AnimationType } from "../../../../systems/animation/index.js";
10
+ import { TrigramStance } from "../../../../types/common.js";
11
11
  /**
12
12
  * Configuration for combat attack movement
13
13
  */
@@ -19,8 +19,8 @@
19
19
  * actions.incrementCombo();
20
20
  * ```
21
21
  */
22
- import { HitEffect } from "../../../../systems/index.d.ts";
23
- import type { StanceLaterality } from "../../../../systems/trigram/types.d.ts";
22
+ import { HitEffect } from "../../../../systems/index.js";
23
+ import type { StanceLaterality } from "../../../../systems/trigram/types.js";
24
24
  /**
25
25
  * Combat screen state managed by the reducer
26
26
  * Note: Renamed from CombatState to avoid conflict with CombatState enum in types/common.ts