blacktrigram 0.7.98 → 0.7.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioManager.js +2 -4
  3. package/lib/audio/AudioManager.js.map +1 -1
  4. package/lib/components/screens/combat/CombatScreen3D.js +8 -9
  5. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  6. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  7. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  8. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  9. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  10. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  11. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  12. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  13. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  14. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js +1 -1
  15. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  16. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  17. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js +2 -1
  18. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  19. package/lib/components/screens/combat/components/effects/InternalDamage3D.js +3 -1
  20. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  23. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +16 -15
  24. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  25. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  26. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  27. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  28. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  29. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  30. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  31. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  32. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  33. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  34. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  36. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  37. package/lib/components/screens/combat/hooks/useAICombat.js +0 -1
  38. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  39. package/lib/components/screens/combat/hooks/useCombatActions.js +7 -6
  40. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  41. package/lib/components/screens/combat/hooks/useCombatLayout.js +3 -1
  42. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  43. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  44. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  45. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js +7 -6
  46. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  47. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  48. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +34 -30
  49. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  50. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  51. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  52. package/lib/components/screens/endscreen/EndScreen3D.js +2 -2
  53. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  54. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js +1 -1
  55. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  56. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  57. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  58. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  59. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  60. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js +2 -2
  61. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  62. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js +2 -1
  63. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  64. package/lib/components/screens/intro/IntroScreen3D.js +2 -4
  65. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  66. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  67. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  68. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  69. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  72. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  74. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  75. package/lib/components/screens/training/TrainingScreen3D.js +10 -9
  76. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  77. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js +14 -13
  78. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  79. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  80. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  81. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  83. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  84. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  85. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  86. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  87. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  88. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/training/components/VitalPointMarker3D.js +8 -7
  90. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  91. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js +15 -13
  92. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  93. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  94. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  95. package/lib/components/screens/training/components/hud/TrainingRightHUD.js +3 -2
  96. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  97. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  98. package/lib/components/screens/training/hooks/useTrainingActions.js +1 -2
  99. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  100. package/lib/components/screens/training/hooks/useTrainingLayout.js +3 -1
  101. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  102. package/lib/components/shared/base/BaseButton.js.map +1 -1
  103. package/lib/components/shared/base/BaseButtonOverlayHtml.js +59 -53
  104. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  105. package/lib/components/shared/base/BasePanel.js +19 -18
  106. package/lib/components/shared/base/BasePanel.js.map +1 -1
  107. package/lib/components/shared/base/BaseText.js.map +1 -1
  108. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  109. package/lib/components/shared/mobile/GestureRecognizerPure.js +22 -18
  110. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  111. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  112. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  113. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  114. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js +4 -5
  115. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  116. package/lib/components/shared/three/anatomy/BoneClothing.js +29 -24
  117. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  118. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  119. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  120. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  121. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  122. package/lib/components/shared/three/effects/ActionFeedback.js +3 -2
  123. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  124. package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
  125. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  126. package/lib/components/shared/three/effects/HitEffects3D.js +3 -2
  127. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  128. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  129. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  130. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  131. package/lib/components/shared/three/effects/VitalPointMarkers3D.js +6 -5
  132. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  133. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  134. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  135. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  136. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  137. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  138. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  139. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  140. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  141. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  142. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  143. package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -1
  144. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  145. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  146. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  147. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  148. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  149. package/lib/components/shared/three/ui/PlayerHUD.js +2 -1
  150. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  151. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  152. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  153. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  154. package/lib/components/shared/three/ui/TechniqueBar.js +25 -24
  155. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  156. package/lib/components/shared/three/ui/TechniqueCard.js +1 -3
  157. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  158. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  159. package/lib/components/shared/ui/BackButton.js +2 -1
  160. package/lib/components/shared/ui/BackButton.js.map +1 -1
  161. package/lib/components/shared/ui/BaseHUDContainer.js +69 -68
  162. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  163. package/lib/components/shared/ui/CombatTimer.js.map +1 -1
  164. package/lib/components/shared/ui/ErrorModal.js.map +1 -1
  165. package/lib/components/shared/ui/LoadingState.js.map +1 -1
  166. package/lib/components/shared/ui/SplashScreen.js +2 -2
  167. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  168. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  169. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  170. package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
  171. package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
  172. package/lib/hooks/useKeyboardControls.js +0 -1
  173. package/lib/hooks/useKeyboardControls.js.map +1 -1
  174. package/lib/hooks/useSkeletalAnimation.js +12 -3
  175. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  176. package/lib/systems/CombatSystem.js +6 -2
  177. package/lib/systems/CombatSystem.js.map +1 -1
  178. package/lib/systems/PlayerEffectManager.js +0 -1
  179. package/lib/systems/PlayerEffectManager.js.map +1 -1
  180. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  181. package/lib/systems/ai/DecisionTree.js +10 -21
  182. package/lib/systems/ai/DecisionTree.js.map +1 -1
  183. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  184. package/lib/systems/animation/systems/AdvancedJointMovements.js +1 -3
  185. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  186. package/lib/systems/animation/systems/MuscleActivation.js +3 -4
  187. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  188. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  189. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js +2 -1
  190. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  191. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  192. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  193. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  194. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  195. package/lib/systems/combat/GrappleSystem.js +3 -9
  196. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  197. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  198. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  199. package/lib/systems/movement/InjuryMovementModifier.js +1 -1
  200. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  201. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  202. package/lib/systems/vitalpoint/DamageCalculator.js +8 -5
  203. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  204. package/lib/types/facial.js.map +1 -1
  205. package/lib/utils/accessibility.js +0 -1
  206. package/lib/utils/accessibility.js.map +1 -1
  207. package/lib/utils/effectUtils.js +1 -3
  208. package/lib/utils/effectUtils.js.map +1 -1
  209. package/lib/utils/inputSystem.js +0 -2
  210. package/lib/utils/inputSystem.js.map +1 -1
  211. package/lib/utils/koreanThemeHelpers.js +6 -3
  212. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  213. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  214. package/lib/utils/playerUtils.js +4 -2
  215. package/lib/utils/playerUtils.js.map +1 -1
  216. package/package.json +14 -14
@@ -1 +1 @@
1
- {"version":3,"file":"effectUtils.js","names":[],"sources":["../../src/utils/effectUtils.ts"],"sourcesContent":["import { HitEffect, StatusEffect } from \"../systems\";\nimport { EffectIntensity, EffectType, HitEffectType } from \"../systems/effects\";\nimport type { KoreanText, Position } from \"../types\";\n\n/**\n * Create a hit effect for visual feedback\n */\nexport function createHitEffect(\n id: string,\n type: HitEffectType,\n position: Position,\n intensity: number,\n duration: number = 1000\n): HitEffect {\n return {\n id,\n type,\n attackerId: \"unknown\",\n defenderId: \"unknown\",\n timestamp: Date.now(),\n duration,\n position,\n intensity,\n lifespan: duration,\n alpha: 1.0,\n size: 1.0,\n startTime: Date.now(),\n };\n}\n\n/**\n * Get hit effect color based on type\n */\nexport function getHitEffectColor(type: HitEffectType): number {\n switch (type) {\n case HitEffectType.CRITICAL_HIT:\n return 0xff6b00; // Orange\n case HitEffectType.VITAL_POINT_STRIKE:\n return 0xff0000; // Red\n case HitEffectType.BLOCK:\n return 0x4a90e2; // Blue\n case HitEffectType.MISS:\n return 0x888888; // Gray\n default:\n return 0xffffff; // White\n }\n}\n\n/**\n * Create a status effect\n */\nexport function createStatusEffect(\n id: string,\n type: EffectType,\n intensity: EffectIntensity,\n duration: number,\n description: KoreanText,\n source: string = \"unknown\"\n): StatusEffect {\n const currentTime = Date.now();\n return {\n id,\n type,\n intensity,\n duration,\n description,\n stackable: false,\n source,\n startTime: currentTime,\n endTime: currentTime + duration,\n };\n}\n\n/**\n * Calculate effect intensity based on damage\n */\nexport function calculateEffectIntensity(damage: number): EffectIntensity {\n // Fix: Use proper EffectIntensity values that match the type definition\n if (damage >= 80) return \"severe\" as EffectIntensity; // was \"extreme\"\n if (damage >= 50) return \"severe\" as EffectIntensity; // was \"critical\"\n if (damage >= 30) return \"moderate\" as EffectIntensity; // was \"high\"\n if (damage >= 15) return \"medium\" as EffectIntensity; // this one is correct\n if (damage >= 5) return \"minor\" as EffectIntensity; // was \"low\"\n return \"weak\" as EffectIntensity; // this one is correct\n}\n\n/**\n * Get effect duration modifier based on type\n */\nexport function getEffectDurationModifier(type: EffectType): number {\n switch (type) {\n case \"bleed\": // Fix: Use string literal instead of enum\n return 1.5;\n case \"poison\":\n return 2.0;\n case \"stun\":\n return 0.5;\n case \"burn\":\n return 1.2;\n default:\n return 1.0;\n }\n}\n\n/**\n * Apply effect to player stats\n */\nexport function applyEffectModifiers(\n baseValue: number,\n effects: readonly StatusEffect[],\n statType: \"attack\" | \"defense\" | \"speed\"\n): number {\n let modifier = 1.0;\n\n effects.forEach((effect) => {\n switch (effect.type) {\n case \"bleed\":\n if (statType === \"attack\") modifier *= 0.9;\n break;\n case \"strengthened\":\n if (statType === \"attack\") modifier *= 1.2;\n break;\n case \"weakened\":\n modifier *= 0.8;\n break;\n case \"exhausted\":\n if (statType === \"speed\") modifier *= 0.7;\n break;\n }\n });\n\n return Math.floor(baseValue * modifier);\n}\n\n/**\n * Check if effect is beneficial or harmful\n */\nexport function isEffectBeneficial(type: EffectType): boolean {\n const beneficialEffects: EffectType[] = [\n \"focused\",\n \"rage\",\n \"defensive\",\n \"strengthened\",\n ];\n return beneficialEffects.includes(type);\n}\n\n/**\n * Get effect description based on type and intensity\n */\nexport function getEffectDescription(\n type: EffectType,\n intensity: EffectIntensity\n): KoreanText {\n const descriptions: Record<\n EffectType,\n Record<EffectIntensity, KoreanText>\n > = {\n stun: {\n weak: { korean: \"가벼운 기절\", english: \"Light Stun\" },\n minor: { korean: \"경미한 기절\", english: \"Minor Stun\" },\n medium: { korean: \"기절\", english: \"Stun\" },\n moderate: { korean: \"기절\", english: \"Stun\" },\n high: { korean: \"심한 기절\", english: \"Heavy Stun\" },\n severe: { korean: \"심한 기절\", english: \"Severe Stun\" },\n critical: { korean: \"완전 기절\", english: \"Complete Stun\" },\n extreme: { korean: \"완전 기절\", english: \"Complete Stun\" },\n low: { korean: \"약한 기절\", english: \"Weak Stun\" },\n },\n poison: {\n weak: { korean: \"경미한 중독\", english: \"Minor Poison\" },\n minor: { korean: \"경미한 중독\", english: \"Minor Poison\" },\n medium: { korean: \"중독\", english: \"Poison\" },\n moderate: { korean: \"중독\", english: \"Poison\" },\n high: { korean: \"심한 중독\", english: \"Severe Poison\" },\n severe: { korean: \"심한 중독\", english: \"Severe Poison\" },\n critical: { korean: \"치명적 중독\", english: \"Lethal Poison\" },\n extreme: { korean: \"치명적 중독\", english: \"Lethal Poison\" },\n low: { korean: \"약한 중독\", english: \"Weak Poison\" },\n },\n weakened: {\n weak: { korean: \"약간 약화\", english: \"Slightly Weakened\" },\n minor: { korean: \"경미한 약화\", english: \"Minor Weakness\" },\n medium: { korean: \"약화\", english: \"Weakened\" },\n moderate: { korean: \"약화\", english: \"Weakened\" },\n high: { korean: \"심하게 약화\", english: \"Severely Weakened\" },\n severe: { korean: \"심하게 약화\", english: \"Severely Weakened\" },\n critical: { korean: \"극도로 약화\", english: \"Critically Weakened\" },\n extreme: { korean: \"극도로 약화\", english: \"Critically Weakened\" },\n low: { korean: \"약간 약화\", english: \"Slightly Weakened\" },\n },\n burn: {\n weak: { korean: \"가벼운 화상\", english: \"Minor Burn\" },\n minor: { korean: \"경미한 화상\", english: \"Minor Burn\" },\n medium: { korean: \"화상\", english: \"Burn\" },\n moderate: { korean: \"화상\", english: \"Burn\" },\n high: { korean: \"심한 화상\", english: \"Severe Burn\" },\n severe: { korean: \"심한 화상\", english: \"Severe Burn\" },\n critical: { korean: \"치명적 화상\", english: \"Critical Burn\" },\n extreme: { korean: \"치명적 화상\", english: \"Critical Burn\" },\n low: { korean: \"약한 화상\", english: \"Weak Burn\" },\n },\n bleed: {\n weak: { korean: \"가벼운 출혈\", english: \"Minor Bleeding\" },\n minor: { korean: \"경미한 출혈\", english: \"Minor Bleeding\" },\n medium: { korean: \"출혈\", english: \"Bleeding\" },\n moderate: { korean: \"출혈\", english: \"Bleeding\" },\n high: { korean: \"심한 출혈\", english: \"Heavy Bleeding\" },\n severe: { korean: \"심한 출혈\", english: \"Severe Bleeding\" },\n critical: { korean: \"치명적 출혈\", english: \"Critical Bleeding\" },\n extreme: { korean: \"치명적 출혈\", english: \"Critical Bleeding\" },\n low: { korean: \"약한 출혈\", english: \"Weak Bleeding\" },\n },\n exhausted: {\n weak: { korean: \"약간 지침\", english: \"Slightly Tired\" },\n minor: { korean: \"경미한 피로\", english: \"Minor Fatigue\" },\n medium: { korean: \"지침\", english: \"Exhausted\" },\n moderate: { korean: \"지침\", english: \"Exhausted\" },\n high: { korean: \"심하게 지침\", english: \"Severely Exhausted\" },\n severe: { korean: \"심하게 지침\", english: \"Severely Exhausted\" },\n critical: { korean: \"완전 탈진\", english: \"Completely Drained\" },\n extreme: { korean: \"완전 탈진\", english: \"Completely Drained\" },\n low: { korean: \"약간 피로\", english: \"Slightly Fatigued\" },\n },\n focused: {\n weak: { korean: \"약간 집중\", english: \"Slightly Focused\" },\n minor: { korean: \"경미한 집중\", english: \"Minor Focus\" },\n medium: { korean: \"집중\", english: \"Focused\" },\n moderate: { korean: \"집중\", english: \"Focused\" },\n high: { korean: \"깊은 집중\", english: \"Deeply Focused\" },\n severe: { korean: \"깊은 집중\", english: \"Deeply Focused\" },\n critical: { korean: \"완전 집중\", english: \"Completely Focused\" },\n extreme: { korean: \"완전 집중\", english: \"Completely Focused\" },\n low: { korean: \"약한 집중\", english: \"Weak Focus\" },\n },\n rage: {\n weak: { korean: \"약간 분노\", english: \"Slightly Enraged\" },\n minor: { korean: \"경미한 분노\", english: \"Minor Rage\" },\n medium: { korean: \"분노\", english: \"Enraged\" },\n moderate: { korean: \"분노\", english: \"Enraged\" },\n high: { korean: \"맹렬한 분노\", english: \"Furious\" },\n severe: { korean: \"맹렬한 분노\", english: \"Furious\" },\n critical: { korean: \"광분\", english: \"Berserk\" },\n extreme: { korean: \"광분\", english: \"Berserk\" },\n low: { korean: \"약한 분노\", english: \"Weak Rage\" },\n },\n defensive: {\n weak: { korean: \"약간 방어적\", english: \"Slightly Defensive\" },\n minor: { korean: \"경미한 방어\", english: \"Minor Defense\" },\n medium: { korean: \"방어적\", english: \"Defensive\" },\n moderate: { korean: \"방어적\", english: \"Defensive\" },\n high: { korean: \"높은 방어\", english: \"Highly Defensive\" },\n severe: { korean: \"높은 방어\", english: \"Highly Defensive\" },\n critical: { korean: \"완벽한 방어\", english: \"Perfect Defense\" },\n extreme: { korean: \"완벽한 방어\", english: \"Perfect Defense\" },\n low: { korean: \"약한 방어\", english: \"Weak Defense\" },\n },\n strengthened: {\n weak: { korean: \"약간 강화\", english: \"Slightly Strengthened\" },\n minor: { korean: \"경미한 강화\", english: \"Minor Strength\" },\n medium: { korean: \"강화\", english: \"Strengthened\" },\n moderate: { korean: \"강화\", english: \"Strengthened\" },\n high: { korean: \"크게 강화\", english: \"Greatly Strengthened\" },\n severe: { korean: \"크게 강화\", english: \"Greatly Strengthened\" },\n critical: { korean: \"극대 강화\", english: \"Maximally Strengthened\" },\n extreme: { korean: \"극대 강화\", english: \"Maximally Strengthened\" },\n low: { korean: \"약한 강화\", english: \"Weak Strength\" },\n },\n paralysis: {\n weak: { korean: \"가벼운 마비\", english: \"Minor Paralysis\" },\n minor: { korean: \"경미한 마비\", english: \"Minor Paralysis\" },\n medium: { korean: \"마비\", english: \"Paralysis\" },\n moderate: { korean: \"마비\", english: \"Paralysis\" },\n high: { korean: \"심한 마비\", english: \"Severe Paralysis\" },\n severe: { korean: \"심한 마비\", english: \"Severe Paralysis\" },\n critical: { korean: \"완전 마비\", english: \"Complete Paralysis\" },\n extreme: { korean: \"완전 마비\", english: \"Complete Paralysis\" },\n low: { korean: \"약한 마비\", english: \"Weak Paralysis\" },\n },\n confusion: {\n weak: { korean: \"약간 혼란\", english: \"Slight Confusion\" },\n minor: { korean: \"경미한 혼란\", english: \"Minor Confusion\" },\n medium: { korean: \"혼란\", english: \"Confusion\" },\n moderate: { korean: \"혼란\", english: \"Confusion\" },\n high: { korean: \"심한 혼란\", english: \"Severe Confusion\" },\n severe: { korean: \"심한 혼란\", english: \"Severe Confusion\" },\n critical: { korean: \"완전 혼란\", english: \"Complete Confusion\" },\n extreme: { korean: \"완전 혼란\", english: \"Complete Confusion\" },\n low: { korean: \"약한 혼란\", english: \"Weak Confusion\" },\n },\n vulnerability: {\n weak: { korean: \"약간 취약\", english: \"Slightly Vulnerable\" },\n minor: { korean: \"경미한 취약\", english: \"Minor Vulnerability\" },\n medium: { korean: \"취약\", english: \"Vulnerable\" },\n moderate: { korean: \"취약\", english: \"Vulnerable\" },\n high: { korean: \"매우 취약\", english: \"Very Vulnerable\" },\n severe: { korean: \"매우 취약\", english: \"Very Vulnerable\" },\n critical: { korean: \"극도로 취약\", english: \"Extremely Vulnerable\" },\n extreme: { korean: \"극도로 취약\", english: \"Extremely Vulnerable\" },\n low: { korean: \"약한 취약\", english: \"Weak Vulnerability\" },\n },\n stamina_drain: {\n weak: { korean: \"체력 소모\", english: \"Stamina Drain\" },\n minor: { korean: \"경미한 체력 소모\", english: \"Minor Stamina Drain\" },\n medium: { korean: \"체력 고갈\", english: \"Stamina Depletion\" },\n moderate: { korean: \"체력 고갈\", english: \"Stamina Depletion\" },\n high: { korean: \"심한 체력 고갈\", english: \"Severe Stamina Loss\" },\n severe: { korean: \"심한 체력 고갈\", english: \"Severe Stamina Loss\" },\n critical: { korean: \"완전 체력 고갈\", english: \"Complete Stamina Loss\" },\n extreme: { korean: \"완전 체력 고갈\", english: \"Complete Stamina Loss\" },\n low: { korean: \"약한 체력 소모\", english: \"Weak Stamina Drain\" },\n },\n };\n\n return (\n descriptions[type]?.[intensity] || {\n korean: `${type} 효과`,\n english: `${type} effect`,\n }\n );\n}\n\n/**\n * Update effect over time\n */\nexport function updateEffect(\n effect: StatusEffect,\n currentTime: number\n): StatusEffect | null {\n if (currentTime >= effect.endTime) {\n return null; // Effect has expired\n }\n\n return effect; // Effect is still active\n}\n\n/**\n * Combine multiple effects of same type\n */\nexport function combineEffects(effects: StatusEffect[]): StatusEffect[] {\n const combinedEffects: StatusEffect[] = [];\n // Fix: Use string type instead of EffectType since StatusEffect.type is string\n const effectsByType = new Map<string, StatusEffect[]>();\n\n // Group effects by type\n effects.forEach((effect) => {\n // Fix: effect.type is string, so use string methods\n if (!effectsByType.has(effect.type)) {\n effectsByType.set(effect.type, []);\n }\n const typeEffects = effectsByType.get(effect.type);\n if (typeEffects) {\n typeEffects.push(effect);\n }\n });\n\n // Combine stackable effects, keep latest non-stackable\n effectsByType.forEach((typeEffects, _) => {\n if (typeEffects.length === 0) return;\n\n const firstEffect = typeEffects[0];\n if (firstEffect.stackable) {\n // Keep all stackable effects\n combinedEffects.push(...typeEffects);\n } else {\n // Keep only the latest non-stackable effect\n const latestEffect = typeEffects.reduce((latest, current) =>\n current.startTime > latest.startTime ? current : latest\n );\n combinedEffects.push(latestEffect);\n }\n });\n\n return combinedEffects;\n}\n\n/**\n * Remove expired effects\n */\nexport function removeExpiredEffects(effects: StatusEffect[]): StatusEffect[] {\n const currentTime = Date.now();\n return effects.filter((effect) => effect.endTime > currentTime);\n}\n\n/**\n * Create effect from Korean text\n */\nexport function createEffectFromKoreanText(\n koreanText: string,\n englishText: string,\n duration: number = 3000\n): StatusEffect {\n // Fix: Provide all required arguments\n return createStatusEffect(\n koreanText.toLowerCase(),\n \"stun\" as EffectType, // Use proper EffectType\n \"medium\" as EffectIntensity,\n duration,\n {\n korean: koreanText,\n english: englishText,\n }\n );\n}\n\n/**\n * Get effect display text\n */\nexport function getEffectDisplayText(\n effect: StatusEffect,\n preferKorean: boolean = true\n): string {\n return preferKorean ? effect.description.korean : effect.description.english;\n}\n\n// Fix: Korean martial arts specific effect utilities\nexport function createKoreanStatusEffect(\n koreanName: string,\n englishName: string,\n intensity: EffectIntensity = \"medium\" as EffectIntensity,\n duration: number = 3000\n): StatusEffect {\n return createStatusEffect(\n koreanName.toLowerCase().replace(/\\s+/g, \"_\"),\n \"stun\" as EffectType, // This is correct - keep as is\n intensity,\n duration,\n { korean: koreanName, english: englishName }\n );\n}\n\nexport function createTrigramEffect(\n stanceName: string,\n effectType: EffectType,\n duration: number = 2000\n): StatusEffect {\n return createStatusEffect(\n `trigram_${stanceName}_${effectType}`,\n effectType, // This is correct - use the parameter directly\n \"medium\" as EffectIntensity,\n duration,\n {\n korean: `${stanceName}괘 효과`,\n english: `${stanceName} trigram effect`,\n }\n );\n}\n\n// Fix: Group effects by type using proper typing\nexport function groupEffectsByTypeEnum(\n effects: StatusEffect[]\n): Map<EffectType, StatusEffect[]> {\n const effectsByType = new Map<EffectType, StatusEffect[]>();\n\n for (const effect of effects) {\n // Cast string to EffectType for enum operations\n const effectType = effect.type as EffectType;\n if (!effectsByType.has(effectType)) {\n effectsByType.set(effectType, []);\n }\n const typeEffects = effectsByType.get(effectType);\n if (typeEffects) {\n typeEffects.push(effect);\n }\n }\n\n return effectsByType;\n}\n\n/**\n * Group effects by type using string keys (works with StatusEffect.type)\n */\nexport function groupEffectsByType(\n effects: StatusEffect[]\n): Map<string, StatusEffect[]> {\n const effectsByType = new Map<string, StatusEffect[]>();\n\n for (const effect of effects) {\n // Fix: Use string type directly since StatusEffect.type is string\n if (!effectsByType.has(effect.type)) {\n effectsByType.set(effect.type, []);\n }\n const typeEffects = effectsByType.get(effect.type);\n if (typeEffects) {\n typeEffects.push(effect);\n }\n }\n\n return effectsByType;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"effectUtils.js","names":[],"sources":["../../src/utils/effectUtils.ts"],"sourcesContent":["import { HitEffect, StatusEffect } from \"../systems\";\nimport { EffectIntensity, EffectType, HitEffectType } from \"../systems/effects\";\nimport type { KoreanText, Position } from \"../types\";\n\n/**\n * Create a hit effect for visual feedback\n */\nexport function createHitEffect(\n id: string,\n type: HitEffectType,\n position: Position,\n intensity: number,\n duration: number = 1000\n): HitEffect {\n return {\n id,\n type,\n attackerId: \"unknown\",\n defenderId: \"unknown\",\n timestamp: Date.now(),\n duration,\n position,\n intensity,\n lifespan: duration,\n alpha: 1.0,\n size: 1.0,\n startTime: Date.now(),\n };\n}\n\n/**\n * Get hit effect color based on type\n */\nexport function getHitEffectColor(type: HitEffectType): number {\n switch (type) {\n case HitEffectType.CRITICAL_HIT:\n return 0xff6b00; // Orange\n case HitEffectType.VITAL_POINT_STRIKE:\n return 0xff0000; // Red\n case HitEffectType.BLOCK:\n return 0x4a90e2; // Blue\n case HitEffectType.MISS:\n return 0x888888; // Gray\n default:\n return 0xffffff; // White\n }\n}\n\n/**\n * Create a status effect\n */\nexport function createStatusEffect(\n id: string,\n type: EffectType,\n intensity: EffectIntensity,\n duration: number,\n description: KoreanText,\n source: string = \"unknown\"\n): StatusEffect {\n const currentTime = Date.now();\n return {\n id,\n type,\n intensity,\n duration,\n description,\n stackable: false,\n source,\n startTime: currentTime,\n endTime: currentTime + duration,\n };\n}\n\n/**\n * Calculate effect intensity based on damage\n */\nexport function calculateEffectIntensity(damage: number): EffectIntensity {\n // Fix: Use proper EffectIntensity values that match the type definition\n if (damage >= 80) return \"severe\" as EffectIntensity; // was \"extreme\"\n if (damage >= 50) return \"severe\" as EffectIntensity; // was \"critical\"\n if (damage >= 30) return \"moderate\" as EffectIntensity; // was \"high\"\n if (damage >= 15) return \"medium\" as EffectIntensity; // this one is correct\n if (damage >= 5) return \"minor\" as EffectIntensity; // was \"low\"\n return \"weak\" as EffectIntensity; // this one is correct\n}\n\n/**\n * Get effect duration modifier based on type\n */\nexport function getEffectDurationModifier(type: EffectType): number {\n switch (type) {\n case \"bleed\": // Fix: Use string literal instead of enum\n return 1.5;\n case \"poison\":\n return 2.0;\n case \"stun\":\n return 0.5;\n case \"burn\":\n return 1.2;\n default:\n return 1.0;\n }\n}\n\n/**\n * Apply effect to player stats\n */\nexport function applyEffectModifiers(\n baseValue: number,\n effects: readonly StatusEffect[],\n statType: \"attack\" | \"defense\" | \"speed\"\n): number {\n let modifier = 1.0;\n\n effects.forEach((effect) => {\n switch (effect.type) {\n case \"bleed\":\n if (statType === \"attack\") modifier *= 0.9;\n break;\n case \"strengthened\":\n if (statType === \"attack\") modifier *= 1.2;\n break;\n case \"weakened\":\n modifier *= 0.8;\n break;\n case \"exhausted\":\n if (statType === \"speed\") modifier *= 0.7;\n break;\n }\n });\n\n return Math.floor(baseValue * modifier);\n}\n\n/**\n * Check if effect is beneficial or harmful\n */\nexport function isEffectBeneficial(type: EffectType): boolean {\n const beneficialEffects: EffectType[] = [\n \"focused\",\n \"rage\",\n \"defensive\",\n \"strengthened\",\n ];\n return beneficialEffects.includes(type);\n}\n\n/**\n * Get effect description based on type and intensity\n */\nexport function getEffectDescription(\n type: EffectType,\n intensity: EffectIntensity\n): KoreanText {\n const descriptions: Record<\n EffectType,\n Record<EffectIntensity, KoreanText>\n > = {\n stun: {\n weak: { korean: \"가벼운 기절\", english: \"Light Stun\" },\n minor: { korean: \"경미한 기절\", english: \"Minor Stun\" },\n medium: { korean: \"기절\", english: \"Stun\" },\n moderate: { korean: \"기절\", english: \"Stun\" },\n high: { korean: \"심한 기절\", english: \"Heavy Stun\" },\n severe: { korean: \"심한 기절\", english: \"Severe Stun\" },\n critical: { korean: \"완전 기절\", english: \"Complete Stun\" },\n extreme: { korean: \"완전 기절\", english: \"Complete Stun\" },\n low: { korean: \"약한 기절\", english: \"Weak Stun\" },\n },\n poison: {\n weak: { korean: \"경미한 중독\", english: \"Minor Poison\" },\n minor: { korean: \"경미한 중독\", english: \"Minor Poison\" },\n medium: { korean: \"중독\", english: \"Poison\" },\n moderate: { korean: \"중독\", english: \"Poison\" },\n high: { korean: \"심한 중독\", english: \"Severe Poison\" },\n severe: { korean: \"심한 중독\", english: \"Severe Poison\" },\n critical: { korean: \"치명적 중독\", english: \"Lethal Poison\" },\n extreme: { korean: \"치명적 중독\", english: \"Lethal Poison\" },\n low: { korean: \"약한 중독\", english: \"Weak Poison\" },\n },\n weakened: {\n weak: { korean: \"약간 약화\", english: \"Slightly Weakened\" },\n minor: { korean: \"경미한 약화\", english: \"Minor Weakness\" },\n medium: { korean: \"약화\", english: \"Weakened\" },\n moderate: { korean: \"약화\", english: \"Weakened\" },\n high: { korean: \"심하게 약화\", english: \"Severely Weakened\" },\n severe: { korean: \"심하게 약화\", english: \"Severely Weakened\" },\n critical: { korean: \"극도로 약화\", english: \"Critically Weakened\" },\n extreme: { korean: \"극도로 약화\", english: \"Critically Weakened\" },\n low: { korean: \"약간 약화\", english: \"Slightly Weakened\" },\n },\n burn: {\n weak: { korean: \"가벼운 화상\", english: \"Minor Burn\" },\n minor: { korean: \"경미한 화상\", english: \"Minor Burn\" },\n medium: { korean: \"화상\", english: \"Burn\" },\n moderate: { korean: \"화상\", english: \"Burn\" },\n high: { korean: \"심한 화상\", english: \"Severe Burn\" },\n severe: { korean: \"심한 화상\", english: \"Severe Burn\" },\n critical: { korean: \"치명적 화상\", english: \"Critical Burn\" },\n extreme: { korean: \"치명적 화상\", english: \"Critical Burn\" },\n low: { korean: \"약한 화상\", english: \"Weak Burn\" },\n },\n bleed: {\n weak: { korean: \"가벼운 출혈\", english: \"Minor Bleeding\" },\n minor: { korean: \"경미한 출혈\", english: \"Minor Bleeding\" },\n medium: { korean: \"출혈\", english: \"Bleeding\" },\n moderate: { korean: \"출혈\", english: \"Bleeding\" },\n high: { korean: \"심한 출혈\", english: \"Heavy Bleeding\" },\n severe: { korean: \"심한 출혈\", english: \"Severe Bleeding\" },\n critical: { korean: \"치명적 출혈\", english: \"Critical Bleeding\" },\n extreme: { korean: \"치명적 출혈\", english: \"Critical Bleeding\" },\n low: { korean: \"약한 출혈\", english: \"Weak Bleeding\" },\n },\n exhausted: {\n weak: { korean: \"약간 지침\", english: \"Slightly Tired\" },\n minor: { korean: \"경미한 피로\", english: \"Minor Fatigue\" },\n medium: { korean: \"지침\", english: \"Exhausted\" },\n moderate: { korean: \"지침\", english: \"Exhausted\" },\n high: { korean: \"심하게 지침\", english: \"Severely Exhausted\" },\n severe: { korean: \"심하게 지침\", english: \"Severely Exhausted\" },\n critical: { korean: \"완전 탈진\", english: \"Completely Drained\" },\n extreme: { korean: \"완전 탈진\", english: \"Completely Drained\" },\n low: { korean: \"약간 피로\", english: \"Slightly Fatigued\" },\n },\n focused: {\n weak: { korean: \"약간 집중\", english: \"Slightly Focused\" },\n minor: { korean: \"경미한 집중\", english: \"Minor Focus\" },\n medium: { korean: \"집중\", english: \"Focused\" },\n moderate: { korean: \"집중\", english: \"Focused\" },\n high: { korean: \"깊은 집중\", english: \"Deeply Focused\" },\n severe: { korean: \"깊은 집중\", english: \"Deeply Focused\" },\n critical: { korean: \"완전 집중\", english: \"Completely Focused\" },\n extreme: { korean: \"완전 집중\", english: \"Completely Focused\" },\n low: { korean: \"약한 집중\", english: \"Weak Focus\" },\n },\n rage: {\n weak: { korean: \"약간 분노\", english: \"Slightly Enraged\" },\n minor: { korean: \"경미한 분노\", english: \"Minor Rage\" },\n medium: { korean: \"분노\", english: \"Enraged\" },\n moderate: { korean: \"분노\", english: \"Enraged\" },\n high: { korean: \"맹렬한 분노\", english: \"Furious\" },\n severe: { korean: \"맹렬한 분노\", english: \"Furious\" },\n critical: { korean: \"광분\", english: \"Berserk\" },\n extreme: { korean: \"광분\", english: \"Berserk\" },\n low: { korean: \"약한 분노\", english: \"Weak Rage\" },\n },\n defensive: {\n weak: { korean: \"약간 방어적\", english: \"Slightly Defensive\" },\n minor: { korean: \"경미한 방어\", english: \"Minor Defense\" },\n medium: { korean: \"방어적\", english: \"Defensive\" },\n moderate: { korean: \"방어적\", english: \"Defensive\" },\n high: { korean: \"높은 방어\", english: \"Highly Defensive\" },\n severe: { korean: \"높은 방어\", english: \"Highly Defensive\" },\n critical: { korean: \"완벽한 방어\", english: \"Perfect Defense\" },\n extreme: { korean: \"완벽한 방어\", english: \"Perfect Defense\" },\n low: { korean: \"약한 방어\", english: \"Weak Defense\" },\n },\n strengthened: {\n weak: { korean: \"약간 강화\", english: \"Slightly Strengthened\" },\n minor: { korean: \"경미한 강화\", english: \"Minor Strength\" },\n medium: { korean: \"강화\", english: \"Strengthened\" },\n moderate: { korean: \"강화\", english: \"Strengthened\" },\n high: { korean: \"크게 강화\", english: \"Greatly Strengthened\" },\n severe: { korean: \"크게 강화\", english: \"Greatly Strengthened\" },\n critical: { korean: \"극대 강화\", english: \"Maximally Strengthened\" },\n extreme: { korean: \"극대 강화\", english: \"Maximally Strengthened\" },\n low: { korean: \"약한 강화\", english: \"Weak Strength\" },\n },\n paralysis: {\n weak: { korean: \"가벼운 마비\", english: \"Minor Paralysis\" },\n minor: { korean: \"경미한 마비\", english: \"Minor Paralysis\" },\n medium: { korean: \"마비\", english: \"Paralysis\" },\n moderate: { korean: \"마비\", english: \"Paralysis\" },\n high: { korean: \"심한 마비\", english: \"Severe Paralysis\" },\n severe: { korean: \"심한 마비\", english: \"Severe Paralysis\" },\n critical: { korean: \"완전 마비\", english: \"Complete Paralysis\" },\n extreme: { korean: \"완전 마비\", english: \"Complete Paralysis\" },\n low: { korean: \"약한 마비\", english: \"Weak Paralysis\" },\n },\n confusion: {\n weak: { korean: \"약간 혼란\", english: \"Slight Confusion\" },\n minor: { korean: \"경미한 혼란\", english: \"Minor Confusion\" },\n medium: { korean: \"혼란\", english: \"Confusion\" },\n moderate: { korean: \"혼란\", english: \"Confusion\" },\n high: { korean: \"심한 혼란\", english: \"Severe Confusion\" },\n severe: { korean: \"심한 혼란\", english: \"Severe Confusion\" },\n critical: { korean: \"완전 혼란\", english: \"Complete Confusion\" },\n extreme: { korean: \"완전 혼란\", english: \"Complete Confusion\" },\n low: { korean: \"약한 혼란\", english: \"Weak Confusion\" },\n },\n vulnerability: {\n weak: { korean: \"약간 취약\", english: \"Slightly Vulnerable\" },\n minor: { korean: \"경미한 취약\", english: \"Minor Vulnerability\" },\n medium: { korean: \"취약\", english: \"Vulnerable\" },\n moderate: { korean: \"취약\", english: \"Vulnerable\" },\n high: { korean: \"매우 취약\", english: \"Very Vulnerable\" },\n severe: { korean: \"매우 취약\", english: \"Very Vulnerable\" },\n critical: { korean: \"극도로 취약\", english: \"Extremely Vulnerable\" },\n extreme: { korean: \"극도로 취약\", english: \"Extremely Vulnerable\" },\n low: { korean: \"약한 취약\", english: \"Weak Vulnerability\" },\n },\n stamina_drain: {\n weak: { korean: \"체력 소모\", english: \"Stamina Drain\" },\n minor: { korean: \"경미한 체력 소모\", english: \"Minor Stamina Drain\" },\n medium: { korean: \"체력 고갈\", english: \"Stamina Depletion\" },\n moderate: { korean: \"체력 고갈\", english: \"Stamina Depletion\" },\n high: { korean: \"심한 체력 고갈\", english: \"Severe Stamina Loss\" },\n severe: { korean: \"심한 체력 고갈\", english: \"Severe Stamina Loss\" },\n critical: { korean: \"완전 체력 고갈\", english: \"Complete Stamina Loss\" },\n extreme: { korean: \"완전 체력 고갈\", english: \"Complete Stamina Loss\" },\n low: { korean: \"약한 체력 소모\", english: \"Weak Stamina Drain\" },\n },\n };\n\n return (\n descriptions[type]?.[intensity] || {\n korean: `${type} 효과`,\n english: `${type} effect`,\n }\n );\n}\n\n/**\n * Update effect over time\n */\nexport function updateEffect(\n effect: StatusEffect,\n currentTime: number\n): StatusEffect | null {\n if (currentTime >= effect.endTime) {\n return null; // Effect has expired\n }\n\n return effect; // Effect is still active\n}\n\n/**\n * Combine multiple effects of same type\n */\nexport function combineEffects(effects: StatusEffect[]): StatusEffect[] {\n const combinedEffects: StatusEffect[] = [];\n // Fix: Use string type instead of EffectType since StatusEffect.type is string\n const effectsByType = new Map<string, StatusEffect[]>();\n\n // Group effects by type\n effects.forEach((effect) => {\n // Fix: effect.type is string, so use string methods\n if (!effectsByType.has(effect.type)) {\n effectsByType.set(effect.type, []);\n }\n const typeEffects = effectsByType.get(effect.type);\n if (typeEffects) {\n typeEffects.push(effect);\n }\n });\n\n // Combine stackable effects, keep latest non-stackable\n effectsByType.forEach((typeEffects, _) => {\n if (typeEffects.length === 0) return;\n\n const firstEffect = typeEffects[0];\n if (firstEffect.stackable) {\n // Keep all stackable effects\n combinedEffects.push(...typeEffects);\n } else {\n // Keep only the latest non-stackable effect\n const latestEffect = typeEffects.reduce((latest, current) =>\n current.startTime > latest.startTime ? current : latest\n );\n combinedEffects.push(latestEffect);\n }\n });\n\n return combinedEffects;\n}\n\n/**\n * Remove expired effects\n */\nexport function removeExpiredEffects(effects: StatusEffect[]): StatusEffect[] {\n const currentTime = Date.now();\n return effects.filter((effect) => effect.endTime > currentTime);\n}\n\n/**\n * Create effect from Korean text\n */\nexport function createEffectFromKoreanText(\n koreanText: string,\n englishText: string,\n duration: number = 3000\n): StatusEffect {\n // Fix: Provide all required arguments\n return createStatusEffect(\n koreanText.toLowerCase(),\n \"stun\" as EffectType, // Use proper EffectType\n \"medium\" as EffectIntensity,\n duration,\n {\n korean: koreanText,\n english: englishText,\n }\n );\n}\n\n/**\n * Get effect display text\n */\nexport function getEffectDisplayText(\n effect: StatusEffect,\n preferKorean: boolean = true\n): string {\n return preferKorean ? effect.description.korean : effect.description.english;\n}\n\n// Fix: Korean martial arts specific effect utilities\nexport function createKoreanStatusEffect(\n koreanName: string,\n englishName: string,\n intensity: EffectIntensity = \"medium\" as EffectIntensity,\n duration: number = 3000\n): StatusEffect {\n return createStatusEffect(\n koreanName.toLowerCase().replace(/\\s+/g, \"_\"),\n \"stun\" as EffectType, // This is correct - keep as is\n intensity,\n duration,\n { korean: koreanName, english: englishName }\n );\n}\n\nexport function createTrigramEffect(\n stanceName: string,\n effectType: EffectType,\n duration: number = 2000\n): StatusEffect {\n return createStatusEffect(\n `trigram_${stanceName}_${effectType}`,\n effectType, // This is correct - use the parameter directly\n \"medium\" as EffectIntensity,\n duration,\n {\n korean: `${stanceName}괘 효과`,\n english: `${stanceName} trigram effect`,\n }\n );\n}\n\n// Fix: Group effects by type using proper typing\nexport function groupEffectsByTypeEnum(\n effects: StatusEffect[]\n): Map<EffectType, StatusEffect[]> {\n const effectsByType = new Map<EffectType, StatusEffect[]>();\n\n for (const effect of effects) {\n // Cast string to EffectType for enum operations\n const effectType = effect.type as EffectType;\n if (!effectsByType.has(effectType)) {\n effectsByType.set(effectType, []);\n }\n const typeEffects = effectsByType.get(effectType);\n if (typeEffects) {\n typeEffects.push(effect);\n }\n }\n\n return effectsByType;\n}\n\n/**\n * Group effects by type using string keys (works with StatusEffect.type)\n */\nexport function groupEffectsByType(\n effects: StatusEffect[]\n): Map<string, StatusEffect[]> {\n const effectsByType = new Map<string, StatusEffect[]>();\n\n for (const effect of effects) {\n // Fix: Use string type directly since StatusEffect.type is string\n if (!effectsByType.has(effect.type)) {\n effectsByType.set(effect.type, []);\n }\n const typeEffects = effectsByType.get(effect.type);\n if (typeEffects) {\n typeEffects.push(effect);\n }\n }\n\n return effectsByType;\n}\n"],"mappings":"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"}
@@ -155,7 +155,6 @@ function usePlayerMovement(config) {
155
155
  right: true
156
156
  }));
157
157
  event.preventDefault();
158
- break;
159
158
  }
160
159
  }, [enabled]);
161
160
  const handleKeyUp = useCallback((event) => {
@@ -194,7 +193,6 @@ function usePlayerMovement(config) {
194
193
  ...prev,
195
194
  right: false
196
195
  }));
197
- break;
198
196
  }
199
197
  }, [enabled]);
200
198
  const updatePositionRef = useRef(null);
@@ -1 +1 @@
1
- {"version":3,"file":"inputSystem.js","names":[],"sources":["../../src/utils/inputSystem.ts"],"sourcesContent":["import { COMBAT_CONTROLS } from \"@/systems/types\";\nimport type { Position } from \"@/types/common\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport type { MovementInput } from \"../systems/physics/MovementPhysics\";\nimport { MovementPhysics } from \"../systems/physics/MovementPhysics\";\nimport { TrigramStance } from \"../types/common\";\nimport { calculateArenaBounds, DEFAULT_PHYSICS_ARENA_BOUNDS } from \"../types/PhysicsTypes\";\nimport type { MovementArenaBounds } from \"../types/PhysicsTypes\";\n\n/**\n * Configuration interface for the input system and player movement.\n * Uses physics-first approach: all positions and velocities are in meters.\n *\n * **Korean**: 입력 시스템 설정 (Input System Configuration)\n *\n * ## Physics-First Architecture\n *\n * This interface requires worldWidthMeters and worldDepthMeters to enable\n * the new physics-first coordinate system. Without these properties, the\n * movement system cannot properly convert between physics (meters) and\n * rendering (pixels).\n *\n * ### Migration Guide\n *\n * Existing code must be updated to pass world dimensions:\n *\n * ```typescript\n * // Before (incorrect):\n * const config = { bounds: { x: 0, y: 0, width: 960, height: 480 } };\n *\n * // After (correct):\n * const config = {\n * bounds: {\n * worldWidthMeters: 10, // From layout hook\n * worldDepthMeters: 10 // From layout hook\n * }\n * };\n * ```\n *\n * ### Fallback Behavior\n *\n * If worldWidthMeters/worldDepthMeters are not provided, the system falls back\n * to DEFAULT_PHYSICS_ARENA_BOUNDS (10m × 7.5m) to ensure movement stays bounded.\n * Callers SHOULD provide these values from their layout hooks (useCombatLayout, \n * useTrainingLayout) for proper arena sizing.\n */\nexport interface InputSystemConfig {\n /** Whether the input system is enabled and processing input */\n readonly enabled?: boolean;\n\n /**\n * Arena world dimensions in meters for physics calculations.\n *\n * **REQUIRED for physics-first coordinate system to work.**\n *\n * These values must come from layout hooks:\n * - CombatScreen3D: Use arenaBounds.worldWidthMeters/worldDepthMeters from useCombatLayout()\n * - TrainingScreen3D: Use trainingAreaBounds.worldWidthMeters/worldDepthMeters from useTrainingLayout()\n */\n readonly bounds?: {\n /** Physical arena width in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldWidthMeters: number;\n /** Physical arena depth in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldDepthMeters: number;\n };\n\n /** Callback invoked when player position changes (position in meters) */\n readonly onPositionChange?: (position: Position) => void;\n\n /** Initial player position in METERS (x = lateral, y = forward/backward) */\n readonly initialPositionMeters?: Position;\n\n // Physics-based movement parameters (always enabled)\n /** Current trigram stance affecting movement speed */\n readonly currentStance?: TrigramStance;\n\n /** Leg injury factor (0-1, where 1 is fully injured) affecting movement speed */\n readonly legInjuryFactor?: number;\n\n /** Whether player is running (sprint mode) */\n readonly isRunning?: boolean;\n\n /** Whether to use tactical step mode (30cm grid quantization) */\n readonly useTacticalSteps?: boolean;\n\n // Speed modifier overrides from SpeedModifierSystem\n /** Final calculated maximum speed in meters per second */\n readonly maxSpeedOverride?: number;\n\n /** Final calculated acceleration in meters per second squared */\n readonly accelerationOverride?: number;\n}\n\nexport interface MovementState {\n readonly up: boolean;\n readonly down: boolean;\n readonly left: boolean;\n readonly right: boolean;\n readonly position: Position;\n readonly isMoving: boolean; // Add isMoving to movement state\n}\n\nexport interface PlayerMovementResult {\n /** Player position in METERS (x = lateral, y = forward/backward in arena) */\n readonly playerPosition: Position;\n readonly movementState: MovementState;\n readonly isMoving: boolean;\n readonly isKeyPressed: (key: string) => boolean;\n /** Velocity in m/s (x = lateral, y = forward/backward) */\n readonly velocity?: { x: number; y: number };\n /** Current speed magnitude in m/s */\n readonly speed?: number;\n}\n\n/**\n * Hook for handling player movement with physics-first approach.\n * All positions and velocities are in METERS - no pixel conversions.\n *\n * **Korean**: 플레이어 이동 훅 (Player Movement Hook)\n *\n * @param config - Physics-first configuration with positions in meters\n * @returns Movement state and physics data (all in meters)\n */\nexport function usePlayerMovement(\n config: InputSystemConfig,\n): PlayerMovementResult {\n const {\n enabled = true,\n bounds,\n onPositionChange,\n initialPositionMeters = { x: 0, y: 0 },\n currentStance = TrigramStance.GEON,\n legInjuryFactor = 0,\n isRunning: isRunningProp = false,\n useTacticalSteps = false,\n maxSpeedOverride,\n accelerationOverride,\n } = config;\n\n // Position in METERS (x = lateral position, y = forward/backward position)\n const [playerPosition, setPlayerPosition] = useState<Position>(\n initialPositionMeters,\n );\n const [keyState, setKeyState] = useState({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n // Physics state for render (velocity and speed in m/s)\n const [velocity, setVelocity] = useState<\n { x: number; y: number } | undefined\n >(undefined);\n const [speed, setSpeed] = useState<number | undefined>(undefined);\n\n // Auto-run detection: track how long movement keys have been held\n // After sustained movement, automatically transition from walking to running\n const movementStartTimeRef = useRef<number | null>(null);\n const AUTO_RUN_THRESHOLD_MS = 300; // Transition to run after 300ms of sustained movement\n\n // Physics-based movement state (always initialized for realistic combat)\n const physicsEngineRef = useRef<MovementPhysics | null>(null);\n const physicsStateRef = useRef<{\n position: THREE.Vector3;\n velocity: THREE.Vector3;\n acceleration: number;\n maxSpeed: number;\n currentStance: TrigramStance;\n legInjuryFactor: number;\n } | null>(null);\n\n // Initialize physics engine once on mount (always enabled)\n // All positions are in METERS - no pixel conversion needed\n useEffect(() => {\n if (!physicsEngineRef.current) {\n // Use arena width for physics-aware speed scaling\n // Validate and fall back to default if invalid\n const width = bounds?.worldWidthMeters;\n const arenaWidth =\n width != null && Number.isFinite(width) && width > 0\n ? width\n : DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters;\n physicsEngineRef.current = new MovementPhysics(arenaWidth);\n // Initial position in meters (x = lateral, z = forward/backward)\n physicsStateRef.current = {\n position: new THREE.Vector3(\n initialPositionMeters.x,\n 0,\n initialPositionMeters.y,\n ),\n velocity: new THREE.Vector3(0, 0, 0),\n acceleration: 0,\n maxSpeed: 6.0, // Default to BASE_WALK_SPEED (6.0 m/s for responsive combat)\n currentStance,\n legInjuryFactor: legInjuryFactor ?? 0,\n };\n }\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Compute arena bounds synchronously when bounds dimensions change\n // Uses useMemo to ensure bounds are available immediately (not after effect runs)\n // Falls back to default arena bounds if invalid or missing\n // Depend on the whole `bounds` object so the compiler's inferred property-access\n // dependencies (bounds.worldWidthMeters / bounds.worldDepthMeters) are covered.\n const arenaBoundsResult = useMemo<{\n bounds: MovementArenaBounds | undefined;\n error?: Error;\n }>(() => {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: bounds.worldWidthMeters,\n worldDepthMeters: bounds.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // If validation fails, fall back to default bounds\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }\n\n // Fallback: use default arena bounds to ensure movement stays bounded\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters,\n worldDepthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // Should never happen with default bounds, but handle gracefully\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }, [bounds]);\n\n const arenaBounds = arenaBoundsResult.bounds;\n\n // Log arena bounds calculation errors in an effect (not during render)\n useEffect(() => {\n if (arenaBoundsResult.error) {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n // Custom bounds failed validation\n console.warn(\n \"Failed to calculate arena bounds, using defaults:\",\n arenaBoundsResult.error\n );\n } else {\n // Should never happen with default bounds\n console.error(\n \"Failed to calculate default arena bounds:\",\n arenaBoundsResult.error\n );\n }\n }\n }, [arenaBoundsResult.error, bounds?.worldWidthMeters, bounds?.worldDepthMeters]);\n\n // Update physics engine arena width when bounds change (legacy)\n useEffect(() => {\n if (!physicsEngineRef.current) {\n return;\n }\n\n const width = bounds?.worldWidthMeters;\n if (width == null) {\n return;\n }\n\n // Validate width before applying to physics engine to avoid runtime errors\n if (!Number.isFinite(width) || width <= 0) {\n console.warn(\n \"Ignoring invalid worldWidthMeters when updating arena width:\",\n width,\n );\n return;\n }\n\n try {\n physicsEngineRef.current.setArenaWidth(width);\n } catch (error) {\n console.warn(\"Failed to update physics arena width:\", error);\n }\n }, [bounds?.worldWidthMeters]);\n\n // Track pressed keys for combat system\n const pressedKeys = useRef<Set<string>>(new Set());\n // Use useState lazy initializer for performance.now() to avoid impure function during render\n const [initialTime] = useState(() => performance.now());\n const lastUpdateTime = useRef(initialTime);\n const animationFrameId = useRef<number | null>(null);\n\n // Refs to track last reported position/velocity to avoid useCallback dependency issues\n // This prevents the animation frame from being cancelled every frame due to callback recreation\n const lastReportedPositionRef = useRef<Position>(initialPositionMeters);\n const lastReportedVelocityRef = useRef<{ x: number; y: number } | undefined>(\n undefined,\n );\n const lastReportedSpeedRef = useRef<number | undefined>(undefined);\n\n // Ref to track keyState for physics loop - avoids recreating callback on key changes\n const keyStateRef = useRef({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n\n // Calculate if currently moving\n const isMoving =\n keyState.up || keyState.down || keyState.left || keyState.right;\n\n // Create complete movement state\n const movementState: MovementState = {\n ...keyState,\n position: playerPosition,\n isMoving,\n };\n\n // Key press checker for combat system\n const isKeyPressed = useCallback((key: string): boolean => {\n return pressedKeys.current.has(key);\n }, []);\n\n // Enhanced keyboard event handlers\n const handleKeyDown = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.add(key);\n\n // ✅ FIXED: Add all movement keys including WASD and arrows\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = true;\n setKeyState((prev) => ({ ...prev, up: true }));\n event.preventDefault();\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = true;\n setKeyState((prev) => ({ ...prev, down: true }));\n event.preventDefault();\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = true;\n setKeyState((prev) => ({ ...prev, left: true }));\n event.preventDefault();\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = true;\n setKeyState((prev) => ({ ...prev, right: true }));\n event.preventDefault();\n break;\n }\n },\n [enabled],\n );\n\n const handleKeyUp = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.delete(key);\n\n // ✅ FIXED: Handle key release for all movement keys\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = false;\n setKeyState((prev) => ({ ...prev, up: false }));\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = false;\n setKeyState((prev) => ({ ...prev, down: false }));\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = false;\n setKeyState((prev) => ({ ...prev, left: false }));\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = false;\n setKeyState((prev) => ({ ...prev, right: false }));\n break;\n }\n },\n [enabled],\n );\n\n // ✅ FIXED: Proper movement calculation with correct bounds\n // Use a ref to store the callback to avoid reference before declaration issue\n const updatePositionRef = useRef<(() => void) | null>(null);\n\n const updatePosition = useCallback(() => {\n // Check if any movement keys are pressed using ref (not stale state)\n const keys = keyStateRef.current;\n const isCurrentlyMoving = keys.up || keys.down || keys.left || keys.right;\n\n if (!enabled || !isCurrentlyMoving) {\n animationFrameId.current = null;\n return;\n }\n\n const now = performance.now();\n const deltaTime = Math.min(now - (lastUpdateTime.current ?? now), 50);\n lastUpdateTime.current = now;\n\n if (deltaTime <= 0) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n return;\n }\n\n // Physics-based movement (always enabled for realistic combat)\n if (physicsEngineRef.current && physicsStateRef.current) {\n // Apply speed modifiers if provided by SpeedModifierSystem\n // BUG FIX: Now properly passing maxSpeedOverride to physics engine\n if (maxSpeedOverride !== undefined) {\n physicsEngineRef.current.setMaxSpeed(maxSpeedOverride);\n }\n\n if (accelerationOverride !== undefined) {\n physicsEngineRef.current.setAcceleration(accelerationOverride);\n }\n\n // Convert key state to physics input (using ref to avoid callback recreation)\n // Screen coordinates: UP/W = toward top of screen, DOWN/S = toward bottom\n // Physics Z-axis: negative Z = toward top, positive Z = toward bottom\n const keys = keyStateRef.current;\n const forward = keys.up ? -1 : keys.down ? 1 : 0;\n const lateral = keys.right ? 1 : keys.left ? -1 : 0;\n const isCurrentlyMoving = forward !== 0 || lateral !== 0;\n\n // Auto-run detection: transition to running after sustained movement\n const now = performance.now();\n if (isCurrentlyMoving) {\n movementStartTimeRef.current ??= now;\n } else {\n movementStartTimeRef.current = null;\n }\n\n // Determine if player should be running (auto-run after threshold)\n const movementDuration = movementStartTimeRef.current\n ? now - movementStartTimeRef.current\n : 0;\n const shouldRun =\n isRunningProp || movementDuration > AUTO_RUN_THRESHOLD_MS;\n\n const physicsInput: MovementInput = {\n forward,\n lateral,\n isRunning: shouldRun,\n isMoving: isCurrentlyMoving,\n useTacticalSteps,\n };\n\n // Update physics state\n const state = physicsStateRef.current;\n state.currentStance = currentStance;\n state.legInjuryFactor = legInjuryFactor;\n\n // Clamp delta time to 1/30s (≈33.33ms) to match usePlayerMovement and prevent instability\n const clampedDeltaTimeMs = Math.min(deltaTime, 1000 / 30);\n\n // Use arena bounds computed via useMemo (available synchronously)\n physicsEngineRef.current.updateMovement(\n state,\n physicsInput,\n clampedDeltaTimeMs / 1000,\n arenaBounds, // Use memoized bounds\n );\n\n // Position in meters (x = lateral, y = forward/backward)\n const newPosition = { x: state.position.x, y: state.position.z };\n\n // Velocity in m/s (x = lateral, y = forward/backward)\n const newVelocity = { x: state.velocity.x, y: state.velocity.z };\n const newSpeed = state.velocity.length();\n\n // Use refs for comparison to avoid recreating callback on every frame\n // This prevents the animation frame from being cancelled due to useCallback recreation\n const lastPos = lastReportedPositionRef.current;\n if (newPosition.x !== lastPos.x || newPosition.y !== lastPos.y) {\n lastReportedPositionRef.current = newPosition;\n setPlayerPosition(newPosition);\n onPositionChange?.(newPosition);\n }\n\n // Update velocity and speed if changed (with epsilon tolerance for floating-point stability)\n const EPSILON = 0.001;\n const lastVel = lastReportedVelocityRef.current;\n const velocityChanged =\n !lastVel ||\n Math.abs(lastVel.x - newVelocity.x) > EPSILON ||\n Math.abs(lastVel.y - newVelocity.y) > EPSILON;\n if (velocityChanged) {\n lastReportedVelocityRef.current = newVelocity;\n setVelocity(newVelocity);\n }\n // Initialize speed when undefined, then update only on significant changes\n const lastSpd = lastReportedSpeedRef.current;\n if (lastSpd === undefined || Math.abs(lastSpd - newSpeed) > EPSILON) {\n lastReportedSpeedRef.current = newSpeed;\n setSpeed(newSpeed);\n }\n }\n\n // Continue animation if still moving (check ref, not stale closure)\n const stillMoving =\n keyStateRef.current.up ||\n keyStateRef.current.down ||\n keyStateRef.current.left ||\n keyStateRef.current.right;\n if (stillMoving) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n } else {\n animationFrameId.current = null;\n }\n // NOTE: playerPosition, velocity, speed, keyState, isMoving intentionally excluded from deps\n // Using refs (lastReportedPositionRef, lastReportedVelocityRef, lastReportedSpeedRef, keyStateRef)\n // for comparison to prevent animation frame cancellation on every state update.\n // arenaBounds is computed from bounds and automatically updates when bounds changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n // playerPosition - excluded, using ref\n // keyState - excluded, using keyStateRef\n // isMoving - excluded, using keyStateRef for movement check\n // arenaBounds - excluded, derived from bounds (below)\n bounds,\n onPositionChange,\n currentStance,\n legInjuryFactor,\n isRunningProp,\n useTacticalSteps,\n // velocity - excluded, using ref\n // speed - excluded, using ref\n maxSpeedOverride,\n accelerationOverride,\n ]);\n\n // Keep updatePositionRef in sync via useEffect (not during render)\n useEffect(() => {\n updatePositionRef.current = updatePosition;\n }, [updatePosition]);\n\n // Handle keyboard input\n useEffect(() => {\n if (!enabled) return;\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 if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [enabled, handleKeyDown, handleKeyUp]);\n\n // Start animation loop when movement begins\n useEffect(() => {\n if (isMoving && !animationFrameId.current) {\n lastUpdateTime.current = performance.now();\n // Use ref to avoid dependency on updatePosition callback\n animationFrameId.current = requestAnimationFrame(() => {\n updatePositionRef.current?.();\n });\n } else if (!isMoving && animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n };\n // Only depend on isMoving - updatePositionRef is stable\n }, [isMoving]);\n\n return {\n playerPosition,\n movementState,\n isMoving,\n isKeyPressed,\n velocity,\n speed,\n };\n}\n\nexport interface InputEvent {\n readonly type: \"keydown\" | \"keyup\" | \"click\" | \"touchstart\" | \"touchend\";\n readonly key?: string;\n readonly target?: EventTarget | null;\n readonly timestamp: number;\n}\n\nexport interface CombatInput {\n readonly stanceChange?: TrigramStance;\n readonly attack?: boolean;\n readonly block?: boolean;\n readonly movement?: MovementState;\n readonly timestamp: number;\n}\n\n/**\n * Input system for combat controls\n */\nexport class InputSystem {\n private actionCallbacks = new Map<string, (() => void)[]>();\n private isEnabled = true;\n\n constructor() {\n this.setupEventListeners();\n }\n\n private setupEventListeners() {\n window.addEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.addEventListener(\"keyup\", this.handleKeyUp.bind(this));\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keydown:${key}`);\n this.triggerAction(\"keydown\");\n }\n\n private handleKeyUp(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keyup:${key}`);\n this.triggerAction(\"keyup\");\n }\n\n registerAction(action: string, callback: () => void) {\n if (!this.actionCallbacks.has(action)) {\n this.actionCallbacks.set(action, []);\n }\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.push(callback);\n }\n }\n\n unregisterAction(action: string, callback?: () => void) {\n if (!this.actionCallbacks.has(action)) return;\n\n if (callback) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n const index = callbacks.indexOf(callback);\n if (index > -1) {\n callbacks.splice(index, 1);\n }\n }\n } else {\n this.actionCallbacks.delete(action);\n }\n }\n\n clearActions() {\n this.actionCallbacks.clear();\n }\n\n isActionActive(action: string): boolean {\n return this.actionCallbacks.has(action);\n }\n\n enable() {\n this.isEnabled = true;\n }\n\n disable() {\n this.isEnabled = false;\n }\n\n private triggerAction(action: string) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.forEach((callback) => callback());\n }\n }\n\n destroy() {\n window.removeEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.removeEventListener(\"keyup\", this.handleKeyUp.bind(this));\n this.clearActions();\n }\n}\n\n/**\n * Get stance from keyboard input\n */\nexport function getStanceFromKey(key: string): TrigramStance | null {\n const stanceKey = key as keyof typeof COMBAT_CONTROLS.stanceControls;\n\n if (stanceKey in COMBAT_CONTROLS.stanceControls) {\n return COMBAT_CONTROLS.stanceControls[stanceKey].stance;\n }\n\n return null;\n}\n\n/**\n * Process combat input and return structured combat data\n */\nexport function processCombatInput(event: KeyboardEvent): CombatInput | null {\n const key = event.key;\n const timestamp = performance.now();\n\n // Check for stance change (1-8 keys)\n const stance = getStanceFromKey(key);\n if (stance) {\n return {\n stanceChange: stance,\n timestamp,\n };\n }\n\n // Check for combat actions\n switch (key.toLowerCase()) {\n case \" \": // Space for attack\n return {\n attack: true,\n timestamp,\n };\n case \"shift\":\n return {\n block: true,\n timestamp,\n };\n default:\n return null;\n }\n}\n\n/**\n * Hook for combat input handling\n */\nexport function useCombatInput(onCombatInput: (input: CombatInput) => void) {\n const isEnabled = useRef<boolean>(true);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!isEnabled.current) return;\n\n const combatInput = processCombatInput(event);\n if (combatInput) {\n onCombatInput(combatInput);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onCombatInput]);\n\n return {\n enable: () => {\n isEnabled.current = true;\n },\n disable: () => {\n isEnabled.current = false;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"inputSystem.js","names":[],"sources":["../../src/utils/inputSystem.ts"],"sourcesContent":["import { COMBAT_CONTROLS } from \"@/systems/types\";\nimport type { Position } from \"@/types/common\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport type { MovementInput } from \"../systems/physics/MovementPhysics\";\nimport { MovementPhysics } from \"../systems/physics/MovementPhysics\";\nimport { TrigramStance } from \"../types/common\";\nimport { calculateArenaBounds, DEFAULT_PHYSICS_ARENA_BOUNDS } from \"../types/PhysicsTypes\";\nimport type { MovementArenaBounds } from \"../types/PhysicsTypes\";\n\n/**\n * Configuration interface for the input system and player movement.\n * Uses physics-first approach: all positions and velocities are in meters.\n *\n * **Korean**: 입력 시스템 설정 (Input System Configuration)\n *\n * ## Physics-First Architecture\n *\n * This interface requires worldWidthMeters and worldDepthMeters to enable\n * the new physics-first coordinate system. Without these properties, the\n * movement system cannot properly convert between physics (meters) and\n * rendering (pixels).\n *\n * ### Migration Guide\n *\n * Existing code must be updated to pass world dimensions:\n *\n * ```typescript\n * // Before (incorrect):\n * const config = { bounds: { x: 0, y: 0, width: 960, height: 480 } };\n *\n * // After (correct):\n * const config = {\n * bounds: {\n * worldWidthMeters: 10, // From layout hook\n * worldDepthMeters: 10 // From layout hook\n * }\n * };\n * ```\n *\n * ### Fallback Behavior\n *\n * If worldWidthMeters/worldDepthMeters are not provided, the system falls back\n * to DEFAULT_PHYSICS_ARENA_BOUNDS (10m × 7.5m) to ensure movement stays bounded.\n * Callers SHOULD provide these values from their layout hooks (useCombatLayout, \n * useTrainingLayout) for proper arena sizing.\n */\nexport interface InputSystemConfig {\n /** Whether the input system is enabled and processing input */\n readonly enabled?: boolean;\n\n /**\n * Arena world dimensions in meters for physics calculations.\n *\n * **REQUIRED for physics-first coordinate system to work.**\n *\n * These values must come from layout hooks:\n * - CombatScreen3D: Use arenaBounds.worldWidthMeters/worldDepthMeters from useCombatLayout()\n * - TrainingScreen3D: Use trainingAreaBounds.worldWidthMeters/worldDepthMeters from useTrainingLayout()\n */\n readonly bounds?: {\n /** Physical arena width in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldWidthMeters: number;\n /** Physical arena depth in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldDepthMeters: number;\n };\n\n /** Callback invoked when player position changes (position in meters) */\n readonly onPositionChange?: (position: Position) => void;\n\n /** Initial player position in METERS (x = lateral, y = forward/backward) */\n readonly initialPositionMeters?: Position;\n\n // Physics-based movement parameters (always enabled)\n /** Current trigram stance affecting movement speed */\n readonly currentStance?: TrigramStance;\n\n /** Leg injury factor (0-1, where 1 is fully injured) affecting movement speed */\n readonly legInjuryFactor?: number;\n\n /** Whether player is running (sprint mode) */\n readonly isRunning?: boolean;\n\n /** Whether to use tactical step mode (30cm grid quantization) */\n readonly useTacticalSteps?: boolean;\n\n // Speed modifier overrides from SpeedModifierSystem\n /** Final calculated maximum speed in meters per second */\n readonly maxSpeedOverride?: number;\n\n /** Final calculated acceleration in meters per second squared */\n readonly accelerationOverride?: number;\n}\n\nexport interface MovementState {\n readonly up: boolean;\n readonly down: boolean;\n readonly left: boolean;\n readonly right: boolean;\n readonly position: Position;\n readonly isMoving: boolean; // Add isMoving to movement state\n}\n\nexport interface PlayerMovementResult {\n /** Player position in METERS (x = lateral, y = forward/backward in arena) */\n readonly playerPosition: Position;\n readonly movementState: MovementState;\n readonly isMoving: boolean;\n readonly isKeyPressed: (key: string) => boolean;\n /** Velocity in m/s (x = lateral, y = forward/backward) */\n readonly velocity?: { x: number; y: number };\n /** Current speed magnitude in m/s */\n readonly speed?: number;\n}\n\n/**\n * Hook for handling player movement with physics-first approach.\n * All positions and velocities are in METERS - no pixel conversions.\n *\n * **Korean**: 플레이어 이동 훅 (Player Movement Hook)\n *\n * @param config - Physics-first configuration with positions in meters\n * @returns Movement state and physics data (all in meters)\n */\nexport function usePlayerMovement(\n config: InputSystemConfig,\n): PlayerMovementResult {\n const {\n enabled = true,\n bounds,\n onPositionChange,\n initialPositionMeters = { x: 0, y: 0 },\n currentStance = TrigramStance.GEON,\n legInjuryFactor = 0,\n isRunning: isRunningProp = false,\n useTacticalSteps = false,\n maxSpeedOverride,\n accelerationOverride,\n } = config;\n\n // Position in METERS (x = lateral position, y = forward/backward position)\n const [playerPosition, setPlayerPosition] = useState<Position>(\n initialPositionMeters,\n );\n const [keyState, setKeyState] = useState({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n // Physics state for render (velocity and speed in m/s)\n const [velocity, setVelocity] = useState<\n { x: number; y: number } | undefined\n >(undefined);\n const [speed, setSpeed] = useState<number | undefined>(undefined);\n\n // Auto-run detection: track how long movement keys have been held\n // After sustained movement, automatically transition from walking to running\n const movementStartTimeRef = useRef<number | null>(null);\n const AUTO_RUN_THRESHOLD_MS = 300; // Transition to run after 300ms of sustained movement\n\n // Physics-based movement state (always initialized for realistic combat)\n const physicsEngineRef = useRef<MovementPhysics | null>(null);\n const physicsStateRef = useRef<{\n position: THREE.Vector3;\n velocity: THREE.Vector3;\n acceleration: number;\n maxSpeed: number;\n currentStance: TrigramStance;\n legInjuryFactor: number;\n } | null>(null);\n\n // Initialize physics engine once on mount (always enabled)\n // All positions are in METERS - no pixel conversion needed\n useEffect(() => {\n if (!physicsEngineRef.current) {\n // Use arena width for physics-aware speed scaling\n // Validate and fall back to default if invalid\n const width = bounds?.worldWidthMeters;\n const arenaWidth =\n width != null && Number.isFinite(width) && width > 0\n ? width\n : DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters;\n physicsEngineRef.current = new MovementPhysics(arenaWidth);\n // Initial position in meters (x = lateral, z = forward/backward)\n physicsStateRef.current = {\n position: new THREE.Vector3(\n initialPositionMeters.x,\n 0,\n initialPositionMeters.y,\n ),\n velocity: new THREE.Vector3(0, 0, 0),\n acceleration: 0,\n maxSpeed: 6.0, // Default to BASE_WALK_SPEED (6.0 m/s for responsive combat)\n currentStance,\n legInjuryFactor: legInjuryFactor ?? 0,\n };\n }\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Compute arena bounds synchronously when bounds dimensions change\n // Uses useMemo to ensure bounds are available immediately (not after effect runs)\n // Falls back to default arena bounds if invalid or missing\n // Depend on the whole `bounds` object so the compiler's inferred property-access\n // dependencies (bounds.worldWidthMeters / bounds.worldDepthMeters) are covered.\n const arenaBoundsResult = useMemo<{\n bounds: MovementArenaBounds | undefined;\n error?: Error;\n }>(() => {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: bounds.worldWidthMeters,\n worldDepthMeters: bounds.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // If validation fails, fall back to default bounds\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }\n\n // Fallback: use default arena bounds to ensure movement stays bounded\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters,\n worldDepthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // Should never happen with default bounds, but handle gracefully\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }, [bounds]);\n\n const arenaBounds = arenaBoundsResult.bounds;\n\n // Log arena bounds calculation errors in an effect (not during render)\n useEffect(() => {\n if (arenaBoundsResult.error) {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n // Custom bounds failed validation\n console.warn(\n \"Failed to calculate arena bounds, using defaults:\",\n arenaBoundsResult.error\n );\n } else {\n // Should never happen with default bounds\n console.error(\n \"Failed to calculate default arena bounds:\",\n arenaBoundsResult.error\n );\n }\n }\n }, [arenaBoundsResult.error, bounds?.worldWidthMeters, bounds?.worldDepthMeters]);\n\n // Update physics engine arena width when bounds change (legacy)\n useEffect(() => {\n if (!physicsEngineRef.current) {\n return;\n }\n\n const width = bounds?.worldWidthMeters;\n if (width == null) {\n return;\n }\n\n // Validate width before applying to physics engine to avoid runtime errors\n if (!Number.isFinite(width) || width <= 0) {\n console.warn(\n \"Ignoring invalid worldWidthMeters when updating arena width:\",\n width,\n );\n return;\n }\n\n try {\n physicsEngineRef.current.setArenaWidth(width);\n } catch (error) {\n console.warn(\"Failed to update physics arena width:\", error);\n }\n }, [bounds?.worldWidthMeters]);\n\n // Track pressed keys for combat system\n const pressedKeys = useRef<Set<string>>(new Set());\n // Use useState lazy initializer for performance.now() to avoid impure function during render\n const [initialTime] = useState(() => performance.now());\n const lastUpdateTime = useRef(initialTime);\n const animationFrameId = useRef<number | null>(null);\n\n // Refs to track last reported position/velocity to avoid useCallback dependency issues\n // This prevents the animation frame from being cancelled every frame due to callback recreation\n const lastReportedPositionRef = useRef<Position>(initialPositionMeters);\n const lastReportedVelocityRef = useRef<{ x: number; y: number } | undefined>(\n undefined,\n );\n const lastReportedSpeedRef = useRef<number | undefined>(undefined);\n\n // Ref to track keyState for physics loop - avoids recreating callback on key changes\n const keyStateRef = useRef({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n\n // Calculate if currently moving\n const isMoving =\n keyState.up || keyState.down || keyState.left || keyState.right;\n\n // Create complete movement state\n const movementState: MovementState = {\n ...keyState,\n position: playerPosition,\n isMoving,\n };\n\n // Key press checker for combat system\n const isKeyPressed = useCallback((key: string): boolean => {\n return pressedKeys.current.has(key);\n }, []);\n\n // Enhanced keyboard event handlers\n const handleKeyDown = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.add(key);\n\n // ✅ FIXED: Add all movement keys including WASD and arrows\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = true;\n setKeyState((prev) => ({ ...prev, up: true }));\n event.preventDefault();\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = true;\n setKeyState((prev) => ({ ...prev, down: true }));\n event.preventDefault();\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = true;\n setKeyState((prev) => ({ ...prev, left: true }));\n event.preventDefault();\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = true;\n setKeyState((prev) => ({ ...prev, right: true }));\n event.preventDefault();\n break;\n }\n },\n [enabled],\n );\n\n const handleKeyUp = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.delete(key);\n\n // ✅ FIXED: Handle key release for all movement keys\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = false;\n setKeyState((prev) => ({ ...prev, up: false }));\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = false;\n setKeyState((prev) => ({ ...prev, down: false }));\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = false;\n setKeyState((prev) => ({ ...prev, left: false }));\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = false;\n setKeyState((prev) => ({ ...prev, right: false }));\n break;\n }\n },\n [enabled],\n );\n\n // ✅ FIXED: Proper movement calculation with correct bounds\n // Use a ref to store the callback to avoid reference before declaration issue\n const updatePositionRef = useRef<(() => void) | null>(null);\n\n const updatePosition = useCallback(() => {\n // Check if any movement keys are pressed using ref (not stale state)\n const keys = keyStateRef.current;\n const isCurrentlyMoving = keys.up || keys.down || keys.left || keys.right;\n\n if (!enabled || !isCurrentlyMoving) {\n animationFrameId.current = null;\n return;\n }\n\n const now = performance.now();\n const deltaTime = Math.min(now - (lastUpdateTime.current ?? now), 50);\n lastUpdateTime.current = now;\n\n if (deltaTime <= 0) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n return;\n }\n\n // Physics-based movement (always enabled for realistic combat)\n if (physicsEngineRef.current && physicsStateRef.current) {\n // Apply speed modifiers if provided by SpeedModifierSystem\n // BUG FIX: Now properly passing maxSpeedOverride to physics engine\n if (maxSpeedOverride !== undefined) {\n physicsEngineRef.current.setMaxSpeed(maxSpeedOverride);\n }\n\n if (accelerationOverride !== undefined) {\n physicsEngineRef.current.setAcceleration(accelerationOverride);\n }\n\n // Convert key state to physics input (using ref to avoid callback recreation)\n // Screen coordinates: UP/W = toward top of screen, DOWN/S = toward bottom\n // Physics Z-axis: negative Z = toward top, positive Z = toward bottom\n const keys = keyStateRef.current;\n const forward = keys.up ? -1 : keys.down ? 1 : 0;\n const lateral = keys.right ? 1 : keys.left ? -1 : 0;\n const isCurrentlyMoving = forward !== 0 || lateral !== 0;\n\n // Auto-run detection: transition to running after sustained movement\n const now = performance.now();\n if (isCurrentlyMoving) {\n movementStartTimeRef.current ??= now;\n } else {\n movementStartTimeRef.current = null;\n }\n\n // Determine if player should be running (auto-run after threshold)\n const movementDuration = movementStartTimeRef.current\n ? now - movementStartTimeRef.current\n : 0;\n const shouldRun =\n isRunningProp || movementDuration > AUTO_RUN_THRESHOLD_MS;\n\n const physicsInput: MovementInput = {\n forward,\n lateral,\n isRunning: shouldRun,\n isMoving: isCurrentlyMoving,\n useTacticalSteps,\n };\n\n // Update physics state\n const state = physicsStateRef.current;\n state.currentStance = currentStance;\n state.legInjuryFactor = legInjuryFactor;\n\n // Clamp delta time to 1/30s (≈33.33ms) to match usePlayerMovement and prevent instability\n const clampedDeltaTimeMs = Math.min(deltaTime, 1000 / 30);\n\n // Use arena bounds computed via useMemo (available synchronously)\n physicsEngineRef.current.updateMovement(\n state,\n physicsInput,\n clampedDeltaTimeMs / 1000,\n arenaBounds, // Use memoized bounds\n );\n\n // Position in meters (x = lateral, y = forward/backward)\n const newPosition = { x: state.position.x, y: state.position.z };\n\n // Velocity in m/s (x = lateral, y = forward/backward)\n const newVelocity = { x: state.velocity.x, y: state.velocity.z };\n const newSpeed = state.velocity.length();\n\n // Use refs for comparison to avoid recreating callback on every frame\n // This prevents the animation frame from being cancelled due to useCallback recreation\n const lastPos = lastReportedPositionRef.current;\n if (newPosition.x !== lastPos.x || newPosition.y !== lastPos.y) {\n lastReportedPositionRef.current = newPosition;\n setPlayerPosition(newPosition);\n onPositionChange?.(newPosition);\n }\n\n // Update velocity and speed if changed (with epsilon tolerance for floating-point stability)\n const EPSILON = 0.001;\n const lastVel = lastReportedVelocityRef.current;\n const velocityChanged =\n !lastVel ||\n Math.abs(lastVel.x - newVelocity.x) > EPSILON ||\n Math.abs(lastVel.y - newVelocity.y) > EPSILON;\n if (velocityChanged) {\n lastReportedVelocityRef.current = newVelocity;\n setVelocity(newVelocity);\n }\n // Initialize speed when undefined, then update only on significant changes\n const lastSpd = lastReportedSpeedRef.current;\n if (lastSpd === undefined || Math.abs(lastSpd - newSpeed) > EPSILON) {\n lastReportedSpeedRef.current = newSpeed;\n setSpeed(newSpeed);\n }\n }\n\n // Continue animation if still moving (check ref, not stale closure)\n const stillMoving =\n keyStateRef.current.up ||\n keyStateRef.current.down ||\n keyStateRef.current.left ||\n keyStateRef.current.right;\n if (stillMoving) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n } else {\n animationFrameId.current = null;\n }\n // NOTE: playerPosition, velocity, speed, keyState, isMoving intentionally excluded from deps\n // Using refs (lastReportedPositionRef, lastReportedVelocityRef, lastReportedSpeedRef, keyStateRef)\n // for comparison to prevent animation frame cancellation on every state update.\n // arenaBounds is computed from bounds and automatically updates when bounds changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n // playerPosition - excluded, using ref\n // keyState - excluded, using keyStateRef\n // isMoving - excluded, using keyStateRef for movement check\n // arenaBounds - excluded, derived from bounds (below)\n bounds,\n onPositionChange,\n currentStance,\n legInjuryFactor,\n isRunningProp,\n useTacticalSteps,\n // velocity - excluded, using ref\n // speed - excluded, using ref\n maxSpeedOverride,\n accelerationOverride,\n ]);\n\n // Keep updatePositionRef in sync via useEffect (not during render)\n useEffect(() => {\n updatePositionRef.current = updatePosition;\n }, [updatePosition]);\n\n // Handle keyboard input\n useEffect(() => {\n if (!enabled) return;\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 if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [enabled, handleKeyDown, handleKeyUp]);\n\n // Start animation loop when movement begins\n useEffect(() => {\n if (isMoving && !animationFrameId.current) {\n lastUpdateTime.current = performance.now();\n // Use ref to avoid dependency on updatePosition callback\n animationFrameId.current = requestAnimationFrame(() => {\n updatePositionRef.current?.();\n });\n } else if (!isMoving && animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n };\n // Only depend on isMoving - updatePositionRef is stable\n }, [isMoving]);\n\n return {\n playerPosition,\n movementState,\n isMoving,\n isKeyPressed,\n velocity,\n speed,\n };\n}\n\nexport interface InputEvent {\n readonly type: \"keydown\" | \"keyup\" | \"click\" | \"touchstart\" | \"touchend\";\n readonly key?: string;\n readonly target?: EventTarget | null;\n readonly timestamp: number;\n}\n\nexport interface CombatInput {\n readonly stanceChange?: TrigramStance;\n readonly attack?: boolean;\n readonly block?: boolean;\n readonly movement?: MovementState;\n readonly timestamp: number;\n}\n\n/**\n * Input system for combat controls\n */\nexport class InputSystem {\n private actionCallbacks = new Map<string, (() => void)[]>();\n private isEnabled = true;\n\n constructor() {\n this.setupEventListeners();\n }\n\n private setupEventListeners() {\n window.addEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.addEventListener(\"keyup\", this.handleKeyUp.bind(this));\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keydown:${key}`);\n this.triggerAction(\"keydown\");\n }\n\n private handleKeyUp(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keyup:${key}`);\n this.triggerAction(\"keyup\");\n }\n\n registerAction(action: string, callback: () => void) {\n if (!this.actionCallbacks.has(action)) {\n this.actionCallbacks.set(action, []);\n }\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.push(callback);\n }\n }\n\n unregisterAction(action: string, callback?: () => void) {\n if (!this.actionCallbacks.has(action)) return;\n\n if (callback) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n const index = callbacks.indexOf(callback);\n if (index > -1) {\n callbacks.splice(index, 1);\n }\n }\n } else {\n this.actionCallbacks.delete(action);\n }\n }\n\n clearActions() {\n this.actionCallbacks.clear();\n }\n\n isActionActive(action: string): boolean {\n return this.actionCallbacks.has(action);\n }\n\n enable() {\n this.isEnabled = true;\n }\n\n disable() {\n this.isEnabled = false;\n }\n\n private triggerAction(action: string) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.forEach((callback) => callback());\n }\n }\n\n destroy() {\n window.removeEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.removeEventListener(\"keyup\", this.handleKeyUp.bind(this));\n this.clearActions();\n }\n}\n\n/**\n * Get stance from keyboard input\n */\nexport function getStanceFromKey(key: string): TrigramStance | null {\n const stanceKey = key as keyof typeof COMBAT_CONTROLS.stanceControls;\n\n if (stanceKey in COMBAT_CONTROLS.stanceControls) {\n return COMBAT_CONTROLS.stanceControls[stanceKey].stance;\n }\n\n return null;\n}\n\n/**\n * Process combat input and return structured combat data\n */\nexport function processCombatInput(event: KeyboardEvent): CombatInput | null {\n const key = event.key;\n const timestamp = performance.now();\n\n // Check for stance change (1-8 keys)\n const stance = getStanceFromKey(key);\n if (stance) {\n return {\n stanceChange: stance,\n timestamp,\n };\n }\n\n // Check for combat actions\n switch (key.toLowerCase()) {\n case \" \": // Space for attack\n return {\n attack: true,\n timestamp,\n };\n case \"shift\":\n return {\n block: true,\n timestamp,\n };\n default:\n return null;\n }\n}\n\n/**\n * Hook for combat input handling\n */\nexport function useCombatInput(onCombatInput: (input: CombatInput) => void) {\n const isEnabled = useRef<boolean>(true);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!isEnabled.current) return;\n\n const combatInput = processCombatInput(event);\n if (combatInput) {\n onCombatInput(combatInput);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onCombatInput]);\n\n return {\n enable: () => {\n isEnabled.current = true;\n },\n disable: () => {\n isEnabled.current = false;\n },\n };\n}\n"],"mappings":"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"}
@@ -67,13 +67,15 @@ function getKoreanOverlayBaseStyles(opacity = .9) {
67
67
  function getEnhancedKoreanOverlayStyles(config = {}) {
68
68
  const { opacity = .9, glowIntensity = "medium", includeGradient = false, includeBackdropBlur = false, depthLayers = 2 } = config;
69
69
  const baseStyles = getKoreanOverlayBaseStyles(opacity);
70
- const boxShadow = combineShadowEffects([getNeonGlowEffect(KOREAN_COLORS.PRIMARY_CYAN, glowIntensity, true), getLayeredDepthEffect({
70
+ const neonGlow = getNeonGlowEffect(KOREAN_COLORS.PRIMARY_CYAN, glowIntensity, true);
71
+ const depthShadow = getLayeredDepthEffect({
71
72
  layers: depthLayers,
72
73
  baseOffset: 2,
73
74
  baseBlur: 4,
74
75
  color: KOREAN_COLORS.BLACK_SOLID,
75
76
  opacity: .5
76
- })]);
77
+ });
78
+ const boxShadow = combineShadowEffects([neonGlow, depthShadow]);
77
79
  let background = baseStyles.backgroundColor;
78
80
  if (includeGradient) background = `${getCyberpunkGradient(KOREAN_COLORS.PRIMARY_CYAN, KOREAN_COLORS.UI_BACKGROUND_DARK, 135)}, ${background}`;
79
81
  const backdropStyles = includeBackdropBlur ? getBackdropBlurEffect(10, 1.5) : {};
@@ -332,7 +334,8 @@ function getButtonVisualEffectsOnly(config) {
332
334
  * @korean 반응형간격얻기
333
335
  */
334
336
  function getResponsiveSpacing(size, isMobile = false) {
335
- const spacingValue = SPACING[size.toUpperCase()];
337
+ const spacingKey = size.toUpperCase();
338
+ const spacingValue = SPACING[spacingKey];
336
339
  const mobileScale = .875;
337
340
  if (spacingValue === void 0) {
338
341
  const fallback = SPACING.MD;
@@ -1 +1 @@
1
- {"version":3,"file":"koreanThemeHelpers.js","names":[],"sources":["../../src/utils/koreanThemeHelpers.ts"],"sourcesContent":["/**\n * Korean Theme Helper Utilities for HTML Overlays\n *\n * Provides consistent Korean martial arts cyberpunk theming across all HTML overlay components.\n * These utilities ensure color consistency, bilingual text formatting, and responsive spacing.\n *\n * @module utils/koreanThemeHelpers\n * @category UI Utilities\n * @korean 한국테마도우미\n */\n\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { SPACING, BORDER_RADIUS } from \"../types/constants/ui\";\nimport { hexToRgbaString } from \"./colorUtils\";\nimport {\n getNeonGlowEffect,\n getNeonTextShadow,\n getLayeredDepthEffect,\n getCyberpunkGradient,\n getSmoothTransition,\n getKoreanFontOptimization,\n getHoverStateStyles,\n getFocusStateStyles,\n getBackdropBlurEffect,\n getTrigramSymbolGlow as getTrigramGlowEffect,\n combineShadowEffects,\n type GlowIntensity,\n type HoverAnimationType,\n} from \"./visualEffects\";\n\n/**\n * Bilingual text format options\n * @korean 이중언어형식\n */\nexport type BilingualFormat = \"pipe\" | \"parentheses\" | \"bracket\" | \"slash\";\n\n/**\n * Button variant types for Korean theme\n * @korean 버튼변형\n */\nexport type KoreanButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"danger\"\n | \"success\"\n | \"warning\";\n\n/**\n * Responsive spacing size\n * @korean 반응형간격크기\n */\nexport type SpacingSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"xxl\";\n\n/**\n * Enhanced overlay styles configuration\n * @korean 향상된오버레이스타일설정\n */\nexport interface EnhancedOverlayConfig {\n readonly opacity?: number;\n readonly glowIntensity?: GlowIntensity;\n readonly includeGradient?: boolean;\n readonly includeBackdropBlur?: boolean;\n readonly depthLayers?: number;\n}\n\n/**\n * Base styles for all Korean-themed overlays\n *\n * Provides consistent dark background with cyan/gold accents\n *\n * @param opacity - Background opacity (0-1), default 0.9\n * @returns React.CSSProperties object with Korean theme\n *\n * @example\n * ```tsx\n * <div style={getKoreanOverlayBaseStyles(0.95)}>\n * Content\n * </div>\n * ```\n *\n * @korean 한국오버레이기본스타일얻기\n */\nexport function getKoreanOverlayBaseStyles(\n opacity: number = 0.9,\n): React.CSSProperties {\n return {\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, opacity),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n borderRadius: `${BORDER_RADIUS.MD}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 4px 20px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean overlay styles with visual effects\n *\n * Provides advanced cyberpunk styling with neon glow, depth effects,\n * gradients, and backdrop blur for Korean-themed overlays.\n *\n * @param config - Enhanced overlay configuration\n * @returns React.CSSProperties with advanced visual effects\n *\n * @example\n * ```tsx\n * const styles = getEnhancedKoreanOverlayStyles({\n * opacity: 0.95,\n * glowIntensity: 'medium',\n * includeGradient: true,\n * includeBackdropBlur: true,\n * depthLayers: 3,\n * });\n * <div style={styles}>Enhanced Content</div>\n * ```\n *\n * @korean 향상된한국오버레이스타일얻기\n */\nexport function getEnhancedKoreanOverlayStyles(\n config: EnhancedOverlayConfig = {},\n): React.CSSProperties {\n const {\n opacity = 0.9,\n glowIntensity = \"medium\",\n includeGradient = false,\n includeBackdropBlur = false,\n depthLayers = 2,\n } = config;\n\n // Base styles\n const baseStyles = getKoreanOverlayBaseStyles(opacity);\n\n // Neon glow effect\n const neonGlow = getNeonGlowEffect(\n KOREAN_COLORS.PRIMARY_CYAN,\n glowIntensity,\n true,\n );\n\n // Depth effect\n const depthShadow = getLayeredDepthEffect({\n layers: depthLayers,\n baseOffset: 2,\n baseBlur: 4,\n color: KOREAN_COLORS.BLACK_SOLID,\n opacity: 0.5,\n });\n\n // Combine shadows\n const boxShadow = combineShadowEffects([neonGlow, depthShadow]);\n\n // Optional gradient background\n let background = baseStyles.backgroundColor;\n if (includeGradient) {\n const gradient = getCyberpunkGradient(\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n 135,\n );\n background = `${gradient}, ${background}`;\n }\n\n // Optional backdrop blur\n const backdropStyles = includeBackdropBlur\n ? getBackdropBlurEffect(10, 1.5)\n : {};\n\n // Combine all styles\n return {\n ...baseStyles,\n ...backdropStyles,\n background,\n boxShadow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n ...getKoreanFontOptimization(16, \"normal\"),\n };\n}\n\n/**\n * Format bilingual text with Korean and English\n *\n * Supports multiple formatting styles:\n * - pipe: \"한글 | English\"\n * - parentheses: \"한글 (English)\"\n * - bracket: \"한글 [English]\"\n * - slash: \"한글 / English\"\n *\n * @param korean - Korean text\n * @param english - English text\n * @param format - Format style, default \"pipe\"\n * @returns Formatted bilingual string\n *\n * @example\n * ```tsx\n * formatBilingualText('공격', 'Attack', 'pipe') // \"공격 | Attack\"\n * formatBilingualText('방어', 'Defense', 'parentheses') // \"방어 (Defense)\"\n * ```\n *\n * @korean 이중언어텍스트형식화\n */\nexport function formatBilingualText(\n korean: string,\n english: string,\n format: BilingualFormat = \"pipe\",\n): string {\n switch (format) {\n case \"pipe\":\n return `${korean} | ${english}`;\n case \"parentheses\":\n return `${korean} (${english})`;\n case \"bracket\":\n return `${korean} [${english}]`;\n case \"slash\":\n return `${korean} / ${english}`;\n default:\n return `${korean} | ${english}`;\n }\n}\n\n/**\n * Get Korean button styles with variant support\n *\n * Returns consistent button styling based on variant:\n * - primary: Cyan border, gold text\n * - secondary: Gold border, white text\n * - danger: Red border, red text\n * - success: Green border, green text\n * - warning: Orange border, orange text\n *\n * @param variant - Button variant type\n * @param isHovered - Whether button is hovered\n * @param isPressed - Whether button is pressed\n * @returns React.CSSProperties for button\n *\n * @example\n * ```tsx\n * <button style={getKoreanButtonStyles('primary', isHovered, isPressed)}>\n * {formatBilingualText('확인', 'Confirm')}\n * </button>\n * ```\n *\n * @korean 한국버튼스타일얻기\n */\nexport function getKoreanButtonStyles(\n variant: KoreanButtonVariant = \"primary\",\n isHovered: boolean = false,\n isPressed: boolean = false,\n): React.CSSProperties {\n // Variant-specific colors\n const variantColors = {\n primary: {\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: KOREAN_COLORS.PRIMARY_CYAN,\n },\n secondary: {\n border: KOREAN_COLORS.ACCENT_GOLD,\n text: KOREAN_COLORS.TEXT_PRIMARY,\n hoverBg: KOREAN_COLORS.ACCENT_GOLD,\n },\n danger: {\n border: KOREAN_COLORS.ACCENT_RED,\n text: KOREAN_COLORS.ACCENT_RED,\n hoverBg: KOREAN_COLORS.ACCENT_RED,\n },\n success: {\n border: KOREAN_COLORS.ACCENT_GREEN,\n text: KOREAN_COLORS.ACCENT_GREEN,\n hoverBg: KOREAN_COLORS.ACCENT_GREEN,\n },\n warning: {\n border: KOREAN_COLORS.WARNING_ORANGE,\n text: KOREAN_COLORS.WARNING_ORANGE,\n hoverBg: KOREAN_COLORS.WARNING_ORANGE,\n },\n };\n\n const colors = variantColors[variant];\n\n let backgroundColor = hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 0.9,\n );\n let borderColor = hexToRgbaString(colors.border, 0.8);\n const textColor = hexToRgbaString(colors.text);\n let boxShadow = \"none\";\n let transform = \"scale(1)\";\n\n if (isPressed) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.2);\n transform = \"scale(0.98)\";\n } else if (isHovered) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.1);\n borderColor = hexToRgbaString(colors.border, 1.0);\n boxShadow = `0 0 10px ${hexToRgbaString(colors.border, 0.5)}`;\n }\n\n return {\n backgroundColor,\n border: `2px solid ${borderColor}`,\n borderRadius: `${BORDER_RADIUS.SM}px`,\n color: textColor,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n padding: `${SPACING.SM}px ${SPACING.MD}px`,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n boxShadow,\n transform,\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean button configuration\n * @korean 향상된한국버튼설정\n */\nexport interface EnhancedButtonConfig {\n readonly variant?: KoreanButtonVariant;\n readonly isHovered?: boolean;\n readonly isPressed?: boolean;\n readonly isFocused?: boolean;\n readonly glowIntensity?: GlowIntensity;\n readonly hoverAnimation?: HoverAnimationType;\n}\n\n/**\n * Get enhanced Korean button styles with neon glow\n *\n * Provides advanced button styling with cyberpunk neon effects,\n * smooth transitions, and Korean font optimization.\n *\n * @param config - Enhanced button configuration\n * @returns React.CSSProperties with neon glow effects\n *\n * @example\n * ```tsx\n * const buttonStyle = getKoreanButtonWithGlow({\n * variant: 'primary',\n * isHovered: true,\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined',\n * });\n * <button style={buttonStyle}>\n * {formatBilingualText('공격', 'Attack')}\n * </button>\n * ```\n *\n * @korean 네온글로우한국버튼스타일얻기\n */\nexport function getKoreanButtonWithGlow(\n config: EnhancedButtonConfig = {},\n): React.CSSProperties {\n const {\n variant = \"primary\",\n isHovered = false,\n isPressed = false,\n isFocused = false,\n glowIntensity = \"medium\",\n hoverAnimation = \"combined\",\n } = config;\n\n // Get base button styles\n const baseStyles = getKoreanButtonStyles(variant, false, isPressed);\n\n // Variant-specific glow colors\n const glowColors = {\n primary: KOREAN_COLORS.PRIMARY_CYAN,\n secondary: KOREAN_COLORS.ACCENT_GOLD,\n danger: KOREAN_COLORS.ACCENT_RED,\n success: KOREAN_COLORS.ACCENT_GREEN,\n warning: KOREAN_COLORS.WARNING_ORANGE,\n };\n\n const glowColor = glowColors[variant];\n\n // Hover state with visual effects\n let hoverStyles: React.CSSProperties = {};\n if (isHovered) {\n hoverStyles = getHoverStateStyles(glowColor, hoverAnimation, glowIntensity);\n }\n\n // Focus state\n let focusStyles: React.CSSProperties = {};\n if (isFocused) {\n focusStyles = getFocusStateStyles(glowColor, true);\n }\n\n // Extract font size from base styles (getKoreanButtonStyles always returns number | string)\n const baseFontSize =\n typeof baseStyles.fontSize === \"number\"\n ? baseStyles.fontSize\n : parseInt(String(baseStyles.fontSize), 10) || 14;\n\n // Neon text glow for button text\n const textGlow = getNeonTextShadow(\n glowColor,\n isHovered ? \"medium\" : \"subtle\",\n );\n\n // Korean font optimization\n const fontOptimization = getKoreanFontOptimization(baseFontSize, \"bold\");\n\n return {\n ...baseStyles,\n ...fontOptimization,\n ...hoverStyles,\n ...focusStyles,\n textShadow: textGlow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n };\n}\n\n/**\n * Get button visual effects only (no color/background/border)\n *\n * Extracts only visual effects (boxShadow, transition, transform, textShadow) from\n * getKoreanButtonWithGlow without color-related properties. This is useful for cases\n * where buttons need custom colors (e.g., menu selection states) but want to preserve\n * the visual effects system.\n *\n * This function provides an explicit contract for visual effects extraction, preventing\n * fragile coupling that could occur with destructuring patterns. If getKoreanButtonWithGlow\n * adds new visual effect properties in the future, they should be explicitly added here.\n *\n * **Intentionally Excluded Properties:**\n * - Layout: fontSize, fontFamily, fontWeight, padding, width, height, cursor\n * - Typography: letterSpacing, lineHeight, textRendering, WebkitFontSmoothing, MozOsxFontSmoothing\n * - Colors: color, background, backgroundColor, border, borderColor\n *\n * **Included Visual Effects:**\n * - boxShadow: Neon glow effects\n * - transition: Smooth state changes (0.2s ease)\n * - transform: Hover/press animations (scale)\n * - textShadow: Text glow effects\n *\n * **Note:** If getKoreanButtonWithGlow adds new visual effect properties (e.g., 'filter',\n * 'opacity', 'willChange', 'backdropFilter'), they must be explicitly added to this function's\n * return type and extraction logic to maintain the explicit contract.\n *\n * @param config - Same configuration as getKoreanButtonWithGlow\n * @returns React.CSSProperties with only visual effects (boxShadow, transition, transform, textShadow)\n *\n * @example\n * ```tsx\n * const visualEffects = getButtonVisualEffectsOnly({\n * variant: 'primary',\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined'\n * });\n *\n * <button style={{\n * ...visualEffects,\n * color: customColor, // Apply custom colors\n * background: customBg,\n * border: customBorder\n * }}>\n * Custom Button\n * </button>\n * ```\n *\n * @korean 버튼시각효과만얻기\n */\nexport function getButtonVisualEffectsOnly(\n config: Parameters<typeof getKoreanButtonWithGlow>[0],\n): Pick<\n React.CSSProperties,\n \"boxShadow\" | \"transition\" | \"transform\" | \"textShadow\"\n> {\n const fullStyles = getKoreanButtonWithGlow(config);\n\n // Explicitly extract only visual effect properties\n // Note: This explicitly excludes layout, typography, and color properties.\n // If new visual effects (filter, opacity, willChange, etc.) are added to\n // getKoreanButtonWithGlow, they must be added here to maintain the contract.\n return {\n boxShadow: fullStyles.boxShadow,\n transition: fullStyles.transition,\n transform: fullStyles.transform,\n textShadow: fullStyles.textShadow,\n };\n}\n\n/**\n * Get responsive spacing value\n *\n * Returns SPACING constant value for consistent spacing across components.\n * Optionally scales for mobile devices.\n *\n * **IMPORTANT**: This function accepts lowercase size parameters ('xs', 'sm', 'md', etc.)\n * to provide a more intuitive API, then internally converts to uppercase to match\n * SPACING constant keys ('XS', 'SM', 'MD', etc.). This design choice prioritizes\n * developer experience while maintaining compatibility with the SPACING constants.\n *\n * @param size - Spacing size constant ('xs', 'sm', 'md', 'lg', 'xl', 'xxl')\n * @param isMobile - Whether to apply mobile scaling (87.5% for mobile devices)\n * @returns Spacing value in pixels\n *\n * @example\n * ```tsx\n * const padding = getResponsiveSpacing('md', isMobile); // 16px desktop, 14px mobile\n * <div style={{ padding: `${padding}px` }}>Content</div>\n * ```\n *\n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n size: SpacingSize,\n isMobile: boolean = false,\n): number {\n // Convert lowercase API parameter to uppercase SPACING constant key\n // This provides a more ergonomic API while maintaining internal consistency\n const spacingKey = size.toUpperCase() as keyof typeof SPACING;\n const spacingValue = SPACING[spacingKey];\n const mobileScale = 0.875; // 87.5% for mobile\n\n // Runtime validation: While TypeScript prevents invalid sizes at compile time,\n // this check provides safety for JavaScript consumers and edge cases where\n // type assertions bypass TypeScript checks (e.g., 'as any', dynamic values)\n if (spacingValue === undefined) {\n const fallback = SPACING.MD;\n console.warn(\n `[koreanThemeHelpers:getResponsiveSpacing] Invalid spacing size \"${String(\n size,\n )}\" provided. Falling back to \"MD\".`,\n );\n return isMobile ? Math.round(fallback * mobileScale) : fallback;\n }\n\n return isMobile ? Math.round(spacingValue * mobileScale) : spacingValue;\n}\n\n/**\n * Get trigram symbol by name\n *\n * Returns Unicode trigram symbol for visual embellishment\n *\n * @param name - Trigram name in Korean\n * @returns Unicode trigram symbol\n *\n * @example\n * ```tsx\n * <div>{getTrigramSymbol('건')} 건 (Heaven)</div>\n * ```\n *\n * @korean 팔괘기호얻기\n */\nexport function getTrigramSymbol(\n name: \"건\" | \"태\" | \"리\" | \"진\" | \"손\" | \"감\" | \"간\" | \"곤\",\n): string {\n const symbols = {\n 건: \"☰\", // Heaven - 乾\n 태: \"☱\", // Lake - 兌\n 리: \"☲\", // Fire - 離\n 진: \"☳\", // Thunder - 震\n 손: \"☴\", // Wind - 巽\n 감: \"☵\", // Water - 坎\n 간: \"☶\", // Mountain - 艮\n 곤: \"☷\", // Earth - 坤\n };\n return symbols[name];\n}\n\n/**\n * Get trigram symbol styles with glow effect\n *\n * Enhances trigram symbols (☰☱☲☳☴☵☶☷) with cyberpunk neon glow\n * based on stance-specific colors and active state.\n *\n * @param config - Configuration object\n * @param config.stance - Trigram stance identifier (\"geon\", \"tae\", etc.)\n * @param config.isActive - Whether the trigram stance is currently active\n * @param config.size - Font size in pixels (optional, defaults based on active state)\n * @returns React.CSSProperties with trigram-specific glow\n *\n * @example\n * ```tsx\n * const trigramStyle = getTrigramSymbolWithGlow({ stance: 'geon', isActive: true });\n * <div style={trigramStyle}>\n * ☰ 건 | Geon\n * </div>\n * ```\n *\n * @korean 팔괘기호네온글로우스타일얻기\n */\nexport function getTrigramSymbolWithGlow(config: {\n readonly stance:\n | \"geon\"\n | \"tae\"\n | \"li\"\n | \"jin\"\n | \"son\"\n | \"gam\"\n | \"gan\"\n | \"gon\";\n readonly isActive?: boolean;\n readonly size?: number;\n}): React.CSSProperties & {\n WebkitUserSelect?: string;\n} {\n const { stance, isActive = false, size } = config;\n\n // Map English stance names to Korean\n const stanceToKorean = {\n geon: \"건\",\n tae: \"태\",\n li: \"리\",\n jin: \"진\",\n son: \"손\",\n gam: \"감\",\n gan: \"간\",\n gon: \"곤\",\n };\n\n const koreanName = stanceToKorean[stance];\n\n // Trigram-specific colors matching KOREAN_COLORS\n // Note: Comments reflect actual hex color values\n // TODO: Fix source constants (colors.ts lines 84, 87) - TRIGRAM_GEON_PRIMARY should say \"Gold\" not \"White\",\n // TRIGRAM_JIN_PRIMARY should say \"Medium Purple\" not \"Yellow\"\n const trigramColors: Record<string, number> = {\n 건: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY, // Heaven - Gold (0xffd700)\n 태: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY, // Lake - Sky Blue\n 리: KOREAN_COLORS.TRIGRAM_LI_PRIMARY, // Fire - Orange Red\n 진: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY, // Thunder - Medium Purple (0x9370db)\n 손: KOREAN_COLORS.TRIGRAM_SON_PRIMARY, // Wind - Light Green\n 감: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY, // Water - Blue\n 간: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY, // Mountain - Brown\n 곤: KOREAN_COLORS.TRIGRAM_GON_PRIMARY, // Earth - Dark Gray\n };\n\n const trigramColor = trigramColors[koreanName];\n\n // Get glow effect from visualEffects\n const glowStyles = getTrigramGlowEffect(trigramColor, isActive);\n\n // Korean font optimization for trigram symbols\n const fontSize = size ?? (isActive ? 32 : 28);\n const fontStyles = getKoreanFontOptimization(fontSize, \"bold\");\n\n return {\n ...fontStyles,\n ...glowStyles,\n fontFamily: FONT_FAMILY.KOREAN,\n display: \"inline-block\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n}\n\n/**\n * Get Korean color name for documentation\n *\n * Maps hex color to Korean color name for better documentation\n *\n * @param hexColor - Hex color value from KOREAN_COLORS\n * @returns Korean name and English translation\n *\n * @internal Used primarily for JSDoc documentation\n * @korean 한국색상이름얻기\n */\nexport function getKoreanColorName(hexColor: number): {\n korean: string;\n english: string;\n} {\n const colorNames: Record<number, { korean: string; english: string }> = {\n [KOREAN_COLORS.PRIMARY_CYAN]: { korean: \"청록\", english: \"Cyan\" },\n [KOREAN_COLORS.ACCENT_GOLD]: { korean: \"금색\", english: \"Gold\" },\n [KOREAN_COLORS.ACCENT_RED]: { korean: \"빨강\", english: \"Red\" },\n [KOREAN_COLORS.ACCENT_GREEN]: { korean: \"초록\", english: \"Green\" },\n [KOREAN_COLORS.WARNING_ORANGE]: { korean: \"주황\", english: \"Orange\" },\n [KOREAN_COLORS.TEXT_PRIMARY]: { korean: \"흰색\", english: \"White\" },\n [KOREAN_COLORS.UI_BACKGROUND_DARK]: {\n korean: \"어두운배경\",\n english: \"Dark Background\",\n },\n };\n\n return colorNames[hexColor] ?? { korean: \"알수없음\", english: \"Unknown\" };\n}\n\n/**\n * Format stat row with bilingual labels\n *\n * Creates consistent stat row styling for training/combat statistics\n *\n * @param korean - Korean label\n * @param english - English label\n * @param value - Stat value\n * @param valueColor - Hex color for value text\n * @param isMobile - Mobile responsive mode\n * @returns Stat row configuration object for React rendering\n *\n * @example\n * ```tsx\n * const statConfig = formatStatRow('점수', 'Score', 1500, KOREAN_COLORS.ACCENT_GOLD, false);\n * ```\n *\n * @korean 통계행형식화\n */\nexport function formatStatRow(\n korean: string,\n english: string,\n value: string | number,\n valueColor: number,\n isMobile: boolean,\n): {\n korean: string;\n english: string;\n value: string | number;\n valueColor: number;\n labelSize: string;\n subLabelSize: string;\n valueSize: string;\n} {\n const labelSize = isMobile ? \"11px\" : \"12px\";\n const subLabelSize = isMobile ? \"8px\" : \"9px\";\n const valueSize = isMobile ? \"16px\" : \"18px\";\n\n return {\n korean,\n english,\n value,\n valueColor,\n labelSize,\n subLabelSize,\n valueSize,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"koreanThemeHelpers.js","names":[],"sources":["../../src/utils/koreanThemeHelpers.ts"],"sourcesContent":["/**\n * Korean Theme Helper Utilities for HTML Overlays\n *\n * Provides consistent Korean martial arts cyberpunk theming across all HTML overlay components.\n * These utilities ensure color consistency, bilingual text formatting, and responsive spacing.\n *\n * @module utils/koreanThemeHelpers\n * @category UI Utilities\n * @korean 한국테마도우미\n */\n\nimport { KOREAN_COLORS, FONT_FAMILY } from \"@/types/constants\";\nimport { SPACING, BORDER_RADIUS } from \"../types/constants/ui\";\nimport { hexToRgbaString } from \"./colorUtils\";\nimport {\n getNeonGlowEffect,\n getNeonTextShadow,\n getLayeredDepthEffect,\n getCyberpunkGradient,\n getSmoothTransition,\n getKoreanFontOptimization,\n getHoverStateStyles,\n getFocusStateStyles,\n getBackdropBlurEffect,\n getTrigramSymbolGlow as getTrigramGlowEffect,\n combineShadowEffects,\n type GlowIntensity,\n type HoverAnimationType,\n} from \"./visualEffects\";\n\n/**\n * Bilingual text format options\n * @korean 이중언어형식\n */\nexport type BilingualFormat = \"pipe\" | \"parentheses\" | \"bracket\" | \"slash\";\n\n/**\n * Button variant types for Korean theme\n * @korean 버튼변형\n */\nexport type KoreanButtonVariant =\n | \"primary\"\n | \"secondary\"\n | \"danger\"\n | \"success\"\n | \"warning\";\n\n/**\n * Responsive spacing size\n * @korean 반응형간격크기\n */\nexport type SpacingSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"xxl\";\n\n/**\n * Enhanced overlay styles configuration\n * @korean 향상된오버레이스타일설정\n */\nexport interface EnhancedOverlayConfig {\n readonly opacity?: number;\n readonly glowIntensity?: GlowIntensity;\n readonly includeGradient?: boolean;\n readonly includeBackdropBlur?: boolean;\n readonly depthLayers?: number;\n}\n\n/**\n * Base styles for all Korean-themed overlays\n *\n * Provides consistent dark background with cyan/gold accents\n *\n * @param opacity - Background opacity (0-1), default 0.9\n * @returns React.CSSProperties object with Korean theme\n *\n * @example\n * ```tsx\n * <div style={getKoreanOverlayBaseStyles(0.95)}>\n * Content\n * </div>\n * ```\n *\n * @korean 한국오버레이기본스타일얻기\n */\nexport function getKoreanOverlayBaseStyles(\n opacity: number = 0.9,\n): React.CSSProperties {\n return {\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, opacity),\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8)}`,\n borderRadius: `${BORDER_RADIUS.MD}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 4px 20px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean overlay styles with visual effects\n *\n * Provides advanced cyberpunk styling with neon glow, depth effects,\n * gradients, and backdrop blur for Korean-themed overlays.\n *\n * @param config - Enhanced overlay configuration\n * @returns React.CSSProperties with advanced visual effects\n *\n * @example\n * ```tsx\n * const styles = getEnhancedKoreanOverlayStyles({\n * opacity: 0.95,\n * glowIntensity: 'medium',\n * includeGradient: true,\n * includeBackdropBlur: true,\n * depthLayers: 3,\n * });\n * <div style={styles}>Enhanced Content</div>\n * ```\n *\n * @korean 향상된한국오버레이스타일얻기\n */\nexport function getEnhancedKoreanOverlayStyles(\n config: EnhancedOverlayConfig = {},\n): React.CSSProperties {\n const {\n opacity = 0.9,\n glowIntensity = \"medium\",\n includeGradient = false,\n includeBackdropBlur = false,\n depthLayers = 2,\n } = config;\n\n // Base styles\n const baseStyles = getKoreanOverlayBaseStyles(opacity);\n\n // Neon glow effect\n const neonGlow = getNeonGlowEffect(\n KOREAN_COLORS.PRIMARY_CYAN,\n glowIntensity,\n true,\n );\n\n // Depth effect\n const depthShadow = getLayeredDepthEffect({\n layers: depthLayers,\n baseOffset: 2,\n baseBlur: 4,\n color: KOREAN_COLORS.BLACK_SOLID,\n opacity: 0.5,\n });\n\n // Combine shadows\n const boxShadow = combineShadowEffects([neonGlow, depthShadow]);\n\n // Optional gradient background\n let background = baseStyles.backgroundColor;\n if (includeGradient) {\n const gradient = getCyberpunkGradient(\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n 135,\n );\n background = `${gradient}, ${background}`;\n }\n\n // Optional backdrop blur\n const backdropStyles = includeBackdropBlur\n ? getBackdropBlurEffect(10, 1.5)\n : {};\n\n // Combine all styles\n return {\n ...baseStyles,\n ...backdropStyles,\n background,\n boxShadow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n ...getKoreanFontOptimization(16, \"normal\"),\n };\n}\n\n/**\n * Format bilingual text with Korean and English\n *\n * Supports multiple formatting styles:\n * - pipe: \"한글 | English\"\n * - parentheses: \"한글 (English)\"\n * - bracket: \"한글 [English]\"\n * - slash: \"한글 / English\"\n *\n * @param korean - Korean text\n * @param english - English text\n * @param format - Format style, default \"pipe\"\n * @returns Formatted bilingual string\n *\n * @example\n * ```tsx\n * formatBilingualText('공격', 'Attack', 'pipe') // \"공격 | Attack\"\n * formatBilingualText('방어', 'Defense', 'parentheses') // \"방어 (Defense)\"\n * ```\n *\n * @korean 이중언어텍스트형식화\n */\nexport function formatBilingualText(\n korean: string,\n english: string,\n format: BilingualFormat = \"pipe\",\n): string {\n switch (format) {\n case \"pipe\":\n return `${korean} | ${english}`;\n case \"parentheses\":\n return `${korean} (${english})`;\n case \"bracket\":\n return `${korean} [${english}]`;\n case \"slash\":\n return `${korean} / ${english}`;\n default:\n return `${korean} | ${english}`;\n }\n}\n\n/**\n * Get Korean button styles with variant support\n *\n * Returns consistent button styling based on variant:\n * - primary: Cyan border, gold text\n * - secondary: Gold border, white text\n * - danger: Red border, red text\n * - success: Green border, green text\n * - warning: Orange border, orange text\n *\n * @param variant - Button variant type\n * @param isHovered - Whether button is hovered\n * @param isPressed - Whether button is pressed\n * @returns React.CSSProperties for button\n *\n * @example\n * ```tsx\n * <button style={getKoreanButtonStyles('primary', isHovered, isPressed)}>\n * {formatBilingualText('확인', 'Confirm')}\n * </button>\n * ```\n *\n * @korean 한국버튼스타일얻기\n */\nexport function getKoreanButtonStyles(\n variant: KoreanButtonVariant = \"primary\",\n isHovered: boolean = false,\n isPressed: boolean = false,\n): React.CSSProperties {\n // Variant-specific colors\n const variantColors = {\n primary: {\n border: KOREAN_COLORS.PRIMARY_CYAN,\n text: KOREAN_COLORS.ACCENT_GOLD,\n hoverBg: KOREAN_COLORS.PRIMARY_CYAN,\n },\n secondary: {\n border: KOREAN_COLORS.ACCENT_GOLD,\n text: KOREAN_COLORS.TEXT_PRIMARY,\n hoverBg: KOREAN_COLORS.ACCENT_GOLD,\n },\n danger: {\n border: KOREAN_COLORS.ACCENT_RED,\n text: KOREAN_COLORS.ACCENT_RED,\n hoverBg: KOREAN_COLORS.ACCENT_RED,\n },\n success: {\n border: KOREAN_COLORS.ACCENT_GREEN,\n text: KOREAN_COLORS.ACCENT_GREEN,\n hoverBg: KOREAN_COLORS.ACCENT_GREEN,\n },\n warning: {\n border: KOREAN_COLORS.WARNING_ORANGE,\n text: KOREAN_COLORS.WARNING_ORANGE,\n hoverBg: KOREAN_COLORS.WARNING_ORANGE,\n },\n };\n\n const colors = variantColors[variant];\n\n let backgroundColor = hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 0.9,\n );\n let borderColor = hexToRgbaString(colors.border, 0.8);\n const textColor = hexToRgbaString(colors.text);\n let boxShadow = \"none\";\n let transform = \"scale(1)\";\n\n if (isPressed) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.2);\n transform = \"scale(0.98)\";\n } else if (isHovered) {\n backgroundColor = hexToRgbaString(colors.hoverBg, 0.1);\n borderColor = hexToRgbaString(colors.border, 1.0);\n boxShadow = `0 0 10px ${hexToRgbaString(colors.border, 0.5)}`;\n }\n\n return {\n backgroundColor,\n border: `2px solid ${borderColor}`,\n borderRadius: `${BORDER_RADIUS.SM}px`,\n color: textColor,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n padding: `${SPACING.SM}px ${SPACING.MD}px`,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n boxShadow,\n transform,\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n };\n}\n\n/**\n * Enhanced Korean button configuration\n * @korean 향상된한국버튼설정\n */\nexport interface EnhancedButtonConfig {\n readonly variant?: KoreanButtonVariant;\n readonly isHovered?: boolean;\n readonly isPressed?: boolean;\n readonly isFocused?: boolean;\n readonly glowIntensity?: GlowIntensity;\n readonly hoverAnimation?: HoverAnimationType;\n}\n\n/**\n * Get enhanced Korean button styles with neon glow\n *\n * Provides advanced button styling with cyberpunk neon effects,\n * smooth transitions, and Korean font optimization.\n *\n * @param config - Enhanced button configuration\n * @returns React.CSSProperties with neon glow effects\n *\n * @example\n * ```tsx\n * const buttonStyle = getKoreanButtonWithGlow({\n * variant: 'primary',\n * isHovered: true,\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined',\n * });\n * <button style={buttonStyle}>\n * {formatBilingualText('공격', 'Attack')}\n * </button>\n * ```\n *\n * @korean 네온글로우한국버튼스타일얻기\n */\nexport function getKoreanButtonWithGlow(\n config: EnhancedButtonConfig = {},\n): React.CSSProperties {\n const {\n variant = \"primary\",\n isHovered = false,\n isPressed = false,\n isFocused = false,\n glowIntensity = \"medium\",\n hoverAnimation = \"combined\",\n } = config;\n\n // Get base button styles\n const baseStyles = getKoreanButtonStyles(variant, false, isPressed);\n\n // Variant-specific glow colors\n const glowColors = {\n primary: KOREAN_COLORS.PRIMARY_CYAN,\n secondary: KOREAN_COLORS.ACCENT_GOLD,\n danger: KOREAN_COLORS.ACCENT_RED,\n success: KOREAN_COLORS.ACCENT_GREEN,\n warning: KOREAN_COLORS.WARNING_ORANGE,\n };\n\n const glowColor = glowColors[variant];\n\n // Hover state with visual effects\n let hoverStyles: React.CSSProperties = {};\n if (isHovered) {\n hoverStyles = getHoverStateStyles(glowColor, hoverAnimation, glowIntensity);\n }\n\n // Focus state\n let focusStyles: React.CSSProperties = {};\n if (isFocused) {\n focusStyles = getFocusStateStyles(glowColor, true);\n }\n\n // Extract font size from base styles (getKoreanButtonStyles always returns number | string)\n const baseFontSize =\n typeof baseStyles.fontSize === \"number\"\n ? baseStyles.fontSize\n : parseInt(String(baseStyles.fontSize), 10) || 14;\n\n // Neon text glow for button text\n const textGlow = getNeonTextShadow(\n glowColor,\n isHovered ? \"medium\" : \"subtle\",\n );\n\n // Korean font optimization\n const fontOptimization = getKoreanFontOptimization(baseFontSize, \"bold\");\n\n return {\n ...baseStyles,\n ...fontOptimization,\n ...hoverStyles,\n ...focusStyles,\n textShadow: textGlow,\n transition: getSmoothTransition(\"all\", \"normal\"),\n };\n}\n\n/**\n * Get button visual effects only (no color/background/border)\n *\n * Extracts only visual effects (boxShadow, transition, transform, textShadow) from\n * getKoreanButtonWithGlow without color-related properties. This is useful for cases\n * where buttons need custom colors (e.g., menu selection states) but want to preserve\n * the visual effects system.\n *\n * This function provides an explicit contract for visual effects extraction, preventing\n * fragile coupling that could occur with destructuring patterns. If getKoreanButtonWithGlow\n * adds new visual effect properties in the future, they should be explicitly added here.\n *\n * **Intentionally Excluded Properties:**\n * - Layout: fontSize, fontFamily, fontWeight, padding, width, height, cursor\n * - Typography: letterSpacing, lineHeight, textRendering, WebkitFontSmoothing, MozOsxFontSmoothing\n * - Colors: color, background, backgroundColor, border, borderColor\n *\n * **Included Visual Effects:**\n * - boxShadow: Neon glow effects\n * - transition: Smooth state changes (0.2s ease)\n * - transform: Hover/press animations (scale)\n * - textShadow: Text glow effects\n *\n * **Note:** If getKoreanButtonWithGlow adds new visual effect properties (e.g., 'filter',\n * 'opacity', 'willChange', 'backdropFilter'), they must be explicitly added to this function's\n * return type and extraction logic to maintain the explicit contract.\n *\n * @param config - Same configuration as getKoreanButtonWithGlow\n * @returns React.CSSProperties with only visual effects (boxShadow, transition, transform, textShadow)\n *\n * @example\n * ```tsx\n * const visualEffects = getButtonVisualEffectsOnly({\n * variant: 'primary',\n * glowIntensity: 'strong',\n * hoverAnimation: 'combined'\n * });\n *\n * <button style={{\n * ...visualEffects,\n * color: customColor, // Apply custom colors\n * background: customBg,\n * border: customBorder\n * }}>\n * Custom Button\n * </button>\n * ```\n *\n * @korean 버튼시각효과만얻기\n */\nexport function getButtonVisualEffectsOnly(\n config: Parameters<typeof getKoreanButtonWithGlow>[0],\n): Pick<\n React.CSSProperties,\n \"boxShadow\" | \"transition\" | \"transform\" | \"textShadow\"\n> {\n const fullStyles = getKoreanButtonWithGlow(config);\n\n // Explicitly extract only visual effect properties\n // Note: This explicitly excludes layout, typography, and color properties.\n // If new visual effects (filter, opacity, willChange, etc.) are added to\n // getKoreanButtonWithGlow, they must be added here to maintain the contract.\n return {\n boxShadow: fullStyles.boxShadow,\n transition: fullStyles.transition,\n transform: fullStyles.transform,\n textShadow: fullStyles.textShadow,\n };\n}\n\n/**\n * Get responsive spacing value\n *\n * Returns SPACING constant value for consistent spacing across components.\n * Optionally scales for mobile devices.\n *\n * **IMPORTANT**: This function accepts lowercase size parameters ('xs', 'sm', 'md', etc.)\n * to provide a more intuitive API, then internally converts to uppercase to match\n * SPACING constant keys ('XS', 'SM', 'MD', etc.). This design choice prioritizes\n * developer experience while maintaining compatibility with the SPACING constants.\n *\n * @param size - Spacing size constant ('xs', 'sm', 'md', 'lg', 'xl', 'xxl')\n * @param isMobile - Whether to apply mobile scaling (87.5% for mobile devices)\n * @returns Spacing value in pixels\n *\n * @example\n * ```tsx\n * const padding = getResponsiveSpacing('md', isMobile); // 16px desktop, 14px mobile\n * <div style={{ padding: `${padding}px` }}>Content</div>\n * ```\n *\n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n size: SpacingSize,\n isMobile: boolean = false,\n): number {\n // Convert lowercase API parameter to uppercase SPACING constant key\n // This provides a more ergonomic API while maintaining internal consistency\n const spacingKey = size.toUpperCase() as keyof typeof SPACING;\n const spacingValue = SPACING[spacingKey];\n const mobileScale = 0.875; // 87.5% for mobile\n\n // Runtime validation: While TypeScript prevents invalid sizes at compile time,\n // this check provides safety for JavaScript consumers and edge cases where\n // type assertions bypass TypeScript checks (e.g., 'as any', dynamic values)\n if (spacingValue === undefined) {\n const fallback = SPACING.MD;\n console.warn(\n `[koreanThemeHelpers:getResponsiveSpacing] Invalid spacing size \"${String(\n size,\n )}\" provided. Falling back to \"MD\".`,\n );\n return isMobile ? Math.round(fallback * mobileScale) : fallback;\n }\n\n return isMobile ? Math.round(spacingValue * mobileScale) : spacingValue;\n}\n\n/**\n * Get trigram symbol by name\n *\n * Returns Unicode trigram symbol for visual embellishment\n *\n * @param name - Trigram name in Korean\n * @returns Unicode trigram symbol\n *\n * @example\n * ```tsx\n * <div>{getTrigramSymbol('건')} 건 (Heaven)</div>\n * ```\n *\n * @korean 팔괘기호얻기\n */\nexport function getTrigramSymbol(\n name: \"건\" | \"태\" | \"리\" | \"진\" | \"손\" | \"감\" | \"간\" | \"곤\",\n): string {\n const symbols = {\n 건: \"☰\", // Heaven - 乾\n 태: \"☱\", // Lake - 兌\n 리: \"☲\", // Fire - 離\n 진: \"☳\", // Thunder - 震\n 손: \"☴\", // Wind - 巽\n 감: \"☵\", // Water - 坎\n 간: \"☶\", // Mountain - 艮\n 곤: \"☷\", // Earth - 坤\n };\n return symbols[name];\n}\n\n/**\n * Get trigram symbol styles with glow effect\n *\n * Enhances trigram symbols (☰☱☲☳☴☵☶☷) with cyberpunk neon glow\n * based on stance-specific colors and active state.\n *\n * @param config - Configuration object\n * @param config.stance - Trigram stance identifier (\"geon\", \"tae\", etc.)\n * @param config.isActive - Whether the trigram stance is currently active\n * @param config.size - Font size in pixels (optional, defaults based on active state)\n * @returns React.CSSProperties with trigram-specific glow\n *\n * @example\n * ```tsx\n * const trigramStyle = getTrigramSymbolWithGlow({ stance: 'geon', isActive: true });\n * <div style={trigramStyle}>\n * ☰ 건 | Geon\n * </div>\n * ```\n *\n * @korean 팔괘기호네온글로우스타일얻기\n */\nexport function getTrigramSymbolWithGlow(config: {\n readonly stance:\n | \"geon\"\n | \"tae\"\n | \"li\"\n | \"jin\"\n | \"son\"\n | \"gam\"\n | \"gan\"\n | \"gon\";\n readonly isActive?: boolean;\n readonly size?: number;\n}): React.CSSProperties & {\n WebkitUserSelect?: string;\n} {\n const { stance, isActive = false, size } = config;\n\n // Map English stance names to Korean\n const stanceToKorean = {\n geon: \"건\",\n tae: \"태\",\n li: \"리\",\n jin: \"진\",\n son: \"손\",\n gam: \"감\",\n gan: \"간\",\n gon: \"곤\",\n };\n\n const koreanName = stanceToKorean[stance];\n\n // Trigram-specific colors matching KOREAN_COLORS\n // Note: Comments reflect actual hex color values\n // TODO: Fix source constants (colors.ts lines 84, 87) - TRIGRAM_GEON_PRIMARY should say \"Gold\" not \"White\",\n // TRIGRAM_JIN_PRIMARY should say \"Medium Purple\" not \"Yellow\"\n const trigramColors: Record<string, number> = {\n 건: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY, // Heaven - Gold (0xffd700)\n 태: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY, // Lake - Sky Blue\n 리: KOREAN_COLORS.TRIGRAM_LI_PRIMARY, // Fire - Orange Red\n 진: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY, // Thunder - Medium Purple (0x9370db)\n 손: KOREAN_COLORS.TRIGRAM_SON_PRIMARY, // Wind - Light Green\n 감: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY, // Water - Blue\n 간: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY, // Mountain - Brown\n 곤: KOREAN_COLORS.TRIGRAM_GON_PRIMARY, // Earth - Dark Gray\n };\n\n const trigramColor = trigramColors[koreanName];\n\n // Get glow effect from visualEffects\n const glowStyles = getTrigramGlowEffect(trigramColor, isActive);\n\n // Korean font optimization for trigram symbols\n const fontSize = size ?? (isActive ? 32 : 28);\n const fontStyles = getKoreanFontOptimization(fontSize, \"bold\");\n\n return {\n ...fontStyles,\n ...glowStyles,\n fontFamily: FONT_FAMILY.KOREAN,\n display: \"inline-block\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n}\n\n/**\n * Get Korean color name for documentation\n *\n * Maps hex color to Korean color name for better documentation\n *\n * @param hexColor - Hex color value from KOREAN_COLORS\n * @returns Korean name and English translation\n *\n * @internal Used primarily for JSDoc documentation\n * @korean 한국색상이름얻기\n */\nexport function getKoreanColorName(hexColor: number): {\n korean: string;\n english: string;\n} {\n const colorNames: Record<number, { korean: string; english: string }> = {\n [KOREAN_COLORS.PRIMARY_CYAN]: { korean: \"청록\", english: \"Cyan\" },\n [KOREAN_COLORS.ACCENT_GOLD]: { korean: \"금색\", english: \"Gold\" },\n [KOREAN_COLORS.ACCENT_RED]: { korean: \"빨강\", english: \"Red\" },\n [KOREAN_COLORS.ACCENT_GREEN]: { korean: \"초록\", english: \"Green\" },\n [KOREAN_COLORS.WARNING_ORANGE]: { korean: \"주황\", english: \"Orange\" },\n [KOREAN_COLORS.TEXT_PRIMARY]: { korean: \"흰색\", english: \"White\" },\n [KOREAN_COLORS.UI_BACKGROUND_DARK]: {\n korean: \"어두운배경\",\n english: \"Dark Background\",\n },\n };\n\n return colorNames[hexColor] ?? { korean: \"알수없음\", english: \"Unknown\" };\n}\n\n/**\n * Format stat row with bilingual labels\n *\n * Creates consistent stat row styling for training/combat statistics\n *\n * @param korean - Korean label\n * @param english - English label\n * @param value - Stat value\n * @param valueColor - Hex color for value text\n * @param isMobile - Mobile responsive mode\n * @returns Stat row configuration object for React rendering\n *\n * @example\n * ```tsx\n * const statConfig = formatStatRow('점수', 'Score', 1500, KOREAN_COLORS.ACCENT_GOLD, false);\n * ```\n *\n * @korean 통계행형식화\n */\nexport function formatStatRow(\n korean: string,\n english: string,\n value: string | number,\n valueColor: number,\n isMobile: boolean,\n): {\n korean: string;\n english: string;\n value: string | number;\n valueColor: number;\n labelSize: string;\n subLabelSize: string;\n valueSize: string;\n} {\n const labelSize = isMobile ? \"11px\" : \"12px\";\n const subLabelSize = isMobile ? \"8px\" : \"9px\";\n const valueSize = isMobile ? \"16px\" : \"18px\";\n\n return {\n korean,\n english,\n value,\n valueColor,\n labelSize,\n subLabelSize,\n valueSize,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PerformanceOverlay3D.js","names":[],"sources":["../../../src/utils/performance/PerformanceOverlay3D.tsx"],"sourcesContent":["/**\n * PerformanceOverlay3D - Development-only performance stats overlay for Three.js\n *\n * Displays real-time FPS, memory, and draw call statistics\n * Only visible in development mode\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React from \"react\";\nimport { usePerformanceMonitor } from \"./usePerformanceMonitor\";\n\nexport interface PerformanceOverlay3DProps {\n readonly visible?: boolean;\n}\n\n/**\n * 3D Performance overlay component for development\n * Shows FPS, memory, draw calls, and warnings\n * Uses screen-space positioning (bottom-left corner)\n */\nexport const PerformanceOverlay3D: React.FC<PerformanceOverlay3DProps> = ({\n visible = import.meta.env.DEV,\n}) => {\n const { metrics, isGood, warnings } = usePerformanceMonitor({\n enabled: visible,\n updateInterval: 500, // Update UI every 500ms\n });\n\n if (!visible) {\n return null;\n }\n\n const fpsColor = isGood\n ? \"#00ff88\"\n : metrics.avgFps < 30\n ? \"#ff4444\"\n : \"#ffaa00\";\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n bottom: \"20px\",\n left: \"20px\",\n backgroundColor: \"rgba(0, 0, 0, 0.85)\",\n border: \"2px solid #00ffff\",\n borderRadius: \"8px\",\n padding: \"12px 16px\",\n fontFamily: \"monospace\",\n fontSize: \"12px\",\n color: \"#00ffff\",\n minWidth: \"220px\",\n maxWidth: \"280px\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n zIndex: 200,\n }}\n data-testid=\"performance-overlay\"\n >\n {/* Title */}\n <div\n style={{\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n borderBottom: \"1px solid #00ffff\",\n paddingBottom: \"4px\",\n }}\n >\n ⚡ Performance Monitor\n </div>\n\n {/* FPS Stats */}\n <div style={{ marginBottom: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>FPS:</span>\n <span style={{ color: fpsColor, fontWeight: \"bold\" }}>\n {metrics.fps.toFixed(1)}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Avg FPS:</span>\n <span style={{ color: fpsColor }}>{metrics.avgFps.toFixed(1)}</span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Min/Max:</span>\n <span>\n {metrics.minFps.toFixed(1)} / {metrics.maxFps.toFixed(1)}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Frame Time:</span>\n <span>{metrics.frameTime.toFixed(2)}ms</span>\n </div>\n </div>\n\n {/* System Stats */}\n <div\n style={{\n marginBottom: \"8px\",\n borderTop: \"1px solid #444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Memory:</span>\n <span\n style={{\n color:\n metrics.memoryMB > 300\n ? \"#ff4444\"\n : metrics.memoryMB > 250\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.memoryMB > 0\n ? `${metrics.memoryMB.toFixed(1)}MB`\n : \"N/A\"}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Draw Calls:</span>\n <span\n style={{\n color:\n metrics.drawCalls > 150\n ? \"#ff4444\"\n : metrics.drawCalls > 100\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.drawCalls}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Triangles:</span>\n <span>{(metrics.triangles / 1000).toFixed(1)}k</span>\n </div>\n </div>\n\n {/* Performance Status */}\n <div style={{ borderTop: \"1px solid #444\", paddingTop: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n }}\n >\n <span\n style={{\n width: \"12px\",\n height: \"12px\",\n borderRadius: \"50%\",\n backgroundColor: isGood ? \"#00ff88\" : \"#ff4444\",\n display: \"inline-block\",\n }}\n />\n <span style={{ fontWeight: \"bold\" }}>\n {isGood ? \"Performance Good\" : \"Performance Degraded\"}\n </span>\n </div>\n </div>\n\n {/* Warnings */}\n {warnings.length > 0 && (\n <div\n style={{\n marginTop: \"8px\",\n borderTop: \"1px solid #ff4444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n color: \"#ff4444\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n >\n ⚠️ Warnings:\n </div>\n {warnings.map((warning, index) => (\n <div\n key={index}\n style={{\n fontSize: \"11px\",\n color: \"#ffaa00\",\n marginBottom: \"2px\",\n }}\n >\n {warning}\n </div>\n ))}\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nexport default PerformanceOverlay3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"PerformanceOverlay3D.js","names":[],"sources":["../../../src/utils/performance/PerformanceOverlay3D.tsx"],"sourcesContent":["/**\n * PerformanceOverlay3D - Development-only performance stats overlay for Three.js\n *\n * Displays real-time FPS, memory, and draw call statistics\n * Only visible in development mode\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React from \"react\";\nimport { usePerformanceMonitor } from \"./usePerformanceMonitor\";\n\nexport interface PerformanceOverlay3DProps {\n readonly visible?: boolean;\n}\n\n/**\n * 3D Performance overlay component for development\n * Shows FPS, memory, draw calls, and warnings\n * Uses screen-space positioning (bottom-left corner)\n */\nexport const PerformanceOverlay3D: React.FC<PerformanceOverlay3DProps> = ({\n visible = import.meta.env.DEV,\n}) => {\n const { metrics, isGood, warnings } = usePerformanceMonitor({\n enabled: visible,\n updateInterval: 500, // Update UI every 500ms\n });\n\n if (!visible) {\n return null;\n }\n\n const fpsColor = isGood\n ? \"#00ff88\"\n : metrics.avgFps < 30\n ? \"#ff4444\"\n : \"#ffaa00\";\n\n return (\n <Html fullscreen style={{ pointerEvents: \"none\" }}>\n <div\n style={{\n position: \"absolute\",\n bottom: \"20px\",\n left: \"20px\",\n backgroundColor: \"rgba(0, 0, 0, 0.85)\",\n border: \"2px solid #00ffff\",\n borderRadius: \"8px\",\n padding: \"12px 16px\",\n fontFamily: \"monospace\",\n fontSize: \"12px\",\n color: \"#00ffff\",\n minWidth: \"220px\",\n maxWidth: \"280px\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n zIndex: 200,\n }}\n data-testid=\"performance-overlay\"\n >\n {/* Title */}\n <div\n style={{\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"8px\",\n borderBottom: \"1px solid #00ffff\",\n paddingBottom: \"4px\",\n }}\n >\n ⚡ Performance Monitor\n </div>\n\n {/* FPS Stats */}\n <div style={{ marginBottom: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>FPS:</span>\n <span style={{ color: fpsColor, fontWeight: \"bold\" }}>\n {metrics.fps.toFixed(1)}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Avg FPS:</span>\n <span style={{ color: fpsColor }}>{metrics.avgFps.toFixed(1)}</span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Min/Max:</span>\n <span>\n {metrics.minFps.toFixed(1)} / {metrics.maxFps.toFixed(1)}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Frame Time:</span>\n <span>{metrics.frameTime.toFixed(2)}ms</span>\n </div>\n </div>\n\n {/* System Stats */}\n <div\n style={{\n marginBottom: \"8px\",\n borderTop: \"1px solid #444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Memory:</span>\n <span\n style={{\n color:\n metrics.memoryMB > 300\n ? \"#ff4444\"\n : metrics.memoryMB > 250\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.memoryMB > 0\n ? `${metrics.memoryMB.toFixed(1)}MB`\n : \"N/A\"}\n </span>\n </div>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n marginBottom: \"2px\",\n }}\n >\n <span>Draw Calls:</span>\n <span\n style={{\n color:\n metrics.drawCalls > 150\n ? \"#ff4444\"\n : metrics.drawCalls > 100\n ? \"#ffaa00\"\n : \"#00ffff\",\n }}\n >\n {metrics.drawCalls}\n </span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Triangles:</span>\n <span>{(metrics.triangles / 1000).toFixed(1)}k</span>\n </div>\n </div>\n\n {/* Performance Status */}\n <div style={{ borderTop: \"1px solid #444\", paddingTop: \"8px\" }}>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n }}\n >\n <span\n style={{\n width: \"12px\",\n height: \"12px\",\n borderRadius: \"50%\",\n backgroundColor: isGood ? \"#00ff88\" : \"#ff4444\",\n display: \"inline-block\",\n }}\n />\n <span style={{ fontWeight: \"bold\" }}>\n {isGood ? \"Performance Good\" : \"Performance Degraded\"}\n </span>\n </div>\n </div>\n\n {/* Warnings */}\n {warnings.length > 0 && (\n <div\n style={{\n marginTop: \"8px\",\n borderTop: \"1px solid #ff4444\",\n paddingTop: \"8px\",\n }}\n >\n <div\n style={{\n color: \"#ff4444\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n >\n ⚠️ Warnings:\n </div>\n {warnings.map((warning, index) => (\n <div\n key={index}\n style={{\n fontSize: \"11px\",\n color: \"#ffaa00\",\n marginBottom: \"2px\",\n }}\n >\n {warning}\n </div>\n ))}\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nexport default PerformanceOverlay3D;\n"],"mappings":"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"}
@@ -205,7 +205,8 @@ function resetPlayerState(archetype, playerIndex) {
205
205
  * so the toLowerCase() call is defensive programming for type safety.
206
206
  */
207
207
  function getArchetypeAssets(archetype) {
208
- const asset = ARCHETYPE_ASSETS[archetype.toLowerCase()];
208
+ const archetypeId = archetype.toLowerCase();
209
+ const asset = ARCHETYPE_ASSETS[archetypeId];
209
210
  if (!asset) {
210
211
  console.warn(`No assets found for archetype: ${archetype}`);
211
212
  return {
@@ -242,7 +243,8 @@ function getArchetypeAssets(archetype) {
242
243
  * ```
243
244
  */
244
245
  function initializeBodyFacing(playerPosition, opponentPosition) {
245
- return createDefaultBodyFacing(calculateAngleToTarget(playerPosition, opponentPosition));
246
+ const initialAngle = calculateAngleToTarget(playerPosition, opponentPosition);
247
+ return createDefaultBodyFacing(initialAngle);
246
248
  }
247
249
  /**
248
250
  * Toggle the player's stance side (orthodox ↔ southpaw)