blacktrigram 0.7.97 → 0.7.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioManager.js +2 -4
  3. package/lib/audio/AudioManager.js.map +1 -1
  4. package/lib/components/screens/combat/CombatScreen3D.js +8 -9
  5. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  6. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  7. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  8. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  9. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  10. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  11. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  12. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  13. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  14. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js +1 -1
  15. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  16. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  17. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js +2 -1
  18. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  19. package/lib/components/screens/combat/components/effects/InternalDamage3D.js +3 -1
  20. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  23. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +16 -15
  24. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  25. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  26. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  27. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  28. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  29. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  30. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  31. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  32. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  33. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  34. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  36. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  37. package/lib/components/screens/combat/hooks/useAICombat.js +0 -1
  38. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  39. package/lib/components/screens/combat/hooks/useCombatActions.js +7 -6
  40. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  41. package/lib/components/screens/combat/hooks/useCombatLayout.js +3 -1
  42. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  43. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  44. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  45. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js +7 -6
  46. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  47. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  48. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +34 -30
  49. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  50. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  51. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  52. package/lib/components/screens/endscreen/EndScreen3D.js +2 -2
  53. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  54. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js +1 -1
  55. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  56. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  57. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  58. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  59. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  60. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js +2 -2
  61. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  62. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js +2 -1
  63. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  64. package/lib/components/screens/intro/IntroScreen3D.js +2 -4
  65. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  66. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  67. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  68. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  69. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  72. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  74. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  75. package/lib/components/screens/training/TrainingScreen3D.js +10 -9
  76. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  77. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js +14 -13
  78. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  79. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  80. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  81. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  83. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  84. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  85. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  86. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  87. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  88. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/training/components/VitalPointMarker3D.js +8 -7
  90. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  91. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js +15 -13
  92. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  93. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  94. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  95. package/lib/components/screens/training/components/hud/TrainingRightHUD.js +3 -2
  96. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  97. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  98. package/lib/components/screens/training/hooks/useTrainingActions.js +1 -2
  99. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  100. package/lib/components/screens/training/hooks/useTrainingLayout.js +3 -1
  101. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  102. package/lib/components/shared/base/BaseButton.js.map +1 -1
  103. package/lib/components/shared/base/BaseButtonOverlayHtml.js +59 -53
  104. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  105. package/lib/components/shared/base/BasePanel.js +19 -18
  106. package/lib/components/shared/base/BasePanel.js.map +1 -1
  107. package/lib/components/shared/base/BaseText.js.map +1 -1
  108. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  109. package/lib/components/shared/mobile/GestureRecognizerPure.js +22 -18
  110. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  111. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  112. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  113. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  114. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js +4 -5
  115. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  116. package/lib/components/shared/three/anatomy/BoneClothing.js +29 -24
  117. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  118. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  119. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  120. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  121. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  122. package/lib/components/shared/three/effects/ActionFeedback.js +3 -2
  123. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  124. package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
  125. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  126. package/lib/components/shared/three/effects/HitEffects3D.js +3 -2
  127. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  128. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  129. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  130. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  131. package/lib/components/shared/three/effects/VitalPointMarkers3D.js +6 -5
  132. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  133. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  134. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  135. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  136. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  137. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  138. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  139. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  140. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  141. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  142. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  143. package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -1
  144. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  145. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  146. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  147. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  148. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  149. package/lib/components/shared/three/ui/PlayerHUD.js +2 -1
  150. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  151. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  152. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  153. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  154. package/lib/components/shared/three/ui/TechniqueBar.js +25 -24
  155. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  156. package/lib/components/shared/three/ui/TechniqueCard.js +1 -3
  157. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  158. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  159. package/lib/components/shared/ui/BackButton.js +2 -1
  160. package/lib/components/shared/ui/BackButton.js.map +1 -1
  161. package/lib/components/shared/ui/BaseHUDContainer.js +69 -68
  162. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  163. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  164. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  165. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  166. package/lib/components/shared/ui/SplashScreen.js +2 -2
  167. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  168. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  169. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  170. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  171. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  172. package/lib/hooks/useKeyboardControls.js +0 -1
  173. package/lib/hooks/useKeyboardControls.js.map +1 -1
  174. package/lib/hooks/useSkeletalAnimation.js +12 -3
  175. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  176. package/lib/systems/CombatSystem.js +6 -2
  177. package/lib/systems/CombatSystem.js.map +1 -1
  178. package/lib/systems/PlayerEffectManager.js +0 -1
  179. package/lib/systems/PlayerEffectManager.js.map +1 -1
  180. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  181. package/lib/systems/ai/DecisionTree.js +10 -21
  182. package/lib/systems/ai/DecisionTree.js.map +1 -1
  183. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  184. package/lib/systems/animation/systems/AdvancedJointMovements.js +1 -3
  185. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  186. package/lib/systems/animation/systems/MuscleActivation.js +3 -4
  187. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  188. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  189. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js +2 -1
  190. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  191. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  192. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  193. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  194. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  195. package/lib/systems/combat/GrappleSystem.js +3 -9
  196. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  197. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  198. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  199. package/lib/systems/movement/InjuryMovementModifier.js +1 -1
  200. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  201. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  202. package/lib/systems/vitalpoint/DamageCalculator.js +8 -5
  203. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  204. package/lib/types/facial.js.map +1 -1
  205. package/lib/utils/accessibility.js +0 -1
  206. package/lib/utils/accessibility.js.map +1 -1
  207. package/lib/utils/effectUtils.js +1 -3
  208. package/lib/utils/effectUtils.js.map +1 -1
  209. package/lib/utils/inputSystem.js +0 -2
  210. package/lib/utils/inputSystem.js.map +1 -1
  211. package/lib/utils/koreanThemeHelpers.js +6 -3
  212. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  213. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  214. package/lib/utils/playerUtils.js +4 -2
  215. package/lib/utils/playerUtils.js.map +1 -1
  216. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"SplashScreen.js","names":[],"sources":["../../../../src/components/shared/ui/SplashScreen.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, toHex } from \"../../../utils/colorUtils\";\nimport { shouldUseMobileControls } from \"../../../utils/deviceDetection\";\n\ndeclare const APP_VERSION: string | undefined;\n\nconst LOADING_DELAY_MS = 100;\n\nconst HEX_COLORS = {\n PRIMARY_CYAN: toHex(KOREAN_COLORS.PRIMARY_CYAN),\n ACCENT_GOLD: toHex(KOREAN_COLORS.ACCENT_GOLD),\n} as const;\n\nexport interface SplashScreenProps {\n readonly onStart: () => void;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Splash screen that requires user interaction before starting the game.\n * This is necessary to initialize AudioContext which requires a user gesture.\n */\nexport const SplashScreen: React.FC<SplashScreenProps> = ({\n onStart,\n width,\n height,\n}) => {\n const [isLoading, setIsLoading] = useState(false);\n\n const isMobile = useMemo(() => shouldUseMobileControls(), []);\n\n const layoutCalculation = useMemo(\n () => ({\n titleFontSize: isMobile ? 36 : 64,\n subtitleFontSize: isMobile ? 16 : 24,\n bodyFontSize: isMobile ? 12 : 14,\n instructionsFontSize: isMobile ? 11 : 12,\n buttonPadding: isMobile ? \"16px 48px\" : \"20px 60px\",\n buttonFontSize: isMobile ? 16 : 20,\n }),\n [isMobile],\n );\n\n const handleStart = useCallback(() => {\n setIsLoading(true);\n setTimeout(() => {\n onStart();\n }, LOADING_DELAY_MS);\n }, [onStart]);\n\n return (\n <div\n style={{\n width,\n height,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n background: `linear-gradient(180deg, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 0%, ${hexColorToCSS(KOREAN_COLORS.ARENA_BACKGROUND)} 100%)`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontFamily: FONT_FAMILY.CYBER,\n position: \"relative\",\n overflow: \"hidden\",\n }}\n data-testid=\"splash-screen\"\n >\n {/* Animated background grid - decorative only */}\n <div\n role=\"presentation\"\n aria-hidden=\"true\"\n style={{\n position: \"absolute\",\n inset: 0,\n background: `\n repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 51px\n ),\n repeating-linear-gradient(\n 90deg,\n transparent,\n transparent 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 51px\n )\n `,\n opacity: 0.3,\n }}\n />\n\n {/* Screen reader live region for loading state */}\n <div\n aria-live=\"polite\"\n aria-atomic=\"true\"\n style={{\n position: \"absolute\",\n left: \"-10000px\",\n width: \"1px\",\n height: \"1px\",\n overflow: \"hidden\",\n }}\n >\n {isLoading ? \"Initializing audio and loading game\" : \"Ready to start\"}\n </div>\n\n {/* Logo/Title */}\n <div\n style={{\n marginBottom: \"60px\",\n textAlign: \"center\",\n zIndex: 1,\n }}\n >\n <h1\n style={{\n fontSize: `${layoutCalculation.titleFontSize}px`,\n fontWeight: 900,\n color: `#${HEX_COLORS.PRIMARY_CYAN}`,\n textShadow: `0 0 20px #${HEX_COLORS.PRIMARY_CYAN}80`,\n marginBottom: \"20px\",\n letterSpacing: \"4px\",\n }}\n >\n 흑괘\n </h1>\n <h2\n style={{\n fontSize: `${layoutCalculation.subtitleFontSize}px`,\n fontWeight: 400,\n color: `#${HEX_COLORS.ACCENT_GOLD}`,\n letterSpacing: \"2px\",\n marginTop: 0,\n }}\n >\n BLACK TRIGRAM\n </h2>\n <p\n style={{\n fontSize: `${layoutCalculation.bodyFontSize}px`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_TERTIARY),\n marginTop: \"20px\",\n letterSpacing: \"1px\",\n }}\n >\n Korean Martial Arts Dojang\n </p>\n </div>\n\n {/* Start Button */}\n <button\n onClick={handleStart}\n disabled={isLoading}\n aria-label={\n isLoading\n ? \"Starting game and initializing audio\"\n : \"Start game and initialize audio\"\n }\n aria-busy={isLoading}\n aria-describedby=\"splash-instructions\"\n style={{\n padding: layoutCalculation.buttonPadding,\n fontSize: `${layoutCalculation.buttonFontSize}px`,\n fontFamily: FONT_FAMILY.CYBER,\n fontWeight: 700,\n color: isLoading ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT) : hexColorToCSS(KOREAN_COLORS.BLACK),\n background: isLoading\n ? hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_LIGHT)\n : `linear-gradient(135deg, #${HEX_COLORS.PRIMARY_CYAN} 0%, #${HEX_COLORS.ACCENT_GOLD} 100%)`,\n border: \"none\",\n borderRadius: \"8px\",\n cursor: isLoading ? \"not-allowed\" : \"pointer\",\n textTransform: \"uppercase\",\n letterSpacing: \"2px\",\n transition: \"all 0.3s ease\",\n boxShadow: isLoading\n ? \"none\"\n : `0 4px 20px #${HEX_COLORS.PRIMARY_CYAN}40`,\n position: \"relative\",\n zIndex: 1,\n opacity: isLoading ? 0.6 : 1,\n }}\n onMouseEnter={(e) => {\n if (!isLoading) {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 6px 30px #${HEX_COLORS.PRIMARY_CYAN}60`;\n }\n }}\n onMouseLeave={(e) => {\n if (!isLoading) {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 4px 20px #${HEX_COLORS.PRIMARY_CYAN}40`;\n }\n }}\n data-testid=\"splash-start-button\"\n >\n {isLoading ? \"시작 중... Starting...\" : \"시작 | Start\"}\n </button>\n\n {/* Instructions */}\n <div\n id=\"splash-instructions\"\n style={{\n marginTop: \"40px\",\n textAlign: \"center\",\n color: hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n fontSize: `${layoutCalculation.instructionsFontSize}px`,\n maxWidth: \"600px\",\n padding: \"0 20px\",\n zIndex: 1,\n }}\n >\n <p style={{ margin: \"8px 0\" }}>\n Audio initialization requires user interaction\n </p>\n <p style={{ margin: \"8px 0\" }}>\n Click the button above to enable sound and start the game\n </p>\n </div>\n\n {/* Version info */}\n <div\n role=\"contentinfo\"\n aria-label={`Application version ${typeof APP_VERSION !== \"undefined\" ? APP_VERSION : \"0.5.3\"}`}\n style={{\n position: \"absolute\",\n bottom: \"20px\",\n right: \"20px\",\n color: hexColorToCSS(KOREAN_COLORS.UI_STEEL_GRAY),\n fontSize: \"10px\",\n zIndex: 1,\n }}\n >\n v{typeof APP_VERSION !== \"undefined\" ? APP_VERSION : \"0.5.3\"}\n </div>\n </div>\n );\n};\n\nexport default SplashScreen;\n"],"mappings":"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"}
1
+ {"version":3,"file":"SplashScreen.js","names":[],"sources":["../../../../src/components/shared/ui/SplashScreen.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, toHex } from \"../../../utils/colorUtils\";\nimport { shouldUseMobileControls } from \"../../../utils/deviceDetection\";\n\ndeclare const APP_VERSION: string | undefined;\n\nconst LOADING_DELAY_MS = 100;\n\nconst HEX_COLORS = {\n PRIMARY_CYAN: toHex(KOREAN_COLORS.PRIMARY_CYAN),\n ACCENT_GOLD: toHex(KOREAN_COLORS.ACCENT_GOLD),\n} as const;\n\nexport interface SplashScreenProps {\n readonly onStart: () => void;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Splash screen that requires user interaction before starting the game.\n * This is necessary to initialize AudioContext which requires a user gesture.\n */\nexport const SplashScreen: React.FC<SplashScreenProps> = ({\n onStart,\n width,\n height,\n}) => {\n const [isLoading, setIsLoading] = useState(false);\n\n const isMobile = useMemo(() => shouldUseMobileControls(), []);\n\n const layoutCalculation = useMemo(\n () => ({\n titleFontSize: isMobile ? 36 : 64,\n subtitleFontSize: isMobile ? 16 : 24,\n bodyFontSize: isMobile ? 12 : 14,\n instructionsFontSize: isMobile ? 11 : 12,\n buttonPadding: isMobile ? \"16px 48px\" : \"20px 60px\",\n buttonFontSize: isMobile ? 16 : 20,\n }),\n [isMobile],\n );\n\n const handleStart = useCallback(() => {\n setIsLoading(true);\n setTimeout(() => {\n onStart();\n }, LOADING_DELAY_MS);\n }, [onStart]);\n\n return (\n <div\n style={{\n width,\n height,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n background: `linear-gradient(180deg, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 0%, ${hexColorToCSS(KOREAN_COLORS.ARENA_BACKGROUND)} 100%)`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontFamily: FONT_FAMILY.CYBER,\n position: \"relative\",\n overflow: \"hidden\",\n }}\n data-testid=\"splash-screen\"\n >\n {/* Animated background grid - decorative only */}\n <div\n role=\"presentation\"\n aria-hidden=\"true\"\n style={{\n position: \"absolute\",\n inset: 0,\n background: `\n repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 51px\n ),\n repeating-linear-gradient(\n 90deg,\n transparent,\n transparent 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 50px,\n #${HEX_COLORS.PRIMARY_CYAN}08 51px\n )\n `,\n opacity: 0.3,\n }}\n />\n\n {/* Screen reader live region for loading state */}\n <div\n aria-live=\"polite\"\n aria-atomic=\"true\"\n style={{\n position: \"absolute\",\n left: \"-10000px\",\n width: \"1px\",\n height: \"1px\",\n overflow: \"hidden\",\n }}\n >\n {isLoading ? \"Initializing audio and loading game\" : \"Ready to start\"}\n </div>\n\n {/* Logo/Title */}\n <div\n style={{\n marginBottom: \"60px\",\n textAlign: \"center\",\n zIndex: 1,\n }}\n >\n <h1\n style={{\n fontSize: `${layoutCalculation.titleFontSize}px`,\n fontWeight: 900,\n color: `#${HEX_COLORS.PRIMARY_CYAN}`,\n textShadow: `0 0 20px #${HEX_COLORS.PRIMARY_CYAN}80`,\n marginBottom: \"20px\",\n letterSpacing: \"4px\",\n }}\n >\n 흑괘\n </h1>\n <h2\n style={{\n fontSize: `${layoutCalculation.subtitleFontSize}px`,\n fontWeight: 400,\n color: `#${HEX_COLORS.ACCENT_GOLD}`,\n letterSpacing: \"2px\",\n marginTop: 0,\n }}\n >\n BLACK TRIGRAM\n </h2>\n <p\n style={{\n fontSize: `${layoutCalculation.bodyFontSize}px`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_TERTIARY),\n marginTop: \"20px\",\n letterSpacing: \"1px\",\n }}\n >\n Korean Martial Arts Dojang\n </p>\n </div>\n\n {/* Start Button */}\n <button\n onClick={handleStart}\n disabled={isLoading}\n aria-label={\n isLoading\n ? \"Starting game and initializing audio\"\n : \"Start game and initialize audio\"\n }\n aria-busy={isLoading}\n aria-describedby=\"splash-instructions\"\n style={{\n padding: layoutCalculation.buttonPadding,\n fontSize: `${layoutCalculation.buttonFontSize}px`,\n fontFamily: FONT_FAMILY.CYBER,\n fontWeight: 700,\n color: isLoading ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT) : hexColorToCSS(KOREAN_COLORS.BLACK),\n background: isLoading\n ? hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_LIGHT)\n : `linear-gradient(135deg, #${HEX_COLORS.PRIMARY_CYAN} 0%, #${HEX_COLORS.ACCENT_GOLD} 100%)`,\n border: \"none\",\n borderRadius: \"8px\",\n cursor: isLoading ? \"not-allowed\" : \"pointer\",\n textTransform: \"uppercase\",\n letterSpacing: \"2px\",\n transition: \"all 0.3s ease\",\n boxShadow: isLoading\n ? \"none\"\n : `0 4px 20px #${HEX_COLORS.PRIMARY_CYAN}40`,\n position: \"relative\",\n zIndex: 1,\n opacity: isLoading ? 0.6 : 1,\n }}\n onMouseEnter={(e) => {\n if (!isLoading) {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 6px 30px #${HEX_COLORS.PRIMARY_CYAN}60`;\n }\n }}\n onMouseLeave={(e) => {\n if (!isLoading) {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 4px 20px #${HEX_COLORS.PRIMARY_CYAN}40`;\n }\n }}\n data-testid=\"splash-start-button\"\n >\n {isLoading ? \"시작 중... Starting...\" : \"시작 | Start\"}\n </button>\n\n {/* Instructions */}\n <div\n id=\"splash-instructions\"\n style={{\n marginTop: \"40px\",\n textAlign: \"center\",\n color: hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n fontSize: `${layoutCalculation.instructionsFontSize}px`,\n maxWidth: \"600px\",\n padding: \"0 20px\",\n zIndex: 1,\n }}\n >\n <p style={{ margin: \"8px 0\" }}>\n Audio initialization requires user interaction\n </p>\n <p style={{ margin: \"8px 0\" }}>\n Click the button above to enable sound and start the game\n </p>\n </div>\n\n {/* Version info */}\n <div\n role=\"contentinfo\"\n aria-label={`Application version ${typeof APP_VERSION !== \"undefined\" ? APP_VERSION : \"0.5.3\"}`}\n style={{\n position: \"absolute\",\n bottom: \"20px\",\n right: \"20px\",\n color: hexColorToCSS(KOREAN_COLORS.UI_STEEL_GRAY),\n fontSize: \"10px\",\n zIndex: 1,\n }}\n >\n v{typeof APP_VERSION !== \"undefined\" ? APP_VERSION : \"0.5.3\"}\n </div>\n </div>\n );\n};\n\nexport default SplashScreen;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"VitalPointOverlayControlsPure.js","names":[],"sources":["../../../../src/components/shared/ui/VitalPointOverlayControlsPure.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsPure - Pure DOM controls for vital point visualization\n *\n * Pure DOM version (no Three.js Html wrapper) for use in HUD overlays.\n * Identical functionality to VitalPointOverlayControlsHtml but renders as pure React DOM.\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/ui/VitalPointOverlayControlsPure\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../types/common\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"@/utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsPure component\n */\nexport interface VitalPointOverlayControlsPureProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsPure Component\n * Pure DOM version - renders directly without Three.js Html wrapper\n */\nexport const VitalPointOverlayControlsPure: React.FC<\n VitalPointOverlayControlsPureProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile],\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\"),\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\"),\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query),\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange],\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40, inset 0 0 20px ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.ACCENT_BLUE)} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED,\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY,\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport default VitalPointOverlayControlsPure;\n"],"mappings":"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"}
1
+ {"version":3,"file":"VitalPointOverlayControlsPure.js","names":[],"sources":["../../../../src/components/shared/ui/VitalPointOverlayControlsPure.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsPure - Pure DOM controls for vital point visualization\n *\n * Pure DOM version (no Three.js Html wrapper) for use in HUD overlays.\n * Identical functionality to VitalPointOverlayControlsHtml but renders as pure React DOM.\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/ui/VitalPointOverlayControlsPure\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../types/common\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"@/utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsPure component\n */\nexport interface VitalPointOverlayControlsPureProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsPure Component\n * Pure DOM version - renders directly without Three.js Html wrapper\n */\nexport const VitalPointOverlayControlsPure: React.FC<\n VitalPointOverlayControlsPureProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile],\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\"),\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\"),\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query),\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange],\n );\n\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40, inset 0 0 20px ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.ACCENT_BLUE)} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED,\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY,\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport default VitalPointOverlayControlsPure;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeControl.js","names":[],"sources":["../../../../src/components/shared/ui/VolumeControl.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString, toHex } from \"../../../utils/colorUtils\";\n\nexport interface VolumeControlProps {\n readonly position?:\n | \"top-right\"\n | \"bottom-right\"\n | \"top-left\"\n | \"bottom-left\"\n | \"custom\";\n readonly style?: React.CSSProperties;\n readonly showLabels?: boolean;\n readonly compact?: boolean;\n}\n\n/**\n * Volume Control Component\n *\n * Provides controls for:\n * - Master volume\n * - Music volume\n * - SFX volume\n * - Mute/unmute toggle\n *\n * Inspired by template game (https://github.com/Hack23/game)\n */\nexport const VolumeControl: React.FC<VolumeControlProps> = ({\n position = \"top-right\",\n style,\n showLabels = true,\n compact = false,\n}) => {\n const audio = useAudio();\n\n const [masterVolume, setMasterVolume] = useState(audio.masterVolume ?? 1.0);\n const [musicVolume, setMusicVolume] = useState(audio.musicVolume ?? 0.7);\n const [sfxVolume, setSfxVolume] = useState(audio.sfxVolume ?? 0.8);\n const [isMuted, setIsMuted] = useState(audio.muted ?? false);\n\n React.useEffect(() => {\n setMasterVolume(audio.masterVolume ?? 1.0);\n setMusicVolume(audio.musicVolume ?? 0.7);\n setSfxVolume(audio.sfxVolume ?? 0.8);\n setIsMuted(audio.muted ?? false);\n }, [audio.masterVolume, audio.musicVolume, audio.sfxVolume, audio.muted]);\n\n const getPositionStyle = useMemo((): React.CSSProperties => {\n if (position === \"custom\") return {};\n\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n zIndex: 1000,\n padding: compact ? \"8px 12px\" : \"12px 16px\",\n };\n\n switch (position) {\n case \"top-right\":\n return { ...baseStyle, top: \"20px\", right: \"20px\" };\n case \"bottom-right\":\n return { ...baseStyle, bottom: \"20px\", right: \"20px\" };\n case \"top-left\":\n return { ...baseStyle, top: \"20px\", left: \"20px\" };\n case \"bottom-left\":\n return { ...baseStyle, bottom: \"20px\", left: \"20px\" };\n default:\n return baseStyle;\n }\n }, [position, compact]);\n\n const containerStyle = useMemo(\n (): React.CSSProperties => ({\n ...getPositionStyle,\n display: \"flex\",\n flexDirection: compact ? \"row\" : \"column\",\n alignItems: \"center\",\n gap: compact ? \"12px\" : \"8px\",\n background: \"rgba(33, 38, 45, 0.95)\",\n borderRadius: \"12px\",\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n pointerEvents: \"auto\", // Enable interaction even when parent has pointerEvents: none\n ...style,\n }),\n [getPositionStyle, compact, style],\n );\n\n const handleMasterVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMasterVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"master\", value);\n }\n },\n [audio],\n );\n\n const handleMusicVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMusicVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"music\", value);\n }\n },\n [audio],\n );\n\n const handleSfxVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setSfxVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"sfx\", value);\n }\n },\n [audio],\n );\n\n const handleMuteToggle = useCallback(() => {\n setIsMuted((prevMuted) => {\n const newMuted = !prevMuted;\n if (audio.isAudioReady) {\n if (newMuted) {\n audio.mute();\n } else {\n audio.unmute();\n }\n }\n return newMuted;\n });\n }, [audio]);\n\n const sliderStyle = useMemo(\n (): React.CSSProperties => ({\n width: compact ? \"60px\" : \"100px\",\n cursor: \"pointer\",\n accentColor: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }),\n [compact],\n );\n\n const labelStyle = useMemo(\n (): React.CSSProperties => ({\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: compact ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n minWidth: compact ? \"40px\" : \"50px\",\n textAlign: \"left\",\n }),\n [compact],\n );\n\n const valueStyle = useMemo(\n (): React.CSSProperties => ({\n color: `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`,\n fontSize: compact ? \"10px\" : \"11px\",\n minWidth: \"35px\",\n textAlign: \"right\",\n }),\n [compact],\n );\n\n const controlRowStyle = useMemo(\n (): React.CSSProperties => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n width: \"100%\",\n }),\n [],\n );\n\n if (compact) {\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"6px 12px\",\n borderRadius: \"6px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇\" : \"🔊\"}\n </button>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n aria-label=\"마스터 볼륨 | Master Volume\"\n style={sliderStyle}\n title=\"마스터 볼륨 | Master Volume\"\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n {showLabels && (\n <div\n style={{\n color: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n textAlign: \"center\",\n }}\n >\n 🎵 음량 | Volume\n </div>\n )}\n\n {/* Master Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"master-volume\" style={labelStyle}>\n 전체 | Master\n </label>\n <input\n id=\"master-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n\n {/* Music Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"music-volume\" style={labelStyle}>\n 음악 | Music\n </label>\n <input\n id=\"music-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={musicVolume}\n onChange={handleMusicVolumeChange}\n data-testid=\"music-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(musicVolume * 100)}%</span>\n </div>\n\n {/* SFX Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"sfx-volume\" style={labelStyle}>\n 효과음 | SFX\n </label>\n <input\n id=\"sfx-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={sfxVolume}\n onChange={handleSfxVolumeChange}\n data-testid=\"sfx-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(sfxVolume * 100)}%</span>\n </div>\n\n {/* Mute Toggle */}\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"8px 16px\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n marginTop: \"4px\",\n width: \"100%\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇 음소거 해제 | Unmute\" : \"🔊 음소거 | Mute\"}\n </button>\n\n {/* Audio Status Indicator */}\n <div\n style={{\n color: audio.isAudioReady\n ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`\n : hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n fontSize: \"10px\",\n marginTop: \"4px\",\n textAlign: \"center\",\n }}\n >\n {audio.isAudioReady\n ? \"✓ 오디오 준비됨 | Audio Ready\"\n : \"⏳ 초기화 중... | Initializing...\"}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"VolumeControl.js","names":[],"sources":["../../../../src/components/shared/ui/VolumeControl.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString, toHex } from \"../../../utils/colorUtils\";\n\nexport interface VolumeControlProps {\n readonly position?:\n | \"top-right\"\n | \"bottom-right\"\n | \"top-left\"\n | \"bottom-left\"\n | \"custom\";\n readonly style?: React.CSSProperties;\n readonly showLabels?: boolean;\n readonly compact?: boolean;\n}\n\n/**\n * Volume Control Component\n *\n * Provides controls for:\n * - Master volume\n * - Music volume\n * - SFX volume\n * - Mute/unmute toggle\n *\n * Inspired by template game (https://github.com/Hack23/game)\n */\nexport const VolumeControl: React.FC<VolumeControlProps> = ({\n position = \"top-right\",\n style,\n showLabels = true,\n compact = false,\n}) => {\n const audio = useAudio();\n\n const [masterVolume, setMasterVolume] = useState(audio.masterVolume ?? 1.0);\n const [musicVolume, setMusicVolume] = useState(audio.musicVolume ?? 0.7);\n const [sfxVolume, setSfxVolume] = useState(audio.sfxVolume ?? 0.8);\n const [isMuted, setIsMuted] = useState(audio.muted ?? false);\n\n React.useEffect(() => {\n setMasterVolume(audio.masterVolume ?? 1.0);\n setMusicVolume(audio.musicVolume ?? 0.7);\n setSfxVolume(audio.sfxVolume ?? 0.8);\n setIsMuted(audio.muted ?? false);\n }, [audio.masterVolume, audio.musicVolume, audio.sfxVolume, audio.muted]);\n\n const getPositionStyle = useMemo((): React.CSSProperties => {\n if (position === \"custom\") return {};\n\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n zIndex: 1000,\n padding: compact ? \"8px 12px\" : \"12px 16px\",\n };\n\n switch (position) {\n case \"top-right\":\n return { ...baseStyle, top: \"20px\", right: \"20px\" };\n case \"bottom-right\":\n return { ...baseStyle, bottom: \"20px\", right: \"20px\" };\n case \"top-left\":\n return { ...baseStyle, top: \"20px\", left: \"20px\" };\n case \"bottom-left\":\n return { ...baseStyle, bottom: \"20px\", left: \"20px\" };\n default:\n return baseStyle;\n }\n }, [position, compact]);\n\n const containerStyle = useMemo(\n (): React.CSSProperties => ({\n ...getPositionStyle,\n display: \"flex\",\n flexDirection: compact ? \"row\" : \"column\",\n alignItems: \"center\",\n gap: compact ? \"12px\" : \"8px\",\n background: \"rgba(33, 38, 45, 0.95)\",\n borderRadius: \"12px\",\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n pointerEvents: \"auto\", // Enable interaction even when parent has pointerEvents: none\n ...style,\n }),\n [getPositionStyle, compact, style],\n );\n\n const handleMasterVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMasterVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"master\", value);\n }\n },\n [audio],\n );\n\n const handleMusicVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMusicVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"music\", value);\n }\n },\n [audio],\n );\n\n const handleSfxVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setSfxVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"sfx\", value);\n }\n },\n [audio],\n );\n\n const handleMuteToggle = useCallback(() => {\n setIsMuted((prevMuted) => {\n const newMuted = !prevMuted;\n if (audio.isAudioReady) {\n if (newMuted) {\n audio.mute();\n } else {\n audio.unmute();\n }\n }\n return newMuted;\n });\n }, [audio]);\n\n const sliderStyle = useMemo(\n (): React.CSSProperties => ({\n width: compact ? \"60px\" : \"100px\",\n cursor: \"pointer\",\n accentColor: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }),\n [compact],\n );\n\n const labelStyle = useMemo(\n (): React.CSSProperties => ({\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: compact ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n minWidth: compact ? \"40px\" : \"50px\",\n textAlign: \"left\",\n }),\n [compact],\n );\n\n const valueStyle = useMemo(\n (): React.CSSProperties => ({\n color: `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`,\n fontSize: compact ? \"10px\" : \"11px\",\n minWidth: \"35px\",\n textAlign: \"right\",\n }),\n [compact],\n );\n\n const controlRowStyle = useMemo(\n (): React.CSSProperties => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n width: \"100%\",\n }),\n [],\n );\n\n if (compact) {\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"6px 12px\",\n borderRadius: \"6px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇\" : \"🔊\"}\n </button>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n aria-label=\"마스터 볼륨 | Master Volume\"\n style={sliderStyle}\n title=\"마스터 볼륨 | Master Volume\"\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n {showLabels && (\n <div\n style={{\n color: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n textAlign: \"center\",\n }}\n >\n 🎵 음량 | Volume\n </div>\n )}\n\n {/* Master Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"master-volume\" style={labelStyle}>\n 전체 | Master\n </label>\n <input\n id=\"master-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n\n {/* Music Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"music-volume\" style={labelStyle}>\n 음악 | Music\n </label>\n <input\n id=\"music-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={musicVolume}\n onChange={handleMusicVolumeChange}\n data-testid=\"music-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(musicVolume * 100)}%</span>\n </div>\n\n {/* SFX Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"sfx-volume\" style={labelStyle}>\n 효과음 | SFX\n </label>\n <input\n id=\"sfx-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={sfxVolume}\n onChange={handleSfxVolumeChange}\n data-testid=\"sfx-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(sfxVolume * 100)}%</span>\n </div>\n\n {/* Mute Toggle */}\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"8px 16px\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n marginTop: \"4px\",\n width: \"100%\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇 음소거 해제 | Unmute\" : \"🔊 음소거 | Mute\"}\n </button>\n\n {/* Audio Status Indicator */}\n <div\n style={{\n color: audio.isAudioReady\n ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`\n : hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n fontSize: \"10px\",\n marginTop: \"4px\",\n textAlign: \"center\",\n }}\n >\n {audio.isAudioReady\n ? \"✓ 오디오 준비됨 | Audio Ready\"\n : \"⏳ 초기화 중... | Initializing...\"}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConfirmDialog.js","names":[],"sources":["../../../../../src/components/shared/ui/shared/ConfirmDialog.tsx"],"sourcesContent":["/**\n * ConfirmDialog Component - Modal confirmation dialog\n * \n * Features:\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - Responsive sizing\n * - Keyboard shortcuts (Enter = confirm, Esc = cancel)\n * \n * Now uses BaseButtonOverlayHtml for consistent styling\n */\n\nimport React, { useEffect } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { BaseButtonOverlayHtml } from \"../../base\";\n\nexport interface ConfirmDialogProps {\n readonly isOpen: boolean;\n readonly title: string;\n readonly titleKorean: string;\n readonly message: string;\n readonly messageKorean: string;\n readonly onConfirm: () => void;\n readonly onCancel: () => void;\n readonly isMobile: boolean;\n}\n\n/**\n * ConfirmDialog - Modal confirmation dialog with Korean theming\n */\nexport const ConfirmDialog: React.FC<ConfirmDialogProps> = ({\n isOpen,\n title,\n titleKorean,\n message,\n messageKorean,\n onConfirm,\n onCancel,\n isMobile,\n}) => {\n const audio = useAudio();\n\n useEffect(() => {\n if (!isOpen) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n onConfirm();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n onCancel();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [isOpen, onConfirm, onCancel]);\n\n if (!isOpen) return null;\n\n return (\n <div\n data-testid=\"confirm-dialog\"\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: hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.8),\n backdropFilter: \"blur(8px)\",\n zIndex: 1001,\n pointerEvents: \"auto\",\n }}\n onClick={(e) => {\n if (e.target === e.currentTarget) {\n onCancel();\n }\n }}\n >\n <div\n style={{\n padding: isMobile ? \"24px\" : \"32px\",\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"12px\",\n maxWidth: isMobile ? \"320px\" : \"400px\",\n boxShadow: `0 0 30px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }}\n >\n {/* Title */}\n <h2\n data-testid=\"dialog-title\"\n style={{\n fontSize: isMobile ? \"20px\" : \"24px\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 16px 0\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.4)}`,\n }}\n >\n {titleKorean}\n <br />\n {title}\n </h2>\n\n {/* Message */}\n <p\n data-testid=\"dialog-message\"\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n margin: \"0 0 24px 0\",\n lineHeight: \"1.5\",\n textAlign: \"center\",\n }}\n >\n {messageKorean}\n <br />\n {message}\n </p>\n\n {/* Buttons - Now using BaseButtonOverlayHtml */}\n <div\n style={{\n display: \"flex\",\n gap: \"12px\",\n }}\n >\n <BaseButtonOverlayHtml\n korean=\"취소\"\n english=\"Cancel\"\n onClick={() => {\n audio.playSFX(\"menu_back\");\n onCancel();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n variant=\"secondary\"\n size={isMobile ? \"sm\" : \"md\"}\n isMobile={isMobile}\n testId=\"cancel-button\"\n style={{ flex: 1 }}\n />\n <BaseButtonOverlayHtml\n korean=\"확인\"\n english=\"Confirm\"\n onClick={() => {\n audio.playSFX(\"menu_select\");\n onConfirm();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n variant=\"primary\"\n size={isMobile ? \"sm\" : \"md\"}\n isMobile={isMobile}\n testId=\"confirm-button\"\n style={{ flex: 1 }}\n />\n </div>\n </div>\n </div>\n );\n};\n\nexport default ConfirmDialog;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAa,iBAA+C,EAC1D,QACA,OACA,aACA,SACA,eACA,WACA,UACA,eACI;CACJ,MAAM,QAAQ,SAAS;CAEvB,gBAAgB;EACd,IAAI,CAAC,QAAQ;EAEb,MAAM,iBAAiB,MAAqB;GAC1C,IAAI,EAAE,QAAQ,SAAS;IACrB,EAAE,eAAe;IACjB,UAAU;GACZ,OAAO,IAAI,EAAE,QAAQ,UAAU;IAC7B,EAAE,eAAe;IACjB,SAAS;GACX;EACF;EAEA,OAAO,iBAAiB,WAAW,aAAa;EAChD,aAAa,OAAO,oBAAoB,WAAW,aAAa;CAClE,GAAG;EAAC;EAAQ;EAAW;CAAQ,CAAC;CAEhC,IAAI,CAAC,QAAQ,OAAO;CAEpB,OACE,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,iBAAiB,gBAAgB,cAAc,aAAa,EAAG;GAC/D,gBAAgB;GAChB,QAAQ;GACR,eAAe;EACjB;EACA,UAAU,MAAM;GACd,IAAI,EAAE,WAAW,EAAE,eACjB,SAAS;EAEb;YAEA,qBAAC,OAAD;GACE,OAAO;IACL,SAAS,WAAW,SAAS;IAC7B,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;IACpE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAG;IACpE,cAAc;IACd,UAAU,WAAW,UAAU;IAC/B,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;GACxE;aARF;IAWE,qBAAC,MAAD;KACE,eAAY;KACZ,OAAO;MACL,UAAU,WAAW,SAAS;MAC9B,OAAO,gBAAgB,cAAc,aAAa,CAAC;MACnD,YAAY,YAAY;MACxB,YAAY;MACZ,QAAQ;MACR,WAAW;MACX,YAAY,YAAY,gBAAgB,cAAc,aAAa,EAAG;KACxE;eAVF;MAYG;MACD,oBAAC,MAAD,CAAK,CAAA;MACJ;KACC;;IAGJ,qBAAC,KAAD;KACE,eAAY;KACZ,OAAO;MACL,UAAU,WAAW,SAAS;MAC9B,OAAO,gBAAgB,cAAc,cAAc,CAAC;MACpD,YAAY,YAAY;MACxB,QAAQ;MACR,YAAY;MACZ,WAAW;KACb;eATF;MAWG;MACD,oBAAC,MAAD,CAAK,CAAA;MACJ;KACA;;IAGH,qBAAC,OAAD;KACE,OAAO;MACL,SAAS;MACT,KAAK;KACP;eAJF,CAME,oBAAC,uBAAD;MACE,QAAO;MACP,SAAQ;MACR,eAAe;OACb,MAAM,QAAQ,WAAW;OACzB,SAAS;MACX;MACA,oBAAoB,MAAM,QAAQ,YAAY;MAC9C,SAAQ;MACR,MAAM,WAAW,OAAO;MACd;MACV,QAAO;MACP,OAAO,EAAE,MAAM,EAAE;KAClB,CAAA,GACD,oBAAC,uBAAD;MACE,QAAO;MACP,SAAQ;MACR,eAAe;OACb,MAAM,QAAQ,aAAa;OAC3B,UAAU;MACZ;MACA,oBAAoB,MAAM,QAAQ,YAAY;MAC9C,SAAQ;MACR,MAAM,WAAW,OAAO;MACd;MACV,QAAO;MACP,OAAO,EAAE,MAAM,EAAE;KAClB,CAAA,CACE;;GACF;;CACF,CAAA;AAET"}
1
+ {"version":3,"file":"ConfirmDialog.js","names":[],"sources":["../../../../../src/components/shared/ui/shared/ConfirmDialog.tsx"],"sourcesContent":["/**\n * ConfirmDialog Component - Modal confirmation dialog\n * \n * Features:\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - Responsive sizing\n * - Keyboard shortcuts (Enter = confirm, Esc = cancel)\n * \n * Now uses BaseButtonOverlayHtml for consistent styling\n */\n\nimport React, { useEffect } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { BaseButtonOverlayHtml } from \"../../base\";\n\nexport interface ConfirmDialogProps {\n readonly isOpen: boolean;\n readonly title: string;\n readonly titleKorean: string;\n readonly message: string;\n readonly messageKorean: string;\n readonly onConfirm: () => void;\n readonly onCancel: () => void;\n readonly isMobile: boolean;\n}\n\n/**\n * ConfirmDialog - Modal confirmation dialog with Korean theming\n */\nexport const ConfirmDialog: React.FC<ConfirmDialogProps> = ({\n isOpen,\n title,\n titleKorean,\n message,\n messageKorean,\n onConfirm,\n onCancel,\n isMobile,\n}) => {\n const audio = useAudio();\n\n useEffect(() => {\n if (!isOpen) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Enter\") {\n e.preventDefault();\n onConfirm();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n onCancel();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [isOpen, onConfirm, onCancel]);\n\n if (!isOpen) return null;\n\n return (\n <div\n data-testid=\"confirm-dialog\"\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: hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.8),\n backdropFilter: \"blur(8px)\",\n zIndex: 1001,\n pointerEvents: \"auto\",\n }}\n onClick={(e) => {\n if (e.target === e.currentTarget) {\n onCancel();\n }\n }}\n >\n <div\n style={{\n padding: isMobile ? \"24px\" : \"32px\",\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n borderRadius: \"12px\",\n maxWidth: isMobile ? \"320px\" : \"400px\",\n boxShadow: `0 0 30px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }}\n >\n {/* Title */}\n <h2\n data-testid=\"dialog-title\"\n style={{\n fontSize: isMobile ? \"20px\" : \"24px\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n margin: \"0 0 16px 0\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.4)}`,\n }}\n >\n {titleKorean}\n <br />\n {title}\n </h2>\n\n {/* Message */}\n <p\n data-testid=\"dialog-message\"\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n margin: \"0 0 24px 0\",\n lineHeight: \"1.5\",\n textAlign: \"center\",\n }}\n >\n {messageKorean}\n <br />\n {message}\n </p>\n\n {/* Buttons - Now using BaseButtonOverlayHtml */}\n <div\n style={{\n display: \"flex\",\n gap: \"12px\",\n }}\n >\n <BaseButtonOverlayHtml\n korean=\"취소\"\n english=\"Cancel\"\n onClick={() => {\n audio.playSFX(\"menu_back\");\n onCancel();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n variant=\"secondary\"\n size={isMobile ? \"sm\" : \"md\"}\n isMobile={isMobile}\n testId=\"cancel-button\"\n style={{ flex: 1 }}\n />\n <BaseButtonOverlayHtml\n korean=\"확인\"\n english=\"Confirm\"\n onClick={() => {\n audio.playSFX(\"menu_select\");\n onConfirm();\n }}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n variant=\"primary\"\n size={isMobile ? \"sm\" : \"md\"}\n isMobile={isMobile}\n testId=\"confirm-button\"\n style={{ flex: 1 }}\n />\n </div>\n </div>\n </div>\n );\n};\n\nexport default ConfirmDialog;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAa,iBAA+C,EAC1D,QACA,OACA,aACA,SACA,eACA,WACA,UACA,eACI;CACJ,MAAM,QAAQ,SAAS;CAEvB,gBAAgB;EACd,IAAI,CAAC,QAAQ;EAEb,MAAM,iBAAiB,MAAqB;GAC1C,IAAI,EAAE,QAAQ,SAAS;IACrB,EAAE,eAAe;IACjB,UAAU;GACZ,OAAO,IAAI,EAAE,QAAQ,UAAU;IAC7B,EAAE,eAAe;IACjB,SAAS;GACX;EACF;EAEA,OAAO,iBAAiB,WAAW,aAAa;EAChD,aAAa,OAAO,oBAAoB,WAAW,aAAa;CAClE,GAAG;EAAC;EAAQ;EAAW;CAAQ,CAAC;CAEhC,IAAI,CAAC,QAAQ,OAAO;CAEpB,OACE,oBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,iBAAiB,gBAAgB,cAAc,aAAa,EAAG;GAC/D,gBAAgB;GAChB,QAAQ;GACR,eAAe;EACjB;EACA,UAAU,MAAM;GACd,IAAI,EAAE,WAAW,EAAE,eACjB,SAAS;EAEb;EAEA,UAAA,qBAAC,OAAD;GACE,OAAO;IACL,SAAS,WAAW,SAAS;IAC7B,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;IACpE,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAG;IACpE,cAAc;IACd,UAAU,WAAW,UAAU;IAC/B,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;GACxE;GARF,UAAA;IAWE,qBAAC,MAAD;KACE,eAAY;KACZ,OAAO;MACL,UAAU,WAAW,SAAS;MAC9B,OAAO,gBAAgB,cAAc,aAAa,CAAC;MACnD,YAAY,YAAY;MACxB,YAAY;MACZ,QAAQ;MACR,WAAW;MACX,YAAY,YAAY,gBAAgB,cAAc,aAAa,EAAG;KACxE;KAVF,UAAA;MAYG;MACD,oBAAC,MAAD,CAAK,CAAA;MACJ;KACC;;IAGJ,qBAAC,KAAD;KACE,eAAY;KACZ,OAAO;MACL,UAAU,WAAW,SAAS;MAC9B,OAAO,gBAAgB,cAAc,cAAc,CAAC;MACpD,YAAY,YAAY;MACxB,QAAQ;MACR,YAAY;MACZ,WAAW;KACb;KATF,UAAA;MAWG;MACD,oBAAC,MAAD,CAAK,CAAA;MACJ;KACA;;IAGH,qBAAC,OAAD;KACE,OAAO;MACL,SAAS;MACT,KAAK;KACP;KAJF,UAAA,CAME,oBAAC,uBAAD;MACE,QAAO;MACP,SAAQ;MACR,eAAe;OACb,MAAM,QAAQ,WAAW;OACzB,SAAS;MACX;MACA,oBAAoB,MAAM,QAAQ,YAAY;MAC9C,SAAQ;MACR,MAAM,WAAW,OAAO;MACd;MACV,QAAO;MACP,OAAO,EAAE,MAAM,EAAE;KAClB,CAAA,GACD,oBAAC,uBAAD;MACE,QAAO;MACP,SAAQ;MACR,eAAe;OACb,MAAM,QAAQ,aAAa;OAC3B,UAAU;MACZ;MACA,oBAAoB,MAAM,QAAQ,YAAY;MAC9C,SAAQ;MACR,MAAM,WAAW,OAAO;MACd;MACV,QAAO;MACP,OAAO,EAAE,MAAM,EAAE;KAClB,CAAA,CACE;;GACF;;CACF,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"BalanceIndicatorOverlayHtml.js","names":[],"sources":["../../../../src/components/ui/combat/BalanceIndicatorOverlayHtml.tsx"],"sourcesContent":["/**\n * BalanceIndicatorOverlayHtml Component - 3D Html overlay for balance state\n *\n * Displays balance state with vulnerability indicators using Html from @react-three/drei.\n * Shows:\n * - Current balance percentage\n * - Vulnerability state (red border + shake effect)\n * - Bilingual Korean/English tooltips\n * - Stance transition vulnerability\n * - Rapid change penalty indicator\n *\n * @module components/ui/combat/BalanceIndicatorOverlayHtml\n * @category Combat UI\n * @korean 균형표시오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { Html } from \"@react-three/drei\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport type { BalancePlayerState } from \"@/systems/combat/BalanceSystem\";\n\nexport interface BalanceIndicatorOverlayHtmlProps {\n /**\n * Player state with balance and transition data\n * @korean 플레이어상태\n */\n readonly player: BalancePlayerState;\n\n /**\n * Current game time in milliseconds\n * @korean 현재시간\n */\n readonly currentTime: number;\n\n /**\n * Position in 3D space [x, y, z]\n * @korean 3D위치\n */\n readonly position?: [number, number, number];\n\n /**\n * Mobile responsive mode\n * @korean 모바일여부\n */\n readonly isMobile?: boolean;\n}\n\n/**\n * Get balance color based on percentage\n */\nfunction getBalanceColor(balance: number): string {\n if (balance >= 80) return `#${KOREAN_COLORS.POSITIVE_GREEN.toString(16).padStart(6, \"0\")}`;\n if (balance >= 50) return `#${KOREAN_COLORS.WARNING_YELLOW.toString(16).padStart(6, \"0\")}`;\n if (balance >= 20) return `#${KOREAN_COLORS.WARNING_ORANGE.toString(16).padStart(6, \"0\")}`;\n return `#${KOREAN_COLORS.ACCENT_RED.toString(16).padStart(6, \"0\")}`;\n}\n\n/**\n * Get balance state label (bilingual)\n */\nfunction getBalanceLabel(balance: number): {\n korean: string;\n english: string;\n} {\n if (balance >= 80)\n return { korean: \"안정\", english: \"Stable\" };\n if (balance >= 50)\n return { korean: \"불안정\", english: \"Unsteady\" };\n if (balance >= 20)\n return { korean: \"균형상실\", english: \"Off-Balance\" };\n return { korean: \"낙하중\", english: \"Falling\" };\n}\n\n/**\n * BalanceIndicatorOverlayHtml - 3D Html overlay for balance visualization\n *\n * Renders above player character in 3D space using Html from @react-three/drei.\n * Shows balance percentage, vulnerability state, and active modifiers.\n *\n * Features:\n * - Red border + shake animation when vulnerable\n * - Bilingual Korean/English labels\n * - Transition vulnerability indicator\n * - Rapid change penalty warning\n *\n * @example\n * ```tsx\n * <BalanceIndicatorOverlayHtml\n * player={playerState}\n * currentTime={Date.now()}\n * position={[0, 2.5, 0]}\n * isMobile={false}\n * />\n * ```\n */\nexport const BalanceIndicatorOverlayHtml: React.FC<\n BalanceIndicatorOverlayHtmlProps\n> = ({ player, currentTime, position = [0, 2, 0], isMobile = false }) => {\n const isVulnerable = useMemo(() => {\n const isTransitioning = player.transitionState?.isTransitioning ?? false;\n const isLowBalance = player.balance < 50; // Off-balance or worse\n const hasPenalty =\n player.rapidChangePenaltyEnd !== undefined &&\n currentTime < player.rapidChangePenaltyEnd;\n return isTransitioning ?? isLowBalance ?? hasPenalty;\n }, [player.balance, player.transitionState, player.rapidChangePenaltyEnd, currentTime]);\n\n const balanceColor = useMemo(\n () => getBalanceColor(player.balance),\n [player.balance]\n );\n\n const balanceLabel = useMemo(\n () => getBalanceLabel(player.balance),\n [player.balance]\n );\n\n const vulnerableColorHex = `#${KOREAN_COLORS.ACCENT_RED.toString(16).padStart(6, \"0\")}`;\n\n const containerStyle = useMemo(() => {\n const baseStyle: React.CSSProperties = {\n border: isVulnerable\n ? `2px solid ${vulnerableColorHex}`\n : `2px solid ${balanceColor}`,\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n backgroundColor: \"rgba(10, 10, 10, 0.85)\",\n borderRadius: \"8px\",\n boxShadow: isVulnerable\n ? `0 0 16px ${vulnerableColorHex}`\n : `0 0 12px ${balanceColor}`,\n transition: \"all 0.3s ease-out\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"12px\" : \"14px\",\n minWidth: isMobile ? \"140px\" : \"180px\",\n textAlign: \"center\" as const,\n userSelect: \"none\" as const,\n pointerEvents: \"none\" as const,\n };\n\n if (isVulnerable) {\n baseStyle.animation = \"shake 0.3s infinite\";\n }\n\n return baseStyle;\n }, [isVulnerable, balanceColor, vulnerableColorHex, isMobile]);\n\n const balancePercentage = Math.round(player.balance);\n\n return (\n <>\n {/* Inject keyframes for shake animation */}\n <Html position={position} center>\n <style>\n {`\n @keyframes shake {\n 0%, 100% { transform: translateX(0); }\n 25% { transform: translateX(-2px); }\n 75% { transform: translateX(2px); }\n }\n `}\n </style>\n <div style={containerStyle} data-testid=\"balance-indicator-overlay\">\n {/* Balance percentage and label */}\n <div\n style={{\n color: balanceColor,\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n >\n 균형 | Balance: {balancePercentage}%\n </div>\n\n {/* Balance state label */}\n <div\n style={{\n color: `#${KOREAN_COLORS.TEXT_SECONDARY.toString(16).padStart(6, \"0\")}`,\n fontSize: isMobile ? \"10px\" : \"12px\",\n }}\n >\n {balanceLabel.korean} | {balanceLabel.english}\n </div>\n\n {/* Vulnerability indicators */}\n {player.transitionState?.isTransitioning && (\n <div\n style={{\n color: vulnerableColorHex,\n fontSize: isMobile ? \"10px\" : \"11px\",\n marginTop: \"4px\",\n fontWeight: \"bold\",\n }}\n >\n 취약 | Vulnerable!\n </div>\n )}\n\n {/* Rapid change penalty indicator */}\n {player.rapidChangePenaltyEnd && currentTime < player.rapidChangePenaltyEnd && (\n <div\n style={{\n color: `#${KOREAN_COLORS.WARNING_ORANGE.toString(16).padStart(6, \"0\")}`,\n fontSize: isMobile ? \"9px\" : \"10px\",\n marginTop: \"2px\",\n }}\n >\n 급속변경 벌칙 | Rapid Change Penalty\n </div>\n )}\n </div>\n </Html>\n </>\n );\n};\n\nBalanceIndicatorOverlayHtml.displayName = \"BalanceIndicatorOverlayHtml\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"BalanceIndicatorOverlayHtml.js","names":[],"sources":["../../../../src/components/ui/combat/BalanceIndicatorOverlayHtml.tsx"],"sourcesContent":["/**\n * BalanceIndicatorOverlayHtml Component - 3D Html overlay for balance state\n *\n * Displays balance state with vulnerability indicators using Html from @react-three/drei.\n * Shows:\n * - Current balance percentage\n * - Vulnerability state (red border + shake effect)\n * - Bilingual Korean/English tooltips\n * - Stance transition vulnerability\n * - Rapid change penalty indicator\n *\n * @module components/ui/combat/BalanceIndicatorOverlayHtml\n * @category Combat UI\n * @korean 균형표시오버레이\n */\n\nimport React, { useMemo } from \"react\";\nimport { Html } from \"@react-three/drei\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport type { BalancePlayerState } from \"@/systems/combat/BalanceSystem\";\n\nexport interface BalanceIndicatorOverlayHtmlProps {\n /**\n * Player state with balance and transition data\n * @korean 플레이어상태\n */\n readonly player: BalancePlayerState;\n\n /**\n * Current game time in milliseconds\n * @korean 현재시간\n */\n readonly currentTime: number;\n\n /**\n * Position in 3D space [x, y, z]\n * @korean 3D위치\n */\n readonly position?: [number, number, number];\n\n /**\n * Mobile responsive mode\n * @korean 모바일여부\n */\n readonly isMobile?: boolean;\n}\n\n/**\n * Get balance color based on percentage\n */\nfunction getBalanceColor(balance: number): string {\n if (balance >= 80) return `#${KOREAN_COLORS.POSITIVE_GREEN.toString(16).padStart(6, \"0\")}`;\n if (balance >= 50) return `#${KOREAN_COLORS.WARNING_YELLOW.toString(16).padStart(6, \"0\")}`;\n if (balance >= 20) return `#${KOREAN_COLORS.WARNING_ORANGE.toString(16).padStart(6, \"0\")}`;\n return `#${KOREAN_COLORS.ACCENT_RED.toString(16).padStart(6, \"0\")}`;\n}\n\n/**\n * Get balance state label (bilingual)\n */\nfunction getBalanceLabel(balance: number): {\n korean: string;\n english: string;\n} {\n if (balance >= 80)\n return { korean: \"안정\", english: \"Stable\" };\n if (balance >= 50)\n return { korean: \"불안정\", english: \"Unsteady\" };\n if (balance >= 20)\n return { korean: \"균형상실\", english: \"Off-Balance\" };\n return { korean: \"낙하중\", english: \"Falling\" };\n}\n\n/**\n * BalanceIndicatorOverlayHtml - 3D Html overlay for balance visualization\n *\n * Renders above player character in 3D space using Html from @react-three/drei.\n * Shows balance percentage, vulnerability state, and active modifiers.\n *\n * Features:\n * - Red border + shake animation when vulnerable\n * - Bilingual Korean/English labels\n * - Transition vulnerability indicator\n * - Rapid change penalty warning\n *\n * @example\n * ```tsx\n * <BalanceIndicatorOverlayHtml\n * player={playerState}\n * currentTime={Date.now()}\n * position={[0, 2.5, 0]}\n * isMobile={false}\n * />\n * ```\n */\nexport const BalanceIndicatorOverlayHtml: React.FC<\n BalanceIndicatorOverlayHtmlProps\n> = ({ player, currentTime, position = [0, 2, 0], isMobile = false }) => {\n const isVulnerable = useMemo(() => {\n const isTransitioning = player.transitionState?.isTransitioning ?? false;\n const isLowBalance = player.balance < 50; // Off-balance or worse\n const hasPenalty =\n player.rapidChangePenaltyEnd !== undefined &&\n currentTime < player.rapidChangePenaltyEnd;\n return isTransitioning ?? isLowBalance ?? hasPenalty;\n }, [player.balance, player.transitionState, player.rapidChangePenaltyEnd, currentTime]);\n\n const balanceColor = useMemo(\n () => getBalanceColor(player.balance),\n [player.balance]\n );\n\n const balanceLabel = useMemo(\n () => getBalanceLabel(player.balance),\n [player.balance]\n );\n\n const vulnerableColorHex = `#${KOREAN_COLORS.ACCENT_RED.toString(16).padStart(6, \"0\")}`;\n\n const containerStyle = useMemo(() => {\n const baseStyle: React.CSSProperties = {\n border: isVulnerable\n ? `2px solid ${vulnerableColorHex}`\n : `2px solid ${balanceColor}`,\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n backgroundColor: \"rgba(10, 10, 10, 0.85)\",\n borderRadius: \"8px\",\n boxShadow: isVulnerable\n ? `0 0 16px ${vulnerableColorHex}`\n : `0 0 12px ${balanceColor}`,\n transition: \"all 0.3s ease-out\",\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: isMobile ? \"12px\" : \"14px\",\n minWidth: isMobile ? \"140px\" : \"180px\",\n textAlign: \"center\" as const,\n userSelect: \"none\" as const,\n pointerEvents: \"none\" as const,\n };\n\n if (isVulnerable) {\n baseStyle.animation = \"shake 0.3s infinite\";\n }\n\n return baseStyle;\n }, [isVulnerable, balanceColor, vulnerableColorHex, isMobile]);\n\n const balancePercentage = Math.round(player.balance);\n\n return (\n <>\n {/* Inject keyframes for shake animation */}\n <Html position={position} center>\n <style>\n {`\n @keyframes shake {\n 0%, 100% { transform: translateX(0); }\n 25% { transform: translateX(-2px); }\n 75% { transform: translateX(2px); }\n }\n `}\n </style>\n <div style={containerStyle} data-testid=\"balance-indicator-overlay\">\n {/* Balance percentage and label */}\n <div\n style={{\n color: balanceColor,\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n >\n 균형 | Balance: {balancePercentage}%\n </div>\n\n {/* Balance state label */}\n <div\n style={{\n color: `#${KOREAN_COLORS.TEXT_SECONDARY.toString(16).padStart(6, \"0\")}`,\n fontSize: isMobile ? \"10px\" : \"12px\",\n }}\n >\n {balanceLabel.korean} | {balanceLabel.english}\n </div>\n\n {/* Vulnerability indicators */}\n {player.transitionState?.isTransitioning && (\n <div\n style={{\n color: vulnerableColorHex,\n fontSize: isMobile ? \"10px\" : \"11px\",\n marginTop: \"4px\",\n fontWeight: \"bold\",\n }}\n >\n 취약 | Vulnerable!\n </div>\n )}\n\n {/* Rapid change penalty indicator */}\n {player.rapidChangePenaltyEnd && currentTime < player.rapidChangePenaltyEnd && (\n <div\n style={{\n color: `#${KOREAN_COLORS.WARNING_ORANGE.toString(16).padStart(6, \"0\")}`,\n fontSize: isMobile ? \"9px\" : \"10px\",\n marginTop: \"2px\",\n }}\n >\n 급속변경 벌칙 | Rapid Change Penalty\n </div>\n )}\n </div>\n </Html>\n </>\n );\n};\n\nBalanceIndicatorOverlayHtml.displayName = \"BalanceIndicatorOverlayHtml\";\n"],"mappings":"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"}
@@ -273,7 +273,6 @@ function useKeyboardControls({ onStanceChange, onAction, enabled = true, current
273
273
  onAction("pause");
274
274
  addToQueue("Pause", e.key);
275
275
  if (playSFX) playSFX("menu_select");
276
- break;
277
276
  }
278
277
  }
279
278
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useKeyboardControls.js","names":[],"sources":["../../src/hooks/useKeyboardControls.ts"],"sourcesContent":["/**\n * Custom hook for keyboard controls with visual feedback\n * Manages keyboard input for trigram stance switching and combat actions\n *\n * @module hooks/useKeyboardControls\n * @category Input System\n * @korean 키보드 컨트롤 훅\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { FOOTWORK_KOREAN_TERMS } from \"../systems/animation/core/types\";\nimport { ControlMapper } from \"../utils/controlMapping\";\n\n// Korean terminology constants (module-level to avoid recreation on every keystroke)\nconst DIAGONAL_KOREAN_TERMS: Record<string, string> = {\n step_forward_left: \"전좌측보법 (Forward-Left)\",\n step_forward_right: \"전우측보법 (Forward-Right)\",\n step_back_left: \"후좌측보법 (Back-Left)\",\n step_back_right: \"후우측보법 (Back-Right)\",\n};\n\nconst STEP_KOREAN_TERMS: Record<string, string> = {\n step_forward: \"전진보법 (Forward Step)\",\n step_back: \"후퇴보법 (Retreat Step)\",\n step_left: \"좌측면보법 (Left Step)\",\n step_right: \"우측면보법 (Right Step)\",\n};\n\n// Footwork pattern Korean terminology - maps animation states to display names\n// Reuses FOOTWORK_KOREAN_TERMS from types.ts but adds specific animation state labels\nconst FOOTWORK_DISPLAY_TERMS: Record<string, string> = {\n footwork_circular_left: `${FOOTWORK_KOREAN_TERMS.circular.korean} 좌 (Circular Left)`,\n footwork_circular_right: `${FOOTWORK_KOREAN_TERMS.circular.korean} 우 (Circular Right)`,\n footwork_pivot_left: `${FOOTWORK_KOREAN_TERMS.pivot.korean} 좌 (Pivot Left)`,\n footwork_pivot_right: `${FOOTWORK_KOREAN_TERMS.pivot.korean} 우 (Pivot Right)`,\n footwork_slide_forward: `${FOOTWORK_KOREAN_TERMS.slide.korean} 전 (Slide Forward)`,\n footwork_slide_back: `${FOOTWORK_KOREAN_TERMS.slide.korean} 후 (Slide Back)`,\n footwork_slide_left: `${FOOTWORK_KOREAN_TERMS.slide.korean} 좌 (Slide Left)`,\n footwork_slide_right: `${FOOTWORK_KOREAN_TERMS.slide.korean} 우 (Slide Right)`,\n footwork_shuffle: `${FOOTWORK_KOREAN_TERMS.shuffle.korean} (Shuffle)`,\n};\n\n// Step direction mapping (all move_* actions covered explicitly)\nconst STEP_DIRECTION_MAP: Record<string, string> = {\n move_up: \"step_forward\",\n move_down: \"step_back\",\n move_left: \"step_left\",\n move_right: \"step_right\",\n};\n\n/**\n * Queued input for input buffer display\n */\nexport interface QueuedInput {\n readonly action: string;\n readonly key: string;\n readonly timestamp: number;\n}\n\n/**\n * Props for useKeyboardControls hook\n */\nexport interface UseKeyboardControlsProps {\n /** Callback when stance changes (receives stance index 0-7) */\n readonly onStanceChange: (stance: number) => void;\n /** Callback for combat actions (attack, block, etc.) */\n readonly onAction: (action: string) => void;\n /** Whether keyboard input is enabled */\n readonly enabled?: boolean;\n /** Current stance index (0-7) for validation */\n readonly currentStance?: number;\n /** Callback when hints toggle is requested */\n readonly onToggleHints?: () => void;\n /** Play sound effect function */\n readonly playSFX?: (soundId: string) => void;\n /** Current animation state for grounded detection */\n readonly currentAnimationState?: string;\n}\n\n/**\n * Return type for useKeyboardControls hook\n */\nexport interface UseKeyboardControlsReturn {\n /** Queued inputs for display */\n readonly queuedInputs: readonly QueuedInput[];\n /** Whether hints are visible */\n readonly showHints: boolean;\n /** Toggle hints visibility */\n readonly toggleHints: () => void;\n}\n\n/**\n * Maximum number of inputs to keep in queue\n */\nconst MAX_QUEUE_SIZE = 3;\n\n/**\n * Time to keep inputs in queue (milliseconds)\n */\nconst QUEUE_RETENTION_TIME = 2000;\n\n/**\n * Custom hook for handling keyboard controls with visual feedback\n *\n * Features:\n * - Trigram stance switching (1-8 keys or custom bindings)\n * - Combat action handling (attack, block, movement)\n * - Input queue visualization\n * - Keyboard hints toggle (F1)\n * - Invalid input prevention\n * - Audio feedback integration\n *\n * @example\n * ```typescript\n * const { queuedInputs, showHints, toggleHints } = useKeyboardControls({\n * onStanceChange: (stance) => handleStanceChange(stance),\n * onAction: (action) => handleAction(action),\n * enabled: !isPaused,\n * currentStance: player.stance,\n * playSFX: audio.playSFX,\n * });\n * ```\n *\n * @public\n * @korean 키보드컨트롤사용\n */\nexport function useKeyboardControls({\n onStanceChange,\n onAction,\n enabled = true,\n currentStance = 0,\n onToggleHints,\n playSFX,\n currentAnimationState,\n}: UseKeyboardControlsProps): UseKeyboardControlsReturn {\n const [queuedInputs, setQueuedInputs] = useState<readonly QueuedInput[]>([]);\n const [showHints, setShowHints] = useState(false);\n const controlMapperRef = useRef<ControlMapper>(new ControlMapper());\n const lastStanceChangeRef = useRef<number>(0);\n\n // Track currently pressed keys for diagonal step detection\n const pressedKeysRef = useRef<Set<string>>(new Set());\n\n /**\n * Toggle hints visibility\n */\n const toggleHints = useCallback(() => {\n setShowHints((prev) => {\n const newState = !prev;\n if (onToggleHints) {\n onToggleHints();\n }\n // Play UI sound\n if (playSFX) {\n playSFX(\"menu_select\");\n }\n return newState;\n });\n }, [onToggleHints, playSFX]);\n\n /**\n * Detect diagonal step direction from pressed keys\n * Returns step action for diagonal or null if not diagonal\n *\n * @korean 대각선발걸음감지\n */\n const getDiagonalStepAction = useCallback((): string | null => {\n const keys = pressedKeysRef.current;\n const mapper = controlMapperRef.current;\n\n // Check for forward diagonals\n if (keys.has(mapper.getBindings().movement.up.toLowerCase())) {\n if (keys.has(mapper.getBindings().movement.left.toLowerCase())) {\n return \"step_forward_left\"; // 전좌측보법\n }\n if (keys.has(mapper.getBindings().movement.right.toLowerCase())) {\n return \"step_forward_right\"; // 전우측보법\n }\n }\n\n // Check for backward diagonals\n if (keys.has(mapper.getBindings().movement.down.toLowerCase())) {\n if (keys.has(mapper.getBindings().movement.left.toLowerCase())) {\n return \"step_back_left\"; // 후좌측보법\n }\n if (keys.has(mapper.getBindings().movement.right.toLowerCase())) {\n return \"step_back_right\"; // 후우측보법\n }\n }\n\n return null;\n }, []);\n\n /**\n * Add input to queue\n */\n const addToQueue = useCallback((action: string, key: string) => {\n const newInput: QueuedInput = {\n action,\n key,\n timestamp: Date.now(),\n };\n\n setQueuedInputs((prev) => {\n const updated = [newInput, ...prev].slice(0, MAX_QUEUE_SIZE);\n return updated;\n });\n }, []);\n\n /**\n * Clean up old queued inputs\n * Single interval for component lifetime to avoid recreating on every input\n */\n useEffect(() => {\n const interval = setInterval(() => {\n const now = Date.now();\n setQueuedInputs((prev) => {\n if (prev.length === 0) return prev;\n return prev.filter(\n (input) => now - input.timestamp < QUEUE_RETENTION_TIME\n );\n });\n }, 500);\n\n return () => clearInterval(interval);\n }, []); // Empty deps - single interval for component lifetime\n\n /**\n * Handle keyboard input\n */\n useEffect(() => {\n if (!enabled) return;\n\n // Copy ref value for cleanup\n const pressedKeys = pressedKeysRef.current;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n const mapper = controlMapperRef.current;\n\n // Track pressed keys for diagonal detection\n pressedKeysRef.current.add(e.key.toLowerCase());\n\n // F1: Toggle hints (prevent default browser help)\n if (e.key === \"F1\") {\n e.preventDefault();\n toggleHints();\n return;\n }\n\n // Escape: Close hints if open\n if (e.key === \"Escape\" && showHints) {\n e.preventDefault();\n setShowHints(false);\n return;\n }\n\n // Recovery input detection (기상 입력 감지)\n // When player is in ground state, any button triggers recovery options\n const isGrounded = currentAnimationState?.startsWith(\"ground_\");\n\n if (isGrounded) {\n // Space: Quick/default recovery\n if (e.key === \" \") {\n e.preventDefault();\n onAction(\"recovery_quick\");\n addToQueue(\"기상 (Quick Recovery)\", \"Space\");\n if (playSFX) playSFX(\"stance_change\");\n return;\n }\n\n // R or Enter: Roll recovery (회전기상) - fast but costs stamina\n if (e.key.toLowerCase() === \"r\" || e.key === \"Enter\") {\n e.preventDefault();\n onAction(\"recovery_roll\");\n addToQueue(\"회전기상 (Roll Recovery)\", e.key);\n if (playSFX) playSFX(\"stance_change\");\n return;\n }\n\n // Shift: Defensive getup (방어기상) - slow but protected\n if (e.shiftKey) {\n e.preventDefault();\n onAction(\"recovery_defensive\");\n addToQueue(\"방어기상 (Defensive Getup)\", \"Shift\");\n if (playSFX) playSFX(\"stance_change\");\n return;\n }\n\n // If grounded, don't process other inputs (can't attack/move while down)\n return;\n }\n\n // Check for stance change (1-8 or custom bindings)\n const stanceIndex = mapper.getStanceForKey(e.key);\n if (stanceIndex !== null) {\n e.preventDefault();\n\n // Prevent switching to the same stance\n if (stanceIndex === currentStance) {\n // Invalid input feedback (shake animation trigger)\n addToQueue(`Stance ${stanceIndex + 1} (already active)`, e.key);\n\n // Play error sound\n if (playSFX) {\n playSFX(\"menu_error\");\n }\n return;\n }\n\n // Throttle stance changes (minimum 8 frames at 60fps = 133ms)\n const now = Date.now();\n if (now - lastStanceChangeRef.current < 133) {\n return;\n }\n lastStanceChangeRef.current = now;\n\n // Execute stance change\n onStanceChange(stanceIndex);\n addToQueue(`Stance ${stanceIndex + 1}`, e.key);\n\n // Play stance change sound\n if (playSFX) {\n playSFX(\"stance_change\");\n }\n return;\n }\n\n // Handle other actions\n const action = mapper.getActionForKey(e.key);\n if (action) {\n e.preventDefault();\n\n switch (action) {\n case \"attack\":\n onAction(\"attack\");\n addToQueue(\"Attack\", e.key);\n if (playSFX) playSFX(\"attack_light\");\n break;\n\n case \"block\":\n onAction(\"block\");\n addToQueue(\"Block\", e.key);\n if (playSFX) playSFX(\"block\");\n break;\n\n case \"move_up\":\n case \"move_down\":\n case \"move_left\":\n case \"move_right\":\n // Check for Alt+A/D modifier for slide left/right (MUST be checked first)\n if (e.altKey) {\n // Alt modifier for slide left/right footwork patterns\n let slideFootwork: string | null = null;\n\n if (action === \"move_left\") {\n // Alt+A: Slide left (미끄럼보 좌)\n slideFootwork = \"footwork_slide_left\";\n } else if (action === \"move_right\") {\n // Alt+D: Slide right (미끄럼보 우)\n slideFootwork = \"footwork_slide_right\";\n }\n\n if (slideFootwork) {\n onAction(slideFootwork);\n addToQueue(\n FOOTWORK_DISPLAY_TERMS[slideFootwork] ?? \"Slide Footwork\",\n `Alt+${e.key}`\n );\n if (playSFX) playSFX(\"footstep\");\n }\n } else if (e.shiftKey && e.ctrlKey) {\n // Shift+Ctrl modifier for advanced footwork patterns\n let advancedFootwork: string | null = null;\n\n if (action === \"move_left\") {\n // Shift+Ctrl+A: Pivot left (축족회전 좌)\n advancedFootwork = \"footwork_pivot_left\";\n } else if (action === \"move_right\") {\n // Shift+Ctrl+D: Pivot right (축족회전 우)\n advancedFootwork = \"footwork_pivot_right\";\n } else if (action === \"move_up\" || action === \"move_down\") {\n // Shift+Ctrl+W or Shift+Ctrl+S: Shuffle step (섞음보)\n advancedFootwork = \"footwork_shuffle\";\n }\n\n if (advancedFootwork) {\n onAction(advancedFootwork);\n addToQueue(\n FOOTWORK_DISPLAY_TERMS[advancedFootwork] ??\n \"Advanced Footwork\",\n `Shift+Ctrl+${e.key}`\n );\n if (playSFX) playSFX(\"footstep\");\n }\n } else if (e.ctrlKey) {\n // Footwork patterns with Ctrl+WASD\n let footworkAction: string | null = null;\n\n if (action === \"move_left\") {\n // Ctrl+A: Circular step left (원형보)\n footworkAction = \"footwork_circular_left\";\n } else if (action === \"move_right\") {\n // Ctrl+D: Circular step right (원형보)\n footworkAction = \"footwork_circular_right\";\n } else if (action === \"move_up\") {\n // Ctrl+W: Slide forward (미끄럼보)\n footworkAction = \"footwork_slide_forward\";\n } else if (action === \"move_down\") {\n // Ctrl+S: Slide back (미끄럼보)\n footworkAction = \"footwork_slide_back\";\n }\n\n if (footworkAction) {\n onAction(footworkAction);\n addToQueue(\n FOOTWORK_DISPLAY_TERMS[footworkAction] ?? \"Footwork\",\n `Ctrl+${e.key}`\n );\n if (playSFX) playSFX(\"footstep\");\n }\n } else if (e.shiftKey) {\n // Check if Shift is held for tactical step instead of walk\n // Check for diagonal step first\n const diagonalStep = getDiagonalStepAction();\n\n if (diagonalStep) {\n // Diagonal tactical step\n onAction(diagonalStep);\n\n addToQueue(\n DIAGONAL_KOREAN_TERMS[diagonalStep] ?? \"Diagonal Step\",\n `Shift+${e.key}`\n );\n\n if (playSFX) playSFX(\"footstep\");\n } else {\n // Cardinal direction tactical step\n // Map move_up/move_down to step_forward/step_back to match AnimationState\n const stepDirection = STEP_DIRECTION_MAP[action];\n onAction(stepDirection);\n\n addToQueue(\n STEP_KOREAN_TERMS[stepDirection] ?? \"Step\",\n `Shift+${e.key}`\n );\n\n if (playSFX) playSFX(\"footstep\");\n }\n } else {\n // Regular movement\n onAction(action);\n addToQueue(action, e.key);\n }\n break;\n\n case \"stance_side_switch\":\n // H key: Switch front foot (stance side switch)\n onAction(\"stance_side_switch\");\n addToQueue(\"발 바꿈 (Switch Stance Side)\", e.key);\n if (playSFX) playSFX(\"stance_change\");\n break;\n\n case \"vital_points_overlay\":\n // Vital points overlay toggle\n onAction(\"vital_points_overlay\");\n addToQueue(\"Vital Points\", e.key);\n if (playSFX) playSFX(\"menu_select\");\n break;\n\n case \"pause\":\n // Pause menu toggle\n onAction(\"pause\");\n addToQueue(\"Pause\", e.key);\n if (playSFX) playSFX(\"menu_select\");\n break;\n }\n }\n };\n\n const handleKeyUp = (e: KeyboardEvent) => {\n // Remove key from pressed keys set\n pressedKeysRef.current.delete(e.key.toLowerCase());\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n // Clear pressed keys on cleanup using copied ref value\n pressedKeys.clear();\n };\n }, [\n enabled,\n currentStance,\n onStanceChange,\n onAction,\n addToQueue,\n toggleHints,\n getDiagonalStepAction,\n showHints,\n playSFX,\n currentAnimationState,\n ]);\n\n return {\n queuedInputs,\n showHints,\n toggleHints,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useKeyboardControls.js","names":[],"sources":["../../src/hooks/useKeyboardControls.ts"],"sourcesContent":["/**\n * Custom hook for keyboard controls with visual feedback\n * Manages keyboard input for trigram stance switching and combat actions\n *\n * @module hooks/useKeyboardControls\n * @category Input System\n * @korean 키보드 컨트롤 훅\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { FOOTWORK_KOREAN_TERMS } from \"../systems/animation/core/types\";\nimport { ControlMapper } from \"../utils/controlMapping\";\n\n// Korean terminology constants (module-level to avoid recreation on every keystroke)\nconst DIAGONAL_KOREAN_TERMS: Record<string, string> = {\n step_forward_left: \"전좌측보법 (Forward-Left)\",\n step_forward_right: \"전우측보법 (Forward-Right)\",\n step_back_left: \"후좌측보법 (Back-Left)\",\n step_back_right: \"후우측보법 (Back-Right)\",\n};\n\nconst STEP_KOREAN_TERMS: Record<string, string> = {\n step_forward: \"전진보법 (Forward Step)\",\n step_back: \"후퇴보법 (Retreat Step)\",\n step_left: \"좌측면보법 (Left Step)\",\n step_right: \"우측면보법 (Right Step)\",\n};\n\n// Footwork pattern Korean terminology - maps animation states to display names\n// Reuses FOOTWORK_KOREAN_TERMS from types.ts but adds specific animation state labels\nconst FOOTWORK_DISPLAY_TERMS: Record<string, string> = {\n footwork_circular_left: `${FOOTWORK_KOREAN_TERMS.circular.korean} 좌 (Circular Left)`,\n footwork_circular_right: `${FOOTWORK_KOREAN_TERMS.circular.korean} 우 (Circular Right)`,\n footwork_pivot_left: `${FOOTWORK_KOREAN_TERMS.pivot.korean} 좌 (Pivot Left)`,\n footwork_pivot_right: `${FOOTWORK_KOREAN_TERMS.pivot.korean} 우 (Pivot Right)`,\n footwork_slide_forward: `${FOOTWORK_KOREAN_TERMS.slide.korean} 전 (Slide Forward)`,\n footwork_slide_back: `${FOOTWORK_KOREAN_TERMS.slide.korean} 후 (Slide Back)`,\n footwork_slide_left: `${FOOTWORK_KOREAN_TERMS.slide.korean} 좌 (Slide Left)`,\n footwork_slide_right: `${FOOTWORK_KOREAN_TERMS.slide.korean} 우 (Slide Right)`,\n footwork_shuffle: `${FOOTWORK_KOREAN_TERMS.shuffle.korean} (Shuffle)`,\n};\n\n// Step direction mapping (all move_* actions covered explicitly)\nconst STEP_DIRECTION_MAP: Record<string, string> = {\n move_up: \"step_forward\",\n move_down: \"step_back\",\n move_left: \"step_left\",\n move_right: \"step_right\",\n};\n\n/**\n * Queued input for input buffer display\n */\nexport interface QueuedInput {\n readonly action: string;\n readonly key: string;\n readonly timestamp: number;\n}\n\n/**\n * Props for useKeyboardControls hook\n */\nexport interface UseKeyboardControlsProps {\n /** Callback when stance changes (receives stance index 0-7) */\n readonly onStanceChange: (stance: number) => void;\n /** Callback for combat actions (attack, block, etc.) */\n readonly onAction: (action: string) => void;\n /** Whether keyboard input is enabled */\n readonly enabled?: boolean;\n /** Current stance index (0-7) for validation */\n readonly currentStance?: number;\n /** Callback when hints toggle is requested */\n readonly onToggleHints?: () => void;\n /** Play sound effect function */\n readonly playSFX?: (soundId: string) => void;\n /** Current animation state for grounded detection */\n readonly currentAnimationState?: string;\n}\n\n/**\n * Return type for useKeyboardControls hook\n */\nexport interface UseKeyboardControlsReturn {\n /** Queued inputs for display */\n readonly queuedInputs: readonly QueuedInput[];\n /** Whether hints are visible */\n readonly showHints: boolean;\n /** Toggle hints visibility */\n readonly toggleHints: () => void;\n}\n\n/**\n * Maximum number of inputs to keep in queue\n */\nconst MAX_QUEUE_SIZE = 3;\n\n/**\n * Time to keep inputs in queue (milliseconds)\n */\nconst QUEUE_RETENTION_TIME = 2000;\n\n/**\n * Custom hook for handling keyboard controls with visual feedback\n *\n * Features:\n * - Trigram stance switching (1-8 keys or custom bindings)\n * - Combat action handling (attack, block, movement)\n * - Input queue visualization\n * - Keyboard hints toggle (F1)\n * - Invalid input prevention\n * - Audio feedback integration\n *\n * @example\n * ```typescript\n * const { queuedInputs, showHints, toggleHints } = useKeyboardControls({\n * onStanceChange: (stance) => handleStanceChange(stance),\n * onAction: (action) => handleAction(action),\n * enabled: !isPaused,\n * currentStance: player.stance,\n * playSFX: audio.playSFX,\n * });\n * ```\n *\n * @public\n * @korean 키보드컨트롤사용\n */\nexport function useKeyboardControls({\n onStanceChange,\n onAction,\n enabled = true,\n currentStance = 0,\n onToggleHints,\n playSFX,\n currentAnimationState,\n}: UseKeyboardControlsProps): UseKeyboardControlsReturn {\n const [queuedInputs, setQueuedInputs] = useState<readonly QueuedInput[]>([]);\n const [showHints, setShowHints] = useState(false);\n const controlMapperRef = useRef<ControlMapper>(new ControlMapper());\n const lastStanceChangeRef = useRef<number>(0);\n\n // Track currently pressed keys for diagonal step detection\n const pressedKeysRef = useRef<Set<string>>(new Set());\n\n /**\n * Toggle hints visibility\n */\n const toggleHints = useCallback(() => {\n setShowHints((prev) => {\n const newState = !prev;\n if (onToggleHints) {\n onToggleHints();\n }\n // Play UI sound\n if (playSFX) {\n playSFX(\"menu_select\");\n }\n return newState;\n });\n }, [onToggleHints, playSFX]);\n\n /**\n * Detect diagonal step direction from pressed keys\n * Returns step action for diagonal or null if not diagonal\n *\n * @korean 대각선발걸음감지\n */\n const getDiagonalStepAction = useCallback((): string | null => {\n const keys = pressedKeysRef.current;\n const mapper = controlMapperRef.current;\n\n // Check for forward diagonals\n if (keys.has(mapper.getBindings().movement.up.toLowerCase())) {\n if (keys.has(mapper.getBindings().movement.left.toLowerCase())) {\n return \"step_forward_left\"; // 전좌측보법\n }\n if (keys.has(mapper.getBindings().movement.right.toLowerCase())) {\n return \"step_forward_right\"; // 전우측보법\n }\n }\n\n // Check for backward diagonals\n if (keys.has(mapper.getBindings().movement.down.toLowerCase())) {\n if (keys.has(mapper.getBindings().movement.left.toLowerCase())) {\n return \"step_back_left\"; // 후좌측보법\n }\n if (keys.has(mapper.getBindings().movement.right.toLowerCase())) {\n return \"step_back_right\"; // 후우측보법\n }\n }\n\n return null;\n }, []);\n\n /**\n * Add input to queue\n */\n const addToQueue = useCallback((action: string, key: string) => {\n const newInput: QueuedInput = {\n action,\n key,\n timestamp: Date.now(),\n };\n\n setQueuedInputs((prev) => {\n const updated = [newInput, ...prev].slice(0, MAX_QUEUE_SIZE);\n return updated;\n });\n }, []);\n\n /**\n * Clean up old queued inputs\n * Single interval for component lifetime to avoid recreating on every input\n */\n useEffect(() => {\n const interval = setInterval(() => {\n const now = Date.now();\n setQueuedInputs((prev) => {\n if (prev.length === 0) return prev;\n return prev.filter(\n (input) => now - input.timestamp < QUEUE_RETENTION_TIME\n );\n });\n }, 500);\n\n return () => clearInterval(interval);\n }, []); // Empty deps - single interval for component lifetime\n\n /**\n * Handle keyboard input\n */\n useEffect(() => {\n if (!enabled) return;\n\n // Copy ref value for cleanup\n const pressedKeys = pressedKeysRef.current;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n const mapper = controlMapperRef.current;\n\n // Track pressed keys for diagonal detection\n pressedKeysRef.current.add(e.key.toLowerCase());\n\n // F1: Toggle hints (prevent default browser help)\n if (e.key === \"F1\") {\n e.preventDefault();\n toggleHints();\n return;\n }\n\n // Escape: Close hints if open\n if (e.key === \"Escape\" && showHints) {\n e.preventDefault();\n setShowHints(false);\n return;\n }\n\n // Recovery input detection (기상 입력 감지)\n // When player is in ground state, any button triggers recovery options\n const isGrounded = currentAnimationState?.startsWith(\"ground_\");\n\n if (isGrounded) {\n // Space: Quick/default recovery\n if (e.key === \" \") {\n e.preventDefault();\n onAction(\"recovery_quick\");\n addToQueue(\"기상 (Quick Recovery)\", \"Space\");\n if (playSFX) playSFX(\"stance_change\");\n return;\n }\n\n // R or Enter: Roll recovery (회전기상) - fast but costs stamina\n if (e.key.toLowerCase() === \"r\" || e.key === \"Enter\") {\n e.preventDefault();\n onAction(\"recovery_roll\");\n addToQueue(\"회전기상 (Roll Recovery)\", e.key);\n if (playSFX) playSFX(\"stance_change\");\n return;\n }\n\n // Shift: Defensive getup (방어기상) - slow but protected\n if (e.shiftKey) {\n e.preventDefault();\n onAction(\"recovery_defensive\");\n addToQueue(\"방어기상 (Defensive Getup)\", \"Shift\");\n if (playSFX) playSFX(\"stance_change\");\n return;\n }\n\n // If grounded, don't process other inputs (can't attack/move while down)\n return;\n }\n\n // Check for stance change (1-8 or custom bindings)\n const stanceIndex = mapper.getStanceForKey(e.key);\n if (stanceIndex !== null) {\n e.preventDefault();\n\n // Prevent switching to the same stance\n if (stanceIndex === currentStance) {\n // Invalid input feedback (shake animation trigger)\n addToQueue(`Stance ${stanceIndex + 1} (already active)`, e.key);\n\n // Play error sound\n if (playSFX) {\n playSFX(\"menu_error\");\n }\n return;\n }\n\n // Throttle stance changes (minimum 8 frames at 60fps = 133ms)\n const now = Date.now();\n if (now - lastStanceChangeRef.current < 133) {\n return;\n }\n lastStanceChangeRef.current = now;\n\n // Execute stance change\n onStanceChange(stanceIndex);\n addToQueue(`Stance ${stanceIndex + 1}`, e.key);\n\n // Play stance change sound\n if (playSFX) {\n playSFX(\"stance_change\");\n }\n return;\n }\n\n // Handle other actions\n const action = mapper.getActionForKey(e.key);\n if (action) {\n e.preventDefault();\n\n switch (action) {\n case \"attack\":\n onAction(\"attack\");\n addToQueue(\"Attack\", e.key);\n if (playSFX) playSFX(\"attack_light\");\n break;\n\n case \"block\":\n onAction(\"block\");\n addToQueue(\"Block\", e.key);\n if (playSFX) playSFX(\"block\");\n break;\n\n case \"move_up\":\n case \"move_down\":\n case \"move_left\":\n case \"move_right\":\n // Check for Alt+A/D modifier for slide left/right (MUST be checked first)\n if (e.altKey) {\n // Alt modifier for slide left/right footwork patterns\n let slideFootwork: string | null = null;\n\n if (action === \"move_left\") {\n // Alt+A: Slide left (미끄럼보 좌)\n slideFootwork = \"footwork_slide_left\";\n } else if (action === \"move_right\") {\n // Alt+D: Slide right (미끄럼보 우)\n slideFootwork = \"footwork_slide_right\";\n }\n\n if (slideFootwork) {\n onAction(slideFootwork);\n addToQueue(\n FOOTWORK_DISPLAY_TERMS[slideFootwork] ?? \"Slide Footwork\",\n `Alt+${e.key}`\n );\n if (playSFX) playSFX(\"footstep\");\n }\n } else if (e.shiftKey && e.ctrlKey) {\n // Shift+Ctrl modifier for advanced footwork patterns\n let advancedFootwork: string | null = null;\n\n if (action === \"move_left\") {\n // Shift+Ctrl+A: Pivot left (축족회전 좌)\n advancedFootwork = \"footwork_pivot_left\";\n } else if (action === \"move_right\") {\n // Shift+Ctrl+D: Pivot right (축족회전 우)\n advancedFootwork = \"footwork_pivot_right\";\n } else if (action === \"move_up\" || action === \"move_down\") {\n // Shift+Ctrl+W or Shift+Ctrl+S: Shuffle step (섞음보)\n advancedFootwork = \"footwork_shuffle\";\n }\n\n if (advancedFootwork) {\n onAction(advancedFootwork);\n addToQueue(\n FOOTWORK_DISPLAY_TERMS[advancedFootwork] ??\n \"Advanced Footwork\",\n `Shift+Ctrl+${e.key}`\n );\n if (playSFX) playSFX(\"footstep\");\n }\n } else if (e.ctrlKey) {\n // Footwork patterns with Ctrl+WASD\n let footworkAction: string | null = null;\n\n if (action === \"move_left\") {\n // Ctrl+A: Circular step left (원형보)\n footworkAction = \"footwork_circular_left\";\n } else if (action === \"move_right\") {\n // Ctrl+D: Circular step right (원형보)\n footworkAction = \"footwork_circular_right\";\n } else if (action === \"move_up\") {\n // Ctrl+W: Slide forward (미끄럼보)\n footworkAction = \"footwork_slide_forward\";\n } else if (action === \"move_down\") {\n // Ctrl+S: Slide back (미끄럼보)\n footworkAction = \"footwork_slide_back\";\n }\n\n if (footworkAction) {\n onAction(footworkAction);\n addToQueue(\n FOOTWORK_DISPLAY_TERMS[footworkAction] ?? \"Footwork\",\n `Ctrl+${e.key}`\n );\n if (playSFX) playSFX(\"footstep\");\n }\n } else if (e.shiftKey) {\n // Check if Shift is held for tactical step instead of walk\n // Check for diagonal step first\n const diagonalStep = getDiagonalStepAction();\n\n if (diagonalStep) {\n // Diagonal tactical step\n onAction(diagonalStep);\n\n addToQueue(\n DIAGONAL_KOREAN_TERMS[diagonalStep] ?? \"Diagonal Step\",\n `Shift+${e.key}`\n );\n\n if (playSFX) playSFX(\"footstep\");\n } else {\n // Cardinal direction tactical step\n // Map move_up/move_down to step_forward/step_back to match AnimationState\n const stepDirection = STEP_DIRECTION_MAP[action];\n onAction(stepDirection);\n\n addToQueue(\n STEP_KOREAN_TERMS[stepDirection] ?? \"Step\",\n `Shift+${e.key}`\n );\n\n if (playSFX) playSFX(\"footstep\");\n }\n } else {\n // Regular movement\n onAction(action);\n addToQueue(action, e.key);\n }\n break;\n\n case \"stance_side_switch\":\n // H key: Switch front foot (stance side switch)\n onAction(\"stance_side_switch\");\n addToQueue(\"발 바꿈 (Switch Stance Side)\", e.key);\n if (playSFX) playSFX(\"stance_change\");\n break;\n\n case \"vital_points_overlay\":\n // Vital points overlay toggle\n onAction(\"vital_points_overlay\");\n addToQueue(\"Vital Points\", e.key);\n if (playSFX) playSFX(\"menu_select\");\n break;\n\n case \"pause\":\n // Pause menu toggle\n onAction(\"pause\");\n addToQueue(\"Pause\", e.key);\n if (playSFX) playSFX(\"menu_select\");\n break;\n }\n }\n };\n\n const handleKeyUp = (e: KeyboardEvent) => {\n // Remove key from pressed keys set\n pressedKeysRef.current.delete(e.key.toLowerCase());\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n // Clear pressed keys on cleanup using copied ref value\n pressedKeys.clear();\n };\n }, [\n enabled,\n currentStance,\n onStanceChange,\n onAction,\n addToQueue,\n toggleHints,\n getDiagonalStepAction,\n showHints,\n playSFX,\n currentAnimationState,\n ]);\n\n return {\n queuedInputs,\n showHints,\n toggleHints,\n };\n}\n"],"mappings":"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"}
@@ -82,15 +82,24 @@ function useSkeletalAnimation(options) {
82
82
  selectedAnim ??= getAnimation("block") ?? null;
83
83
  playbackSpeed = 1;
84
84
  } else if (currentAnimation === "idle") {
85
- if (stance) selectedAnim = getAnimationByName(`stance_${stance}`) ?? null;
85
+ if (stance) {
86
+ const stanceIdleAnim = `stance_${stance}`;
87
+ selectedAnim = getAnimationByName(stanceIdleAnim) ?? null;
88
+ }
86
89
  selectedAnim ??= getAnimation("idle") ?? null;
87
90
  playbackSpeed = .5;
88
91
  } else if (currentAnimation === "walk") {
89
- if (stance) selectedAnim = getAnimationByName(`walk_${stance}`) ?? null;
92
+ if (stance) {
93
+ const stanceWalkAnim = `walk_${stance}`;
94
+ selectedAnim = getAnimationByName(stanceWalkAnim) ?? null;
95
+ }
90
96
  selectedAnim ??= getAnimation("walk") ?? null;
91
97
  playbackSpeed = 1;
92
98
  } else if (currentAnimation === "run") {
93
- if (stance) selectedAnim = getAnimationByName(`run_${stance}`) ?? null;
99
+ if (stance) {
100
+ const stanceRunAnim = `run_${stance}`;
101
+ selectedAnim = getAnimationByName(stanceRunAnim) ?? null;
102
+ }
94
103
  selectedAnim ??= getAnimation("run") ?? null;
95
104
  playbackSpeed = 1;
96
105
  } else if (currentAnimation?.startsWith("fall_")) {