blacktrigram 0.7.39 → 0.7.41

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 (354) hide show
  1. package/lib/App2.js.map +1 -1
  2. package/lib/audio/AudioAssetLoader.js.map +1 -1
  3. package/lib/audio/AudioAssetRegistry.js.map +1 -1
  4. package/lib/audio/AudioCache.js.map +1 -1
  5. package/lib/audio/AudioManager.js.map +1 -1
  6. package/lib/audio/AudioMonitor.js.map +1 -1
  7. package/lib/audio/AudioPool.js.map +1 -1
  8. package/lib/audio/AudioProvider.js.map +1 -1
  9. package/lib/audio/AudioUtils.js.map +1 -1
  10. package/lib/audio/BoneImpactAudioMap.js.map +1 -1
  11. package/lib/audio/VariantSelector.js.map +1 -1
  12. package/lib/audio/types.js.map +1 -1
  13. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  14. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  15. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
  17. package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
  18. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  19. package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
  20. package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
  21. package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
  22. package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
  23. package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
  24. package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
  25. package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
  26. package/lib/components/screens/combat/components/effects/ConsciousnessBlur.js.map +1 -1
  27. package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
  28. package/lib/components/screens/combat/components/effects/PainVignette.js.map +1 -1
  29. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  30. package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
  31. package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
  32. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  33. package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
  34. package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
  35. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  36. package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
  37. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
  38. package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
  39. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  40. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  41. package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
  42. package/lib/components/screens/combat/components/hud/MobileControlsWrapper.js.map +1 -1
  43. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  44. package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
  45. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  46. package/lib/components/screens/combat/components/indicators/StaminaWarning.js.map +1 -1
  47. package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
  48. package/lib/components/screens/combat/helpers/AnimationUpdater.js.map +1 -1
  49. package/lib/components/screens/combat/helpers/combatHelpers.js.map +1 -1
  50. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  51. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  52. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  53. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  54. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  55. package/lib/components/screens/combat/hooks/useCombatState.js.map +1 -1
  56. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  57. package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
  58. package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
  59. package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
  60. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  61. package/lib/components/screens/controls/components/Key3D.js.map +1 -1
  62. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  63. package/lib/components/screens/controls/constants/ControlsConstants.js.map +1 -1
  64. package/lib/components/screens/controls/hooks/useControlsState.js.map +1 -1
  65. package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
  66. package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
  67. package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
  68. package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
  69. package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
  70. package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
  71. package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
  72. package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
  73. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  74. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  75. package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
  76. package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
  77. package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
  78. package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
  79. package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
  80. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  81. package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
  82. package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
  83. package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
  84. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  85. package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
  86. package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
  87. package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
  88. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  89. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  90. package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
  91. package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
  92. package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
  93. package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
  94. package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
  95. package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
  96. package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
  97. package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
  98. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  99. package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
  100. package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
  101. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  102. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  103. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  104. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  105. package/lib/components/screens/training/hooks/useTrainingState.js.map +1 -1
  106. package/lib/components/shared/base/BaseButton.js.map +1 -1
  107. package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
  108. package/lib/components/shared/base/BasePanel.js.map +1 -1
  109. package/lib/components/shared/base/BaseText.js.map +1 -1
  110. package/lib/components/shared/base/useKoreanTheme.js.map +1 -1
  111. package/lib/components/shared/debug/PerformanceDebugOverlayHtml.js.map +1 -1
  112. package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
  113. package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
  114. package/lib/components/shared/mobile/HapticController.js.map +1 -1
  115. package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
  116. package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
  117. package/lib/components/shared/mobile/TouchOptimizer.js.map +1 -1
  118. package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
  119. package/lib/components/shared/three/anatomy/BodySurface.js.map +1 -1
  120. package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
  121. package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
  122. package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
  123. package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
  124. package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
  125. package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
  126. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  127. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  128. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  129. package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
  130. package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
  131. package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
  132. package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
  133. package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
  134. package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
  135. package/lib/components/shared/three/indicators/HapticFeedback.js.map +1 -1
  136. package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
  137. package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
  138. package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
  139. package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
  140. package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
  141. package/lib/components/shared/three/scene/BackgroundScene3D.js.map +1 -1
  142. package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
  143. package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
  144. package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
  145. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  146. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  147. package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
  148. package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
  149. package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
  150. package/lib/components/shared/three/ui/KoreanButton.js.map +1 -1
  151. package/lib/components/shared/three/ui/KoreanPanel.js.map +1 -1
  152. package/lib/components/shared/three/ui/KoreanText.js.map +1 -1
  153. package/lib/components/shared/three/ui/MenuList.js.map +1 -1
  154. package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
  155. package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
  156. package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
  157. package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
  158. package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
  159. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  160. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  161. package/lib/components/shared/ui/BackButton.js.map +1 -1
  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/constants/bodyDimensions.js.map +1 -1
  173. package/lib/constants/bodyRenderingConstants.js.map +1 -1
  174. package/lib/data/archetypeClothing.js.map +1 -1
  175. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  176. package/lib/data/techniqueMappings.js.map +1 -1
  177. package/lib/data/techniques.js.map +1 -1
  178. package/lib/hooks/useActionFeedback.js.map +1 -1
  179. package/lib/hooks/useBalanceAnimations.js.map +1 -1
  180. package/lib/hooks/useCombatTimer.js.map +1 -1
  181. package/lib/hooks/useDebounce.js.map +1 -1
  182. package/lib/hooks/useHUDLayout.js.map +1 -1
  183. package/lib/hooks/useHandPoseTransitions.js.map +1 -1
  184. package/lib/hooks/useKeyboardControls.js.map +1 -1
  185. package/lib/hooks/useMatchCountdown.js.map +1 -1
  186. package/lib/hooks/useMuscleActivation.js.map +1 -1
  187. package/lib/hooks/usePauseMenu.js.map +1 -1
  188. package/lib/hooks/usePlayerAnimation.js.map +1 -1
  189. package/lib/hooks/useResponsiveLayout.js.map +1 -1
  190. package/lib/hooks/useRoundTransition.js.map +1 -1
  191. package/lib/hooks/useSkeletalAnimation.js.map +1 -1
  192. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  193. package/lib/hooks/useThrottle.js.map +1 -1
  194. package/lib/hooks/useTouchControls.js.map +1 -1
  195. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  196. package/lib/hooks/useWindowSize.js.map +1 -1
  197. package/lib/systems/CombatSystem.js.map +1 -1
  198. package/lib/systems/EffectCalculator.js.map +1 -1
  199. package/lib/systems/LayoutSystem.js.map +1 -1
  200. package/lib/systems/PlayerEffectManager.js.map +1 -1
  201. package/lib/systems/ResponsiveScaling.js.map +1 -1
  202. package/lib/systems/TrigramSystem.js.map +1 -1
  203. package/lib/systems/VitalPointSystem.js.map +1 -1
  204. package/lib/systems/ai/AIPersonality.js.map +1 -1
  205. package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
  206. package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
  207. package/lib/systems/ai/ComboSystem.js.map +1 -1
  208. package/lib/systems/ai/DecisionTree.js.map +1 -1
  209. package/lib/systems/ai/TrainingAI.js.map +1 -1
  210. package/lib/systems/ai/types.js.map +1 -1
  211. package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
  212. package/lib/systems/animation/builders/HandPoseApplicator.js.map +1 -1
  213. package/lib/systems/animation/builders/HandPoses.js.map +1 -1
  214. package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
  215. package/lib/systems/animation/builders/KeyframeInterpolation.js +3 -90
  216. package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
  217. package/lib/systems/animation/builders/KickPhaseApplicator.js.map +1 -1
  218. package/lib/systems/animation/builders/KoreanGuardPositions.js.map +1 -1
  219. package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
  220. package/lib/systems/animation/builders/MartialArtsConstants.js.map +1 -1
  221. package/lib/systems/animation/builders/MartialPoseApplicator.js.map +1 -1
  222. package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
  223. package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
  224. package/lib/systems/animation/builders/TrigramGuardApplicator.js.map +1 -1
  225. package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
  226. package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
  227. package/lib/systems/animation/catalogs/RecoveryAnimations.js.map +1 -1
  228. package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
  229. package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
  230. package/lib/systems/animation/catalogs/StanceGuardPoses.js.map +1 -1
  231. package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
  232. package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
  233. package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
  234. package/lib/systems/animation/core/AnimationHitTiming.js.map +1 -1
  235. package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
  236. package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
  237. package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
  238. package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
  239. package/lib/systems/animation/core/AnimationTransitions.js.map +1 -1
  240. package/lib/systems/animation/core/LateralityTransform.js.map +1 -1
  241. package/lib/systems/animation/core/RecoveryPhaseEnhancer.js.map +1 -1
  242. package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
  243. package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
  244. package/lib/systems/animation/core/types.js.map +1 -1
  245. package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
  246. package/lib/systems/animation/systems/BodyFacingSystem.js.map +1 -1
  247. package/lib/systems/animation/systems/FacialExpressions.js.map +1 -1
  248. package/lib/systems/animation/systems/FallAnimations.js.map +1 -1
  249. package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
  250. package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
  251. package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
  252. package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
  253. package/lib/systems/bodypart/CombatInjuryIntegration.js.map +1 -1
  254. package/lib/systems/bodypart/InjuryIntegration.js.map +1 -1
  255. package/lib/systems/bodypart/InjuryTracker.js.map +1 -1
  256. package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
  257. package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
  258. package/lib/systems/bodypart/types.js.map +1 -1
  259. package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
  260. package/lib/systems/breathing/feedback.js.map +1 -1
  261. package/lib/systems/breathing/integration.js.map +1 -1
  262. package/lib/systems/combat/BalanceSystem.js.map +1 -1
  263. package/lib/systems/combat/BreakingStatusEffects.js.map +1 -1
  264. package/lib/systems/combat/CombatStateSystem.js.map +1 -1
  265. package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
  266. package/lib/systems/combat/FallIntegration.js.map +1 -1
  267. package/lib/systems/combat/GrappleSystem.js.map +1 -1
  268. package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
  269. package/lib/systems/combat/PainResponseSystem.js.map +1 -1
  270. package/lib/systems/combat/TrainingCombatSystem.js.map +1 -1
  271. package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
  272. package/lib/systems/combat/typeGuards.js.map +1 -1
  273. package/lib/systems/effects.js.map +1 -1
  274. package/lib/systems/game.js.map +1 -1
  275. package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
  276. package/lib/systems/movement/helpers/AccelerationUpdater.js.map +1 -1
  277. package/lib/systems/movement/helpers/accelerationUtils.js.map +1 -1
  278. package/lib/systems/movement/integration.js.map +1 -1
  279. package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
  280. package/lib/systems/physics/CollisionDetection.js.map +1 -1
  281. package/lib/systems/physics/CoordinateMapper.js.map +1 -1
  282. package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
  283. package/lib/systems/physics/MovementPhysics.js.map +1 -1
  284. package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
  285. package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
  286. package/lib/systems/trigram/KoreanCulture.js.map +1 -1
  287. package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
  288. package/lib/systems/trigram/StanceManager.js.map +1 -1
  289. package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
  290. package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
  291. package/lib/systems/trigram/techniques/DarkOpsTechniques.js.map +1 -1
  292. package/lib/systems/trigram/techniques/GamTechniques.js.map +1 -1
  293. package/lib/systems/trigram/techniques/GanTechniques.js.map +1 -1
  294. package/lib/systems/trigram/techniques/GonTechniques.js.map +1 -1
  295. package/lib/systems/trigram/techniques/SonTechniques.js.map +1 -1
  296. package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
  297. package/lib/systems/trigram/techniques/index.js.map +1 -1
  298. package/lib/systems/trigram/types/GonTechniqueExtensions.js.map +1 -1
  299. package/lib/systems/trigram/types.js.map +1 -1
  300. package/lib/systems/types.js.map +1 -1
  301. package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
  302. package/lib/systems/vitalpoint/HitDetection.js.map +1 -1
  303. package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
  304. package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
  305. package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
  306. package/lib/types/AccessibilityTypes.js.map +1 -1
  307. package/lib/types/PhysicsTypes.js.map +1 -1
  308. package/lib/types/common.js.map +1 -1
  309. package/lib/types/constants/colors.js.map +1 -1
  310. package/lib/types/constants/designSystem.js.map +1 -1
  311. package/lib/types/constants/layout.js.map +1 -1
  312. package/lib/types/constants/performance.js.map +1 -1
  313. package/lib/types/constants/typography.js.map +1 -1
  314. package/lib/types/facial.js.map +1 -1
  315. package/lib/types/hand-animation.js.map +1 -1
  316. package/lib/types/injury.js.map +1 -1
  317. package/lib/types/physics.js.map +1 -1
  318. package/lib/types/skeletal.js.map +1 -1
  319. package/lib/types/techniqueId.js.map +1 -1
  320. package/lib/utils/accessibility.js.map +1 -1
  321. package/lib/utils/arenaWorldDimensions.js.map +1 -1
  322. package/lib/utils/assetConfig.js.map +1 -1
  323. package/lib/utils/characterScaling.js.map +1 -1
  324. package/lib/utils/colorHelpers.js.map +1 -1
  325. package/lib/utils/colorUtils.js.map +1 -1
  326. package/lib/utils/combatReadiness.js.map +1 -1
  327. package/lib/utils/controlMapping.js.map +1 -1
  328. package/lib/utils/deviceDetection.js.map +1 -1
  329. package/lib/utils/effectUtils.js.map +1 -1
  330. package/lib/utils/fabricTextures.js.map +1 -1
  331. package/lib/utils/hapticFeedback.js.map +1 -1
  332. package/lib/utils/haptics.js.map +1 -1
  333. package/lib/utils/htmlOverlayHelpers.js.map +1 -1
  334. package/lib/utils/inputSystem.js.map +1 -1
  335. package/lib/utils/koreanThemeHelpers.js.map +1 -1
  336. package/lib/utils/math.js.map +1 -1
  337. package/lib/utils/mobileLayoutHelpers.js.map +1 -1
  338. package/lib/utils/mobileUIUtils.js.map +1 -1
  339. package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
  340. package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
  341. package/lib/utils/performance/usePerformanceMonitor.js.map +1 -1
  342. package/lib/utils/performanceOptimization.js.map +1 -1
  343. package/lib/utils/player3DHelpers.js.map +1 -1
  344. package/lib/utils/playerUtils.js.map +1 -1
  345. package/lib/utils/responsiveLayout.js.map +1 -1
  346. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  347. package/lib/utils/responsiveOrientationConstants.js.map +1 -1
  348. package/lib/utils/safeAreaUtils.js.map +1 -1
  349. package/lib/utils/sharedPhysicsConfig.js.map +1 -1
  350. package/lib/utils/skeletonScaling.js.map +1 -1
  351. package/lib/utils/stanceHelpers.js.map +1 -1
  352. package/lib/utils/threeObjectPool.js.map +1 -1
  353. package/lib/utils/visualEffects.js.map +1 -1
  354. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"mobileLayoutHelpers.js","names":[],"sources":["../../src/utils/mobileLayoutHelpers.ts"],"sourcesContent":["/**\n * Mobile Layout Helpers\n *\n * Shared utilities for calculating mobile area bounds with consistent aspect ratios\n * and device-specific sizing. Used by both combat and training layout hooks.\n *\n * @module utils/mobileLayoutHelpers\n * @category Layout\n * @korean 모바일레이아웃도우미\n */\n\nimport { calculateArenaWorldDimensions } from \"./arenaWorldDimensions\";\n\n/**\n * Mobile area bounds with world dimensions.\n *\n * @public\n */\nexport interface MobileAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number;\n readonly worldWidthMeters: number;\n readonly worldDepthMeters: number;\n}\n\n/**\n * Device orientation for mobile area calculation.\n *\n * - `landscape`: width > height, the arena is rendered in 4:3 (wider than tall)\n * so both fighters remain visible from a side view.\n * - `portrait`: height ≥ width, the arena is rendered in 3:4 (taller than wide)\n * so both fighters plus the horizontal breathing-room between them fit\n * inside a narrow viewport without being occluded by the bottom HUD.\n *\n * @public\n */\nexport type MobileOrientation = \"portrait\" | \"landscape\";\n\n/**\n * Calculate mobile area bounds, orientation-aware.\n *\n * Implements consistent mobile area sizing logic shared between combat and training screens.\n * Adapts to different device resolutions while maintaining an orientation-appropriate\n * aspect ratio (4:3 in landscape, 3:4 in portrait).\n *\n * The caller is responsible for passing `bottomClearance` that already includes\n * the bottom HUD (technique bar), the mobile controls (D-Pad / action buttons)\n * and any safe-area insets. This function will never let the arena overflow\n * `height - topClearance - bottomClearance`.\n *\n * @param width - Screen width in pixels\n * @param height - Screen height in pixels\n * @param topClearance - Minimum space to reserve at top (for HUD/header + safe area)\n * @param bottomClearance - Minimum space to reserve at bottom (controls + footer + safe area)\n * @param yOffset - Y position offset (typically header height + padding)\n * @param orientation - `\"portrait\"` for tall-narrow viewports, otherwise `\"landscape\"` (default)\n * @returns Mobile area bounds with position, dimensions, scale, and world dimensions\n *\n * @example\n * ```typescript\n * // Landscape phone: 4:3 arena\n * const landscape = calculateMobileAreaBounds(667, 375, 60, 120, 70, \"landscape\");\n *\n * // Portrait phone: 3:4 arena (taller than wide)\n * const portrait = calculateMobileAreaBounds(375, 667, 80, 220, 90, \"portrait\");\n * ```\n *\n * @public\n * @korean 모바일영역경계계산\n */\nexport function calculateMobileAreaBounds(\n width: number,\n height: number,\n topClearance: number,\n bottomClearance: number,\n yOffset: number,\n orientation: MobileOrientation = \"landscape\",\n): MobileAreaBounds {\n const isPortrait = orientation === \"portrait\";\n\n // Calculate available space for the area.\n // Extra-small devices (<380px) use tighter margins for more screen real estate.\n //\n // Height reservation: the arena starts at `yOffset`, so the available\n // vertical space is bounded by `height - yOffset - bottomClearance`.\n // Falling back to `height - topClearance - bottomClearance` when a caller\n // passes `yOffset < topClearance` keeps the legacy behaviour, but we must\n // never let the arena overflow when `yOffset > topClearance` (which is the\n // case for the updated combat/training layout hooks).\n const horizontalMargin = width < 380 ? 30 : 40; // 15px vs 20px per side\n const effectiveTopReservation = Math.max(topClearance, yOffset);\n const availableHeight = Math.max(\n 0,\n height - effectiveTopReservation - bottomClearance,\n );\n const availableWidth = Math.max(0, width - horizontalMargin);\n\n // Determine max width based on device resolution\n // Device-specific sizing with extra-small support:\n // - 4K mobile landscape (≥2160px): up to 1100px\n // - QHD+/4K portrait (≥1440px): up to 960px\n // - 2K (1200-1439px): up to 760px\n // - Large phones/tablets (768-1199px): up to 560px\n // - Standard phones (380-767px): up to 400px\n // - Extra-small phones (<380px): up to 320px\n let maxMobileWidth: number;\n if (width >= 2160) {\n maxMobileWidth = Math.min(availableWidth, 1100);\n } else if (width >= 1440) {\n maxMobileWidth = Math.min(availableWidth, 960);\n } else if (width >= 1200) {\n maxMobileWidth = Math.min(availableWidth, 760);\n } else if (width >= 768) {\n maxMobileWidth = Math.min(availableWidth, 560);\n } else if (width >= 380) {\n maxMobileWidth = Math.min(availableWidth, 400);\n } else {\n // Extra-small devices (iPhone SE, old Android, budget phones)\n maxMobileWidth = Math.min(availableWidth, 320);\n }\n\n // In portrait we want the arena to consume more vertical real estate,\n // so the per-device max-height cap only applies in landscape.\n const maxMobileHeight = isPortrait\n ? availableHeight\n : Math.min(availableHeight, width < 380 ? 240 : 800);\n\n // Aspect ratio depends on orientation:\n // landscape → 4:3 (height = width × 3/4)\n // portrait → 3:4 (height = width × 4/3)\n // In portrait, width is constrained by height × 3/4 (not height × 4/3).\n const widthFromHeight = isPortrait\n ? maxMobileHeight * (3 / 4)\n : maxMobileHeight * (4 / 3);\n\n // Minimum arena width so the 3D scene stays legible, but never above\n // what the viewport can actually host:\n // - `availableWidth` caps absolute width of the arena element\n // - in portrait, `availableHeight × 3/4` caps arena width so the arena\n // height never exceeds `availableHeight` (preventing the arena from\n // being drawn behind the bottom HUD / D-Pad).\n // - in landscape, `availableHeight × 4/3` caps arena width so the 4:3\n // arena height never exceeds `availableHeight` on short mobile screens.\n const hardWidthCap = isPortrait\n ? Math.min(availableWidth, availableHeight * (3 / 4))\n : Math.min(availableWidth, availableHeight * (4 / 3));\n const minArenaWidth = Math.min(280, hardWidthCap);\n\n const areaWidth = Math.max(\n Math.min(maxMobileWidth, widthFromHeight, hardWidthCap),\n Math.max(0, minArenaWidth),\n );\n const areaHeight = isPortrait ? areaWidth * (4 / 3) : areaWidth * (3 / 4);\n\n // Calculate world dimensions based on RENDERED arena width (not screen width)\n // This ensures correct pixels-per-meter ratio for the actual visible arena\n const worldDimensions = calculateArenaWorldDimensions(areaWidth);\n\n // Calculate 3D scale factor based on reference arena\n // Reference: 10m arena at 1000px = 100 px/m\n const pixelsPerMeter = areaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - areaWidth) / 2, // Centered horizontally\n y: yOffset,\n width: areaWidth,\n height: areaHeight,\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,SAAgB,0BACd,OACA,QACA,cACA,iBACA,SACA,cAAiC,aACf;CAClB,MAAM,aAAa,gBAAgB;CAWnC,MAAM,mBAAmB,QAAQ,MAAM,KAAK;CAE5C,MAAM,kBAAkB,KAAK,IAC3B,GACA,SAH8B,KAAK,IAAI,cAAc,QAG5C,GAA0B,gBACpC;CACD,MAAM,iBAAiB,KAAK,IAAI,GAAG,QAAQ,iBAAiB;CAU5D,IAAI;AACJ,KAAI,SAAS,KACX,kBAAiB,KAAK,IAAI,gBAAgB,KAAK;UACtC,SAAS,KAClB,kBAAiB,KAAK,IAAI,gBAAgB,IAAI;UACrC,SAAS,KAClB,kBAAiB,KAAK,IAAI,gBAAgB,IAAI;UACrC,SAAS,IAClB,kBAAiB,KAAK,IAAI,gBAAgB,IAAI;UACrC,SAAS,IAClB,kBAAiB,KAAK,IAAI,gBAAgB,IAAI;KAG9C,kBAAiB,KAAK,IAAI,gBAAgB,IAAI;CAKhD,MAAM,kBAAkB,aACpB,kBACA,KAAK,IAAI,iBAAiB,QAAQ,MAAM,MAAM,IAAI;CAMtD,MAAM,kBAAkB,aACpB,mBAAmB,IAAI,KACvB,mBAAmB,IAAI;CAU3B,MAAM,eAAe,aACjB,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,GAAG,GACnD,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,GAAG;CAGvD,MAAM,YAAY,KAAK,IACrB,KAAK,IAAI,gBAAgB,iBAAiB,aAAa,EACvD,KAAK,IAAI,GAJW,KAAK,IAAI,KAAK,aAItB,CAAc,CAC3B;CACD,MAAM,aAAa,aAAa,aAAa,IAAI,KAAK,aAAa,IAAI;CAIvE,MAAM,kBAAkB,8BAA8B,UAAU;CAMhE,MAAM,QAFiB,YAAY,gBAAgB,cAEpB;AAE/B,QAAO;EACL,IAAI,QAAQ,aAAa;EACzB,GAAG;EACH,OAAO;EACP,QAAQ;EACR;EACA,kBAAkB,gBAAgB;EAClC,kBAAkB,gBAAgB;EACnC"}
1
+ {"version":3,"file":"mobileLayoutHelpers.js","names":[],"sources":["../../src/utils/mobileLayoutHelpers.ts"],"sourcesContent":["/**\n * Mobile Layout Helpers\n *\n * Shared utilities for calculating mobile area bounds with consistent aspect ratios\n * and device-specific sizing. Used by both combat and training layout hooks.\n *\n * @module utils/mobileLayoutHelpers\n * @category Layout\n * @korean 모바일레이아웃도우미\n */\n\nimport { calculateArenaWorldDimensions } from \"./arenaWorldDimensions\";\n\n/**\n * Mobile area bounds with world dimensions.\n *\n * @public\n */\nexport interface MobileAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number;\n readonly worldWidthMeters: number;\n readonly worldDepthMeters: number;\n}\n\n/**\n * Device orientation for mobile area calculation.\n *\n * - `landscape`: width > height, the arena is rendered in 4:3 (wider than tall)\n * so both fighters remain visible from a side view.\n * - `portrait`: height ≥ width, the arena is rendered in 3:4 (taller than wide)\n * so both fighters plus the horizontal breathing-room between them fit\n * inside a narrow viewport without being occluded by the bottom HUD.\n *\n * @public\n */\nexport type MobileOrientation = \"portrait\" | \"landscape\";\n\n/**\n * Calculate mobile area bounds, orientation-aware.\n *\n * Implements consistent mobile area sizing logic shared between combat and training screens.\n * Adapts to different device resolutions while maintaining an orientation-appropriate\n * aspect ratio (4:3 in landscape, 3:4 in portrait).\n *\n * The caller is responsible for passing `bottomClearance` that already includes\n * the bottom HUD (technique bar), the mobile controls (D-Pad / action buttons)\n * and any safe-area insets. This function will never let the arena overflow\n * `height - topClearance - bottomClearance`.\n *\n * @param width - Screen width in pixels\n * @param height - Screen height in pixels\n * @param topClearance - Minimum space to reserve at top (for HUD/header + safe area)\n * @param bottomClearance - Minimum space to reserve at bottom (controls + footer + safe area)\n * @param yOffset - Y position offset (typically header height + padding)\n * @param orientation - `\"portrait\"` for tall-narrow viewports, otherwise `\"landscape\"` (default)\n * @returns Mobile area bounds with position, dimensions, scale, and world dimensions\n *\n * @example\n * ```typescript\n * // Landscape phone: 4:3 arena\n * const landscape = calculateMobileAreaBounds(667, 375, 60, 120, 70, \"landscape\");\n *\n * // Portrait phone: 3:4 arena (taller than wide)\n * const portrait = calculateMobileAreaBounds(375, 667, 80, 220, 90, \"portrait\");\n * ```\n *\n * @public\n * @korean 모바일영역경계계산\n */\nexport function calculateMobileAreaBounds(\n width: number,\n height: number,\n topClearance: number,\n bottomClearance: number,\n yOffset: number,\n orientation: MobileOrientation = \"landscape\",\n): MobileAreaBounds {\n const isPortrait = orientation === \"portrait\";\n\n // Calculate available space for the area.\n // Extra-small devices (<380px) use tighter margins for more screen real estate.\n //\n // Height reservation: the arena starts at `yOffset`, so the available\n // vertical space is bounded by `height - yOffset - bottomClearance`.\n // Falling back to `height - topClearance - bottomClearance` when a caller\n // passes `yOffset < topClearance` keeps the legacy behaviour, but we must\n // never let the arena overflow when `yOffset > topClearance` (which is the\n // case for the updated combat/training layout hooks).\n const horizontalMargin = width < 380 ? 30 : 40; // 15px vs 20px per side\n const effectiveTopReservation = Math.max(topClearance, yOffset);\n const availableHeight = Math.max(\n 0,\n height - effectiveTopReservation - bottomClearance,\n );\n const availableWidth = Math.max(0, width - horizontalMargin);\n\n // Determine max width based on device resolution\n // Device-specific sizing with extra-small support:\n // - 4K mobile landscape (≥2160px): up to 1100px\n // - QHD+/4K portrait (≥1440px): up to 960px\n // - 2K (1200-1439px): up to 760px\n // - Large phones/tablets (768-1199px): up to 560px\n // - Standard phones (380-767px): up to 400px\n // - Extra-small phones (<380px): up to 320px\n let maxMobileWidth: number;\n if (width >= 2160) {\n maxMobileWidth = Math.min(availableWidth, 1100);\n } else if (width >= 1440) {\n maxMobileWidth = Math.min(availableWidth, 960);\n } else if (width >= 1200) {\n maxMobileWidth = Math.min(availableWidth, 760);\n } else if (width >= 768) {\n maxMobileWidth = Math.min(availableWidth, 560);\n } else if (width >= 380) {\n maxMobileWidth = Math.min(availableWidth, 400);\n } else {\n // Extra-small devices (iPhone SE, old Android, budget phones)\n maxMobileWidth = Math.min(availableWidth, 320);\n }\n\n // In portrait we want the arena to consume more vertical real estate,\n // so the per-device max-height cap only applies in landscape.\n const maxMobileHeight = isPortrait\n ? availableHeight\n : Math.min(availableHeight, width < 380 ? 240 : 800);\n\n // Aspect ratio depends on orientation:\n // landscape → 4:3 (height = width × 3/4)\n // portrait → 3:4 (height = width × 4/3)\n // In portrait, width is constrained by height × 3/4 (not height × 4/3).\n const widthFromHeight = isPortrait\n ? maxMobileHeight * (3 / 4)\n : maxMobileHeight * (4 / 3);\n\n // Minimum arena width so the 3D scene stays legible, but never above\n // what the viewport can actually host:\n // - `availableWidth` caps absolute width of the arena element\n // - in portrait, `availableHeight × 3/4` caps arena width so the arena\n // height never exceeds `availableHeight` (preventing the arena from\n // being drawn behind the bottom HUD / D-Pad).\n // - in landscape, `availableHeight × 4/3` caps arena width so the 4:3\n // arena height never exceeds `availableHeight` on short mobile screens.\n const hardWidthCap = isPortrait\n ? Math.min(availableWidth, availableHeight * (3 / 4))\n : Math.min(availableWidth, availableHeight * (4 / 3));\n const minArenaWidth = Math.min(280, hardWidthCap);\n\n const areaWidth = Math.max(\n Math.min(maxMobileWidth, widthFromHeight, hardWidthCap),\n Math.max(0, minArenaWidth),\n );\n const areaHeight = isPortrait ? areaWidth * (4 / 3) : areaWidth * (3 / 4);\n\n // Calculate world dimensions based on RENDERED arena width (not screen width)\n // This ensures correct pixels-per-meter ratio for the actual visible arena\n const worldDimensions = calculateArenaWorldDimensions(areaWidth);\n\n // Calculate 3D scale factor based on reference arena\n // Reference: 10m arena at 1000px = 100 px/m\n const pixelsPerMeter = areaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - areaWidth) / 2, // Centered horizontally\n y: yOffset,\n width: areaWidth,\n height: areaHeight,\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,SAAgB,0BACd,OACA,QACA,cACA,iBACA,SACA,cAAiC,aACf;CAClB,MAAM,aAAa,gBAAgB;CAWnC,MAAM,mBAAmB,QAAQ,MAAM,KAAK;CAE5C,MAAM,kBAAkB,KAAK,IAC3B,GACA,SAH8B,KAAK,IAAI,cAAc,QAG5C,GAA0B,gBACpC;CACD,MAAM,iBAAiB,KAAK,IAAI,GAAG,QAAQ,iBAAiB;CAU5D,IAAI;CACJ,IAAI,SAAS,MACX,iBAAiB,KAAK,IAAI,gBAAgB,KAAK;MAC1C,IAAI,SAAS,MAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MACzC,IAAI,SAAS,MAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MACzC,IAAI,SAAS,KAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MACzC,IAAI,SAAS,KAClB,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;MAG9C,iBAAiB,KAAK,IAAI,gBAAgB,IAAI;CAKhD,MAAM,kBAAkB,aACpB,kBACA,KAAK,IAAI,iBAAiB,QAAQ,MAAM,MAAM,IAAI;CAMtD,MAAM,kBAAkB,aACpB,mBAAmB,IAAI,KACvB,mBAAmB,IAAI;CAU3B,MAAM,eAAe,aACjB,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,GAAG,GACnD,KAAK,IAAI,gBAAgB,mBAAmB,IAAI,GAAG;CAGvD,MAAM,YAAY,KAAK,IACrB,KAAK,IAAI,gBAAgB,iBAAiB,aAAa,EACvD,KAAK,IAAI,GAJW,KAAK,IAAI,KAAK,aAItB,CAAc,CAC3B;CACD,MAAM,aAAa,aAAa,aAAa,IAAI,KAAK,aAAa,IAAI;CAIvE,MAAM,kBAAkB,8BAA8B,UAAU;CAMhE,MAAM,QAFiB,YAAY,gBAAgB,cAEpB;CAE/B,OAAO;EACL,IAAI,QAAQ,aAAa;EACzB,GAAG;EACH,OAAO;EACP,QAAQ;EACR;EACA,kBAAkB,gBAAgB;EAClC,kBAAkB,gBAAgB;EACnC"}
@@ -1 +1 @@
1
- {"version":3,"file":"mobileUIUtils.js","names":[],"sources":["../../src/utils/mobileUIUtils.ts"],"sourcesContent":["/**\n * Mobile UI utilities for responsive touch-optimized interfaces\n * \n * Provides helpers for touch target sizing, responsive font scaling,\n * and mobile-specific layout calculations following iOS/Android guidelines.\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are top priority and get:\n * - Enhanced touch targets (56px vs 48px)\n * - Larger Korean fonts (+10% for better readability)\n * - Optimized spacing and layout\n * - Full feature parity with desktop\n * \n * Supported Devices (Priority Order):\n * 1. Super HD Mobile (≥768px): Motorola Edge 60 Pro, Galaxy S-series\n * 2. Standard Mobile (375-767px): iPhone 12/13/14, standard Android\n * 3. Small Mobile (<375px): iPhone SE, budget devices\n * \n * @module utils/mobileUIUtils\n * @category Mobile Utilities\n * @korean 모바일UI유틸리티\n */\n\nimport { UI_DIMENSIONS, SPACING } from \"../types/constants/ui\";\nimport {\n KOREAN_MOBILE_FONT_SIZES,\n getKoreanFontSize,\n} from \"../types/constants/typography\";\n\n/**\n * Viewport size category for responsive design\n * \n * @category Mobile UI\n * @korean 뷰포트크기범주\n */\nexport type ViewportSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\n/**\n * Touch target size configuration\n * \n * @category Mobile UI\n * @korean 터치타겟크기설정\n */\nexport interface TouchTargetSize {\n readonly minWidth: number;\n readonly minHeight: number;\n readonly padding: number;\n readonly spacing: number;\n}\n\n/**\n * Get viewport size category from width\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are treated as 'md' for optimal layout\n * \n * @param width - Viewport width in pixels\n * @returns Viewport size category\n * \n * @example\n * ```typescript\n * getViewportSize(375); // 'xs' (iPhone SE)\n * getViewportSize(768); // 'sm' (Standard phones, high-end mobiles start here)\n * getViewportSize(2712); // 'md' (Motorola Edge 60 Pro Super HD)\n * getViewportSize(1200); // 'lg' (Desktop)\n * ```\n * \n * @public\n * @korean 뷰포트크기얻기\n */\nexport function getViewportSize(width: number): ViewportSize {\n if (width < 375) return \"xs\"; // Extra small phones\n if (width < 768) return \"sm\"; // Standard phones\n if (width < 1024) return \"md\"; // High-end mobile (Super HD), tablets\n if (width < 1440) return \"lg\"; // Small desktop\n return \"xl\"; // Large desktop\n}\n\n/**\n * Get touch-optimized button size configuration\n * Ensures minimum 48px touch targets per iOS/Android guidelines\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) get enhanced touch targets (56px)\n * for better precision on high-resolution displays\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width for fine-tuning\n * @returns Touch target size configuration\n * \n * @example\n * ```typescript\n * const buttonSize = getTouchTargetSize(true, 375);\n * // { minWidth: 48, minHeight: 48, padding: 12, spacing: 8 }\n * \n * const superHDButtonSize = getTouchTargetSize(true, 2712);\n * // { minWidth: 56, minHeight: 56, padding: 16, spacing: 12 }\n * ```\n * \n * @public\n * @korean 터치타겟크기얻기\n */\nexport function getTouchTargetSize(\n isMobile: boolean,\n viewportWidth?: number\n): TouchTargetSize {\n if (!isMobile) {\n return {\n minWidth: 120,\n minHeight: 44,\n padding: SPACING.MD,\n spacing: SPACING.MD,\n };\n }\n\n const width = viewportWidth ?? 375;\n const isExtraSmall = width < 360;\n const isSuperHD = width >= 768; // High-end mobile (Motorola Edge 60 Pro, etc.)\n\n // Super HD devices get enhanced touch targets for better precision\n if (isSuperHD) {\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n padding: SPACING.MD,\n spacing: SPACING.COMPACT,\n };\n }\n\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n padding: isExtraSmall ? SPACING.SM : SPACING.COMPACT,\n spacing: UI_DIMENSIONS.TOUCH_TARGET_SPACING,\n };\n}\n\n/**\n * Get responsive Korean font size for mobile\n * Ensures minimum 16px for body text on mobile\n * \n * PRIORITY: High-end mobile (Super HD, 2K+) get enhanced font sizes for better readability\n * \n * @param size - Size category ('SMALL', 'MEDIUM', 'LARGE')\n * @param viewportWidth - Viewport width in pixels\n * @returns Font size in pixels\n * \n * @example\n * ```typescript\n * getMobileKoreanFontSize('SMALL', 375); // 16\n * getMobileKoreanFontSize('MEDIUM', 410); // 18\n * getMobileKoreanFontSize('MEDIUM', 2712); // 20 (Super HD enhanced)\n * getMobileKoreanFontSize('LARGE', 768); // 24\n * ```\n * \n * @public\n * @korean 모바일한글글꼴크기얻기\n */\nexport function getMobileKoreanFontSize(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n viewportWidth: number\n): number {\n const fontSize = getKoreanFontSize(size, viewportWidth);\n \n // Super HD mobile devices (≥768px) get enhanced font sizes\n // for better readability on high-resolution displays\n if (viewportWidth >= 768) {\n return Math.ceil(fontSize * 1.1); // 10% larger for Super HD\n }\n \n return fontSize;\n}\n\n/**\n * Get responsive spacing value\n * Scales spacing based on viewport size\n * \n * @param baseSpacing - Base spacing value from SPACING constant\n * @param isMobile - Whether on mobile device\n * @returns Scaled spacing in pixels\n * \n * @example\n * ```typescript\n * getResponsiveSpacing(SPACING.MD, true); // 12 (COMPACT on mobile)\n * getResponsiveSpacing(SPACING.MD, false); // 16 (original)\n * ```\n * \n * @public\n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n baseSpacing: number,\n isMobile: boolean\n): number {\n return isMobile ? Math.max(SPACING.SM, baseSpacing * 0.75) : baseSpacing;\n}\n\n/**\n * Calculate responsive panel width\n * Ensures panels fit within viewport with proper margins\n * \n * @param viewportWidth - Viewport width in pixels\n * @param isMobile - Whether on mobile device\n * @param minMargin - Minimum margin on each side (default: 20px)\n * @returns Panel width in pixels\n * \n * @example\n * ```typescript\n * getResponsivePanelWidth(375, true); // ~335px (375 - 40 margin)\n * getResponsivePanelWidth(1200, false); // ~400px (max width applied)\n * ```\n * \n * @public\n * @korean 반응형패널폭얻기\n */\nexport function getResponsivePanelWidth(\n viewportWidth: number,\n isMobile: boolean,\n minMargin = 20\n): number {\n if (!isMobile) {\n return Math.min(400, viewportWidth - minMargin * 2);\n }\n\n // Mobile: use most of screen width\n return Math.min(viewportWidth - minMargin * 2, 360);\n}\n\n/**\n * Check if viewport is in landscape orientation\n * \n * @param viewportWidth - Viewport width in pixels\n * @param viewportHeight - Viewport height in pixels\n * @returns Whether in landscape orientation\n * \n * @example\n * ```typescript\n * isLandscape(812, 375); // true (iPhone X landscape)\n * isLandscape(375, 812); // false (iPhone X portrait)\n * ```\n * \n * @public\n * @korean 가로모드여부\n */\nexport function isLandscape(\n viewportWidth: number,\n viewportHeight: number\n): boolean {\n return viewportWidth > viewportHeight;\n}\n\n/**\n * Get responsive button styles for touch optimization\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width\n * @returns CSS properties for button\n * \n * @example\n * ```typescript\n * const buttonStyles = getResponsiveButtonStyles(true, 375);\n * // {\n * // minWidth: '48px',\n * // minHeight: '48px',\n * // padding: '12px',\n * // fontSize: '16px',\n * // ...\n * // }\n * ```\n * \n * @public\n * @korean 반응형버튼스타일얻기\n */\nexport function getResponsiveButtonStyles(\n isMobile: boolean,\n viewportWidth?: number\n): React.CSSProperties {\n const touchTarget = getTouchTargetSize(isMobile, viewportWidth);\n const fontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", viewportWidth ?? 375)\n : 16;\n\n return {\n minWidth: `${touchTarget.minWidth}px`,\n minHeight: `${touchTarget.minHeight}px`,\n padding: `${touchTarget.padding}px`,\n fontSize: `${fontSize}px`,\n lineHeight: \"1.4\",\n cursor: \"pointer\",\n userSelect: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n };\n}\n\n/**\n * Get responsive text styles with proper Korean font sizing\n * \n * @param size - Font size category\n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Viewport width in pixels\n * @returns CSS properties for text\n * \n * @example\n * ```typescript\n * const textStyles = getResponsiveTextStyles('MEDIUM', true, 375);\n * // {\n * // fontSize: '18px',\n * // lineHeight: '1.5',\n * // letterSpacing: '0.02em',\n * // }\n * ```\n * \n * @public\n * @korean 반응형텍스트스타일얻기\n */\nexport function getResponsiveTextStyles(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n isMobile: boolean,\n viewportWidth: number\n): React.CSSProperties {\n const fontSize = isMobile\n ? getMobileKoreanFontSize(size, viewportWidth)\n : KOREAN_MOBILE_FONT_SIZES[size].regular;\n\n return {\n fontSize: `${fontSize}px`,\n lineHeight: isMobile ? \"1.5\" : \"1.4\",\n letterSpacing: \"0.02em\",\n };\n}\n\n/**\n * Calculate minimum spacing between interactive elements\n * Ensures adequate spacing for touch accuracy\n * \n * @param isMobile - Whether on mobile device\n * @returns Minimum spacing in pixels\n * \n * @example\n * ```typescript\n * getMinimumInteractiveSpacing(true); // 8px (mobile)\n * getMinimumInteractiveSpacing(false); // 12px (desktop)\n * ```\n * \n * @public\n * @korean 최소상호작용간격얻기\n */\nexport function getMinimumInteractiveSpacing(isMobile: boolean): number {\n return isMobile\n ? UI_DIMENSIONS.TOUCH_TARGET_SPACING\n : SPACING.COMPACT;\n}\n\n/**\n * Viewport detection utilities\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are explicitly supported\n * \n * @category Mobile UI\n * @korean 뷰포트감지\n */\nexport const ViewportDetection = {\n /**\n * Check if iPhone SE or similar small device\n * @korean iPhone SE여부\n */\n isSmallMobile: (width: number) => width <= 375,\n\n /**\n * Check if standard mobile device (excluding high-end)\n * @korean 표준모바일여부\n */\n isMobile: (width: number) => width < 768,\n\n /**\n * Check if high-end mobile device (Super HD, 2K+)\n * Uses height + DPR to distinguish phones from tablets\n * High-end phones: short side 400-600px, long side >=800px, DPR >=2\n * @korean 고급모바일여부\n */\n isSuperHDMobile: (width: number, height: number = window.innerHeight) => {\n const shortSide = Math.min(width, height);\n const longSide = Math.max(width, height);\n const dpr = window.devicePixelRatio || 1;\n return shortSide >= 400 && shortSide <= 600 && longSide >= 800 && dpr >= 2;\n },\n\n /**\n * Check if tablet device\n * @korean 태블릿여부\n */\n isTablet: (width: number) => width >= 768 && width < 1024,\n\n /**\n * Check if desktop device\n * @korean 데스크톱여부\n */\n isDesktop: (width: number) => width >= 1024,\n\n /**\n * Check if device has notch (iPhone X+)\n * @korean 노치여부\n */\n hasNotch: (width: number, height: number) =>\n (width === 375 && height === 812) || // iPhone X, XS, 11 Pro\n (width === 414 && height === 896) || // iPhone XR, XS Max, 11, 11 Pro Max\n (width === 390 && height === 844) || // iPhone 12, 12 Pro, 13, 13 Pro, 14\n (width === 393 && height === 852) || // iPhone 14 Pro\n (width === 428 && height === 926), // iPhone 12/13/14 Pro Max\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA2JA,SAAgB,wBACd,MACA,eACQ;CACR,MAAM,WAAW,kBAAkB,MAAM,cAAc;AAIvD,KAAI,iBAAiB,IACnB,QAAO,KAAK,KAAK,WAAW,IAAI;AAGlC,QAAO"}
1
+ {"version":3,"file":"mobileUIUtils.js","names":[],"sources":["../../src/utils/mobileUIUtils.ts"],"sourcesContent":["/**\n * Mobile UI utilities for responsive touch-optimized interfaces\n * \n * Provides helpers for touch target sizing, responsive font scaling,\n * and mobile-specific layout calculations following iOS/Android guidelines.\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are top priority and get:\n * - Enhanced touch targets (56px vs 48px)\n * - Larger Korean fonts (+10% for better readability)\n * - Optimized spacing and layout\n * - Full feature parity with desktop\n * \n * Supported Devices (Priority Order):\n * 1. Super HD Mobile (≥768px): Motorola Edge 60 Pro, Galaxy S-series\n * 2. Standard Mobile (375-767px): iPhone 12/13/14, standard Android\n * 3. Small Mobile (<375px): iPhone SE, budget devices\n * \n * @module utils/mobileUIUtils\n * @category Mobile Utilities\n * @korean 모바일UI유틸리티\n */\n\nimport { UI_DIMENSIONS, SPACING } from \"../types/constants/ui\";\nimport {\n KOREAN_MOBILE_FONT_SIZES,\n getKoreanFontSize,\n} from \"../types/constants/typography\";\n\n/**\n * Viewport size category for responsive design\n * \n * @category Mobile UI\n * @korean 뷰포트크기범주\n */\nexport type ViewportSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\n/**\n * Touch target size configuration\n * \n * @category Mobile UI\n * @korean 터치타겟크기설정\n */\nexport interface TouchTargetSize {\n readonly minWidth: number;\n readonly minHeight: number;\n readonly padding: number;\n readonly spacing: number;\n}\n\n/**\n * Get viewport size category from width\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are treated as 'md' for optimal layout\n * \n * @param width - Viewport width in pixels\n * @returns Viewport size category\n * \n * @example\n * ```typescript\n * getViewportSize(375); // 'xs' (iPhone SE)\n * getViewportSize(768); // 'sm' (Standard phones, high-end mobiles start here)\n * getViewportSize(2712); // 'md' (Motorola Edge 60 Pro Super HD)\n * getViewportSize(1200); // 'lg' (Desktop)\n * ```\n * \n * @public\n * @korean 뷰포트크기얻기\n */\nexport function getViewportSize(width: number): ViewportSize {\n if (width < 375) return \"xs\"; // Extra small phones\n if (width < 768) return \"sm\"; // Standard phones\n if (width < 1024) return \"md\"; // High-end mobile (Super HD), tablets\n if (width < 1440) return \"lg\"; // Small desktop\n return \"xl\"; // Large desktop\n}\n\n/**\n * Get touch-optimized button size configuration\n * Ensures minimum 48px touch targets per iOS/Android guidelines\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) get enhanced touch targets (56px)\n * for better precision on high-resolution displays\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width for fine-tuning\n * @returns Touch target size configuration\n * \n * @example\n * ```typescript\n * const buttonSize = getTouchTargetSize(true, 375);\n * // { minWidth: 48, minHeight: 48, padding: 12, spacing: 8 }\n * \n * const superHDButtonSize = getTouchTargetSize(true, 2712);\n * // { minWidth: 56, minHeight: 56, padding: 16, spacing: 12 }\n * ```\n * \n * @public\n * @korean 터치타겟크기얻기\n */\nexport function getTouchTargetSize(\n isMobile: boolean,\n viewportWidth?: number\n): TouchTargetSize {\n if (!isMobile) {\n return {\n minWidth: 120,\n minHeight: 44,\n padding: SPACING.MD,\n spacing: SPACING.MD,\n };\n }\n\n const width = viewportWidth ?? 375;\n const isExtraSmall = width < 360;\n const isSuperHD = width >= 768; // High-end mobile (Motorola Edge 60 Pro, etc.)\n\n // Super HD devices get enhanced touch targets for better precision\n if (isSuperHD) {\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_COMFORTABLE,\n padding: SPACING.MD,\n spacing: SPACING.COMPACT,\n };\n }\n\n return {\n minWidth: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n minHeight: UI_DIMENSIONS.TOUCH_TARGET_MIN,\n padding: isExtraSmall ? SPACING.SM : SPACING.COMPACT,\n spacing: UI_DIMENSIONS.TOUCH_TARGET_SPACING,\n };\n}\n\n/**\n * Get responsive Korean font size for mobile\n * Ensures minimum 16px for body text on mobile\n * \n * PRIORITY: High-end mobile (Super HD, 2K+) get enhanced font sizes for better readability\n * \n * @param size - Size category ('SMALL', 'MEDIUM', 'LARGE')\n * @param viewportWidth - Viewport width in pixels\n * @returns Font size in pixels\n * \n * @example\n * ```typescript\n * getMobileKoreanFontSize('SMALL', 375); // 16\n * getMobileKoreanFontSize('MEDIUM', 410); // 18\n * getMobileKoreanFontSize('MEDIUM', 2712); // 20 (Super HD enhanced)\n * getMobileKoreanFontSize('LARGE', 768); // 24\n * ```\n * \n * @public\n * @korean 모바일한글글꼴크기얻기\n */\nexport function getMobileKoreanFontSize(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n viewportWidth: number\n): number {\n const fontSize = getKoreanFontSize(size, viewportWidth);\n \n // Super HD mobile devices (≥768px) get enhanced font sizes\n // for better readability on high-resolution displays\n if (viewportWidth >= 768) {\n return Math.ceil(fontSize * 1.1); // 10% larger for Super HD\n }\n \n return fontSize;\n}\n\n/**\n * Get responsive spacing value\n * Scales spacing based on viewport size\n * \n * @param baseSpacing - Base spacing value from SPACING constant\n * @param isMobile - Whether on mobile device\n * @returns Scaled spacing in pixels\n * \n * @example\n * ```typescript\n * getResponsiveSpacing(SPACING.MD, true); // 12 (COMPACT on mobile)\n * getResponsiveSpacing(SPACING.MD, false); // 16 (original)\n * ```\n * \n * @public\n * @korean 반응형간격얻기\n */\nexport function getResponsiveSpacing(\n baseSpacing: number,\n isMobile: boolean\n): number {\n return isMobile ? Math.max(SPACING.SM, baseSpacing * 0.75) : baseSpacing;\n}\n\n/**\n * Calculate responsive panel width\n * Ensures panels fit within viewport with proper margins\n * \n * @param viewportWidth - Viewport width in pixels\n * @param isMobile - Whether on mobile device\n * @param minMargin - Minimum margin on each side (default: 20px)\n * @returns Panel width in pixels\n * \n * @example\n * ```typescript\n * getResponsivePanelWidth(375, true); // ~335px (375 - 40 margin)\n * getResponsivePanelWidth(1200, false); // ~400px (max width applied)\n * ```\n * \n * @public\n * @korean 반응형패널폭얻기\n */\nexport function getResponsivePanelWidth(\n viewportWidth: number,\n isMobile: boolean,\n minMargin = 20\n): number {\n if (!isMobile) {\n return Math.min(400, viewportWidth - minMargin * 2);\n }\n\n // Mobile: use most of screen width\n return Math.min(viewportWidth - minMargin * 2, 360);\n}\n\n/**\n * Check if viewport is in landscape orientation\n * \n * @param viewportWidth - Viewport width in pixels\n * @param viewportHeight - Viewport height in pixels\n * @returns Whether in landscape orientation\n * \n * @example\n * ```typescript\n * isLandscape(812, 375); // true (iPhone X landscape)\n * isLandscape(375, 812); // false (iPhone X portrait)\n * ```\n * \n * @public\n * @korean 가로모드여부\n */\nexport function isLandscape(\n viewportWidth: number,\n viewportHeight: number\n): boolean {\n return viewportWidth > viewportHeight;\n}\n\n/**\n * Get responsive button styles for touch optimization\n * \n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Optional viewport width\n * @returns CSS properties for button\n * \n * @example\n * ```typescript\n * const buttonStyles = getResponsiveButtonStyles(true, 375);\n * // {\n * // minWidth: '48px',\n * // minHeight: '48px',\n * // padding: '12px',\n * // fontSize: '16px',\n * // ...\n * // }\n * ```\n * \n * @public\n * @korean 반응형버튼스타일얻기\n */\nexport function getResponsiveButtonStyles(\n isMobile: boolean,\n viewportWidth?: number\n): React.CSSProperties {\n const touchTarget = getTouchTargetSize(isMobile, viewportWidth);\n const fontSize = isMobile\n ? getMobileKoreanFontSize(\"SMALL\", viewportWidth ?? 375)\n : 16;\n\n return {\n minWidth: `${touchTarget.minWidth}px`,\n minHeight: `${touchTarget.minHeight}px`,\n padding: `${touchTarget.padding}px`,\n fontSize: `${fontSize}px`,\n lineHeight: \"1.4\",\n cursor: \"pointer\",\n userSelect: \"none\",\n WebkitTapHighlightColor: \"transparent\",\n };\n}\n\n/**\n * Get responsive text styles with proper Korean font sizing\n * \n * @param size - Font size category\n * @param isMobile - Whether on mobile device\n * @param viewportWidth - Viewport width in pixels\n * @returns CSS properties for text\n * \n * @example\n * ```typescript\n * const textStyles = getResponsiveTextStyles('MEDIUM', true, 375);\n * // {\n * // fontSize: '18px',\n * // lineHeight: '1.5',\n * // letterSpacing: '0.02em',\n * // }\n * ```\n * \n * @public\n * @korean 반응형텍스트스타일얻기\n */\nexport function getResponsiveTextStyles(\n size: keyof typeof KOREAN_MOBILE_FONT_SIZES,\n isMobile: boolean,\n viewportWidth: number\n): React.CSSProperties {\n const fontSize = isMobile\n ? getMobileKoreanFontSize(size, viewportWidth)\n : KOREAN_MOBILE_FONT_SIZES[size].regular;\n\n return {\n fontSize: `${fontSize}px`,\n lineHeight: isMobile ? \"1.5\" : \"1.4\",\n letterSpacing: \"0.02em\",\n };\n}\n\n/**\n * Calculate minimum spacing between interactive elements\n * Ensures adequate spacing for touch accuracy\n * \n * @param isMobile - Whether on mobile device\n * @returns Minimum spacing in pixels\n * \n * @example\n * ```typescript\n * getMinimumInteractiveSpacing(true); // 8px (mobile)\n * getMinimumInteractiveSpacing(false); // 12px (desktop)\n * ```\n * \n * @public\n * @korean 최소상호작용간격얻기\n */\nexport function getMinimumInteractiveSpacing(isMobile: boolean): number {\n return isMobile\n ? UI_DIMENSIONS.TOUCH_TARGET_SPACING\n : SPACING.COMPACT;\n}\n\n/**\n * Viewport detection utilities\n * \n * PRIORITY: High-end mobile devices (Super HD, 2K+) are explicitly supported\n * \n * @category Mobile UI\n * @korean 뷰포트감지\n */\nexport const ViewportDetection = {\n /**\n * Check if iPhone SE or similar small device\n * @korean iPhone SE여부\n */\n isSmallMobile: (width: number) => width <= 375,\n\n /**\n * Check if standard mobile device (excluding high-end)\n * @korean 표준모바일여부\n */\n isMobile: (width: number) => width < 768,\n\n /**\n * Check if high-end mobile device (Super HD, 2K+)\n * Uses height + DPR to distinguish phones from tablets\n * High-end phones: short side 400-600px, long side >=800px, DPR >=2\n * @korean 고급모바일여부\n */\n isSuperHDMobile: (width: number, height: number = window.innerHeight) => {\n const shortSide = Math.min(width, height);\n const longSide = Math.max(width, height);\n const dpr = window.devicePixelRatio || 1;\n return shortSide >= 400 && shortSide <= 600 && longSide >= 800 && dpr >= 2;\n },\n\n /**\n * Check if tablet device\n * @korean 태블릿여부\n */\n isTablet: (width: number) => width >= 768 && width < 1024,\n\n /**\n * Check if desktop device\n * @korean 데스크톱여부\n */\n isDesktop: (width: number) => width >= 1024,\n\n /**\n * Check if device has notch (iPhone X+)\n * @korean 노치여부\n */\n hasNotch: (width: number, height: number) =>\n (width === 375 && height === 812) || // iPhone X, XS, 11 Pro\n (width === 414 && height === 896) || // iPhone XR, XS Max, 11, 11 Pro Max\n (width === 390 && height === 844) || // iPhone 12, 12 Pro, 13, 13 Pro, 14\n (width === 393 && height === 852) || // iPhone 14 Pro\n (width === 428 && height === 926), // iPhone 12/13/14 Pro Max\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA2JA,SAAgB,wBACd,MACA,eACQ;CACR,MAAM,WAAW,kBAAkB,MAAM,cAAc;CAIvD,IAAI,iBAAiB,KACnB,OAAO,KAAK,KAAK,WAAW,IAAI;CAGlC,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"PerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/PerformanceMonitor.ts"],"sourcesContent":["/**\n * PerformanceMonitor - Real-time FPS and performance tracking for Three.js\n *\n * Monitors frame rate, memory usage, and draw calls to maintain 60fps target.\n * Provides warnings in development mode when performance degrades.\n */\n\nimport * as THREE from \"three\";\n\nexport interface PerformanceMetrics {\n readonly fps: number;\n readonly avgFps: number;\n readonly minFps: number;\n readonly maxFps: number;\n readonly frameTime: number;\n readonly memoryMB: number;\n readonly drawCalls: number;\n readonly triangles: number;\n}\n\nexport interface PerformanceThresholds {\n readonly targetFps: number;\n readonly minAcceptableFps: number;\n readonly maxMemoryMB: number;\n readonly maxDrawCalls: number;\n}\n\nconst DEFAULT_THRESHOLDS: PerformanceThresholds = {\n targetFps: 60,\n minAcceptableFps: 55,\n maxMemoryMB: 300,\n maxDrawCalls: 100,\n};\n\n// Prevent unrealistic spikes when frame deltas are extremely small (e.g., mocked timers)\nconst MAX_SAMPLE_FPS = 180;\nconst MIN_FRAME_TIME_MS = 1000 / MAX_SAMPLE_FPS;\n\n/**\n * PerformanceMonitor class for real-time performance tracking\n */\nexport class PerformanceMonitor {\n private frames: number[] = [];\n private lastTime = performance.now();\n private frameCount = 0;\n private readonly maxFrameSamples = 60; // Track last 60 frames (1 second at 60fps)\n\n private minFps = Infinity;\n private maxFps = 0;\n\n private readonly thresholds: PerformanceThresholds;\n private performanceWarnings: string[] = [];\n\n constructor(thresholds: Partial<PerformanceThresholds> = {}) {\n this.thresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update performance metrics (call once per frame)\n * @param renderer Optional Three.js renderer for draw call tracking\n * @returns Current FPS\n */\n update(renderer?: THREE.WebGLRenderer): number {\n const now = performance.now();\n const delta = now - this.lastTime;\n this.lastTime = now;\n\n if (delta <= 0) return 0; // Skip invalid frames\n\n const clampedDelta = Math.max(delta, MIN_FRAME_TIME_MS);\n const fps = Math.min(1000 / clampedDelta, MAX_SAMPLE_FPS);\n this.frames.push(fps);\n\n // Track min/max FPS\n if (fps < this.minFps) this.minFps = fps;\n if (fps > this.maxFps) this.maxFps = fps;\n\n // Keep frames array bounded\n if (this.frames.length > this.maxFrameSamples) {\n this.frames.shift();\n }\n\n this.frameCount++;\n\n // Check performance thresholds (only in dev mode)\n if (import.meta.env.DEV && this.frameCount % 60 === 0) {\n this.checkPerformanceThresholds(renderer);\n }\n\n return fps;\n }\n\n /**\n * Get current FPS\n */\n getCurrentFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames[this.frames.length - 1];\n }\n\n /**\n * Get average FPS over the sampling window\n */\n getAverageFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames.reduce((a, b) => a + b, 0) / this.frames.length;\n }\n\n /**\n * Get minimum FPS recorded\n */\n getMinFPS(): number {\n return this.minFps === Infinity ? 0 : this.minFps;\n }\n\n /**\n * Get maximum FPS recorded\n */\n getMaxFPS(): number {\n return this.maxFps;\n }\n\n /**\n * Check if performance is good (above minimum threshold)\n */\n isPerformanceGood(): boolean {\n const avgFps = this.getAverageFPS();\n return avgFps >= this.thresholds.minAcceptableFps;\n }\n\n /**\n * Get comprehensive performance metrics\n */\n getMetrics(renderer?: THREE.WebGLRenderer): PerformanceMetrics {\n const avgFps = this.getAverageFPS();\n const currentFps = this.getCurrentFPS();\n\n return {\n fps: currentFps,\n avgFps,\n minFps: this.getMinFPS(),\n maxFps: this.getMaxFPS(),\n frameTime: currentFps > 0 ? 1000 / currentFps : 0,\n memoryMB: this.getMemoryUsageMB(),\n drawCalls: renderer?.info?.render?.calls ?? 0,\n triangles: renderer?.info?.render?.triangles ?? 0,\n };\n }\n\n /**\n * Get current memory usage in MB (Chrome only)\n */\n private getMemoryUsageMB(): number {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Chrome-specific performance.memory API not in standard TS types\n const perf = performance as any;\n if (perf.memory) {\n return perf.memory.usedJSHeapSize / 1024 / 1024;\n }\n return 0;\n }\n\n /**\n * Check performance thresholds and log warnings\n */\n private checkPerformanceThresholds(renderer?: THREE.WebGLRenderer): void {\n this.performanceWarnings = [];\n\n const avgFps = this.getAverageFPS();\n const memoryMB = this.getMemoryUsageMB();\n const drawCalls = renderer?.info?.render?.calls ?? 0;\n\n // Check FPS\n if (avgFps < this.thresholds.minAcceptableFps) {\n const warning = `⚠️ FPS below threshold: ${avgFps.toFixed(1)} < ${\n this.thresholds.minAcceptableFps\n }`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check memory\n if (memoryMB > 0 && memoryMB > this.thresholds.maxMemoryMB) {\n const warning = `⚠️ Memory usage high: ${memoryMB.toFixed(1)}MB > ${\n this.thresholds.maxMemoryMB\n }MB`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check draw calls\n if (drawCalls > this.thresholds.maxDrawCalls) {\n const warning = `⚠️ Draw calls high: ${drawCalls} > ${this.thresholds.maxDrawCalls}`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n }\n\n /**\n * Get current performance warnings\n */\n getWarnings(): readonly string[] {\n return this.performanceWarnings;\n }\n\n /**\n * Reset all metrics\n */\n reset(): void {\n this.frames = [];\n this.lastTime = performance.now();\n this.frameCount = 0;\n this.minFps = Infinity;\n this.maxFps = 0;\n this.performanceWarnings = [];\n }\n\n /**\n * Get formatted performance summary string\n */\n getSummary(renderer?: THREE.WebGLRenderer): string {\n const metrics = this.getMetrics(renderer);\n return (\n `FPS: ${metrics.fps.toFixed(1)} | ` +\n `Avg: ${metrics.avgFps.toFixed(1)} | ` +\n `Min: ${metrics.minFps.toFixed(1)} | ` +\n `Max: ${metrics.maxFps.toFixed(1)} | ` +\n `Frame: ${metrics.frameTime.toFixed(2)}ms | ` +\n `Mem: ${metrics.memoryMB.toFixed(1)}MB | ` +\n `Draws: ${metrics.drawCalls} | ` +\n `Tris: ${(metrics.triangles / 1000).toFixed(1)}k`\n );\n }\n}\n\n/**\n * Create a performance monitor with optional custom thresholds\n */\nexport function createPerformanceMonitor(\n thresholds?: Partial<PerformanceThresholds>,\n): PerformanceMonitor {\n return new PerformanceMonitor(thresholds);\n}\n\nexport default PerformanceMonitor;\n"],"mappings":"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"}
1
+ {"version":3,"file":"PerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/PerformanceMonitor.ts"],"sourcesContent":["/**\n * PerformanceMonitor - Real-time FPS and performance tracking for Three.js\n *\n * Monitors frame rate, memory usage, and draw calls to maintain 60fps target.\n * Provides warnings in development mode when performance degrades.\n */\n\nimport * as THREE from \"three\";\n\nexport interface PerformanceMetrics {\n readonly fps: number;\n readonly avgFps: number;\n readonly minFps: number;\n readonly maxFps: number;\n readonly frameTime: number;\n readonly memoryMB: number;\n readonly drawCalls: number;\n readonly triangles: number;\n}\n\nexport interface PerformanceThresholds {\n readonly targetFps: number;\n readonly minAcceptableFps: number;\n readonly maxMemoryMB: number;\n readonly maxDrawCalls: number;\n}\n\nconst DEFAULT_THRESHOLDS: PerformanceThresholds = {\n targetFps: 60,\n minAcceptableFps: 55,\n maxMemoryMB: 300,\n maxDrawCalls: 100,\n};\n\n// Prevent unrealistic spikes when frame deltas are extremely small (e.g., mocked timers)\nconst MAX_SAMPLE_FPS = 180;\nconst MIN_FRAME_TIME_MS = 1000 / MAX_SAMPLE_FPS;\n\n/**\n * PerformanceMonitor class for real-time performance tracking\n */\nexport class PerformanceMonitor {\n private frames: number[] = [];\n private lastTime = performance.now();\n private frameCount = 0;\n private readonly maxFrameSamples = 60; // Track last 60 frames (1 second at 60fps)\n\n private minFps = Infinity;\n private maxFps = 0;\n\n private readonly thresholds: PerformanceThresholds;\n private performanceWarnings: string[] = [];\n\n constructor(thresholds: Partial<PerformanceThresholds> = {}) {\n this.thresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update performance metrics (call once per frame)\n * @param renderer Optional Three.js renderer for draw call tracking\n * @returns Current FPS\n */\n update(renderer?: THREE.WebGLRenderer): number {\n const now = performance.now();\n const delta = now - this.lastTime;\n this.lastTime = now;\n\n if (delta <= 0) return 0; // Skip invalid frames\n\n const clampedDelta = Math.max(delta, MIN_FRAME_TIME_MS);\n const fps = Math.min(1000 / clampedDelta, MAX_SAMPLE_FPS);\n this.frames.push(fps);\n\n // Track min/max FPS\n if (fps < this.minFps) this.minFps = fps;\n if (fps > this.maxFps) this.maxFps = fps;\n\n // Keep frames array bounded\n if (this.frames.length > this.maxFrameSamples) {\n this.frames.shift();\n }\n\n this.frameCount++;\n\n // Check performance thresholds (only in dev mode)\n if (import.meta.env.DEV && this.frameCount % 60 === 0) {\n this.checkPerformanceThresholds(renderer);\n }\n\n return fps;\n }\n\n /**\n * Get current FPS\n */\n getCurrentFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames[this.frames.length - 1];\n }\n\n /**\n * Get average FPS over the sampling window\n */\n getAverageFPS(): number {\n if (this.frames.length === 0) return 0;\n return this.frames.reduce((a, b) => a + b, 0) / this.frames.length;\n }\n\n /**\n * Get minimum FPS recorded\n */\n getMinFPS(): number {\n return this.minFps === Infinity ? 0 : this.minFps;\n }\n\n /**\n * Get maximum FPS recorded\n */\n getMaxFPS(): number {\n return this.maxFps;\n }\n\n /**\n * Check if performance is good (above minimum threshold)\n */\n isPerformanceGood(): boolean {\n const avgFps = this.getAverageFPS();\n return avgFps >= this.thresholds.minAcceptableFps;\n }\n\n /**\n * Get comprehensive performance metrics\n */\n getMetrics(renderer?: THREE.WebGLRenderer): PerformanceMetrics {\n const avgFps = this.getAverageFPS();\n const currentFps = this.getCurrentFPS();\n\n return {\n fps: currentFps,\n avgFps,\n minFps: this.getMinFPS(),\n maxFps: this.getMaxFPS(),\n frameTime: currentFps > 0 ? 1000 / currentFps : 0,\n memoryMB: this.getMemoryUsageMB(),\n drawCalls: renderer?.info?.render?.calls ?? 0,\n triangles: renderer?.info?.render?.triangles ?? 0,\n };\n }\n\n /**\n * Get current memory usage in MB (Chrome only)\n */\n private getMemoryUsageMB(): number {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Chrome-specific performance.memory API not in standard TS types\n const perf = performance as any;\n if (perf.memory) {\n return perf.memory.usedJSHeapSize / 1024 / 1024;\n }\n return 0;\n }\n\n /**\n * Check performance thresholds and log warnings\n */\n private checkPerformanceThresholds(renderer?: THREE.WebGLRenderer): void {\n this.performanceWarnings = [];\n\n const avgFps = this.getAverageFPS();\n const memoryMB = this.getMemoryUsageMB();\n const drawCalls = renderer?.info?.render?.calls ?? 0;\n\n // Check FPS\n if (avgFps < this.thresholds.minAcceptableFps) {\n const warning = `⚠️ FPS below threshold: ${avgFps.toFixed(1)} < ${\n this.thresholds.minAcceptableFps\n }`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check memory\n if (memoryMB > 0 && memoryMB > this.thresholds.maxMemoryMB) {\n const warning = `⚠️ Memory usage high: ${memoryMB.toFixed(1)}MB > ${\n this.thresholds.maxMemoryMB\n }MB`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n\n // Check draw calls\n if (drawCalls > this.thresholds.maxDrawCalls) {\n const warning = `⚠️ Draw calls high: ${drawCalls} > ${this.thresholds.maxDrawCalls}`;\n this.performanceWarnings.push(warning);\n console.warn(warning);\n }\n }\n\n /**\n * Get current performance warnings\n */\n getWarnings(): readonly string[] {\n return this.performanceWarnings;\n }\n\n /**\n * Reset all metrics\n */\n reset(): void {\n this.frames = [];\n this.lastTime = performance.now();\n this.frameCount = 0;\n this.minFps = Infinity;\n this.maxFps = 0;\n this.performanceWarnings = [];\n }\n\n /**\n * Get formatted performance summary string\n */\n getSummary(renderer?: THREE.WebGLRenderer): string {\n const metrics = this.getMetrics(renderer);\n return (\n `FPS: ${metrics.fps.toFixed(1)} | ` +\n `Avg: ${metrics.avgFps.toFixed(1)} | ` +\n `Min: ${metrics.minFps.toFixed(1)} | ` +\n `Max: ${metrics.maxFps.toFixed(1)} | ` +\n `Frame: ${metrics.frameTime.toFixed(2)}ms | ` +\n `Mem: ${metrics.memoryMB.toFixed(1)}MB | ` +\n `Draws: ${metrics.drawCalls} | ` +\n `Tris: ${(metrics.triangles / 1000).toFixed(1)}k`\n );\n }\n}\n\n/**\n * Create a performance monitor with optional custom thresholds\n */\nexport function createPerformanceMonitor(\n thresholds?: Partial<PerformanceThresholds>,\n): PerformanceMonitor {\n return new PerformanceMonitor(thresholds);\n}\n\nexport default PerformanceMonitor;\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":";;;;;;;;;;;;;;;;AAoBA,IAAa,wBAA6D,EACxE,UAAA,YACI;CACJ,MAAM,EAAE,SAAS,QAAQ,aAAa,sBAAsB;EAC1D,SAAS;EACT,gBAAgB;EACjB,CAAC;AAEF,KAAI,CAAC,QACH,QAAO;CAGT,MAAM,WAAW,SACb,YACA,QAAQ,SAAS,KACjB,YACA;AAEJ,QACE,oBAAC,MAAD;EAAM,YAAA;EAAW,OAAO,EAAE,eAAe,QAAQ;YAC/C,qBAAC,OAAD;GACE,OAAO;IACL,UAAU;IACV,QAAQ;IACR,MAAM;IACN,iBAAiB;IACjB,QAAQ;IACR,cAAc;IACd,SAAS;IACT,YAAY;IACZ,UAAU;IACV,OAAO;IACP,UAAU;IACV,UAAU;IACV,YAAY;IACZ,eAAe;IACf,QAAQ;IACT;GACD,eAAY;aAlBd;IAqBE,oBAAC,OAAD;KACE,OAAO;MACL,UAAU;MACV,YAAY;MACZ,cAAc;MACd,cAAc;MACd,eAAe;MAChB;eACF;KAEK,CAAA;IAGN,qBAAC,OAAD;KAAK,OAAO,EAAE,cAAc,OAAO;eAAnC;MACE,qBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,gBAAgB;QAChB,cAAc;QACf;iBALH,CAOE,oBAAC,QAAD,EAAA,UAAM,QAAW,CAAA,EACjB,oBAAC,QAAD;QAAM,OAAO;SAAE,OAAO;SAAU,YAAY;SAAQ;kBACjD,QAAQ,IAAI,QAAQ,EAAE;QAClB,CAAA,CACH;;MACN,qBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,gBAAgB;QAChB,cAAc;QACf;iBALH,CAOE,oBAAC,QAAD,EAAA,UAAM,YAAe,CAAA,EACrB,oBAAC,QAAD;QAAM,OAAO,EAAE,OAAO,UAAU;kBAAG,QAAQ,OAAO,QAAQ,EAAE;QAAQ,CAAA,CAChE;;MACN,qBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,gBAAgB;QAChB,cAAc;QACf;iBALH,CAOE,oBAAC,QAAD,EAAA,UAAM,YAAe,CAAA,EACrB,qBAAC,QAAD,EAAA,UAAA;QACG,QAAQ,OAAO,QAAQ,EAAE;QAAC;QAAI,QAAQ,OAAO,QAAQ,EAAE;QACnD,EAAA,CAAA,CACH;;MACN,qBAAC,OAAD;OAAK,OAAO;QAAE,SAAS;QAAQ,gBAAgB;QAAiB;iBAAhE,CACE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,EACxB,qBAAC,QAAD,EAAA,UAAA,CAAO,QAAQ,UAAU,QAAQ,EAAE,EAAC,KAAS,EAAA,CAAA,CACzC;;MACF;;IAGN,qBAAC,OAAD;KACE,OAAO;MACL,cAAc;MACd,WAAW;MACX,YAAY;MACb;eALH;MAOE,qBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,gBAAgB;QAChB,cAAc;QACf;iBALH,CAOE,oBAAC,QAAD,EAAA,UAAM,WAAc,CAAA,EACpB,oBAAC,QAAD;QACE,OAAO,EACL,OACE,QAAQ,WAAW,MACf,YACA,QAAQ,WAAW,MACnB,YACA,WACP;kBAEA,QAAQ,WAAW,IAChB,GAAG,QAAQ,SAAS,QAAQ,EAAE,CAAC,MAC/B;QACC,CAAA,CACH;;MACN,qBAAC,OAAD;OACE,OAAO;QACL,SAAS;QACT,gBAAgB;QAChB,cAAc;QACf;iBALH,CAOE,oBAAC,QAAD,EAAA,UAAM,eAAkB,CAAA,EACxB,oBAAC,QAAD;QACE,OAAO,EACL,OACE,QAAQ,YAAY,MAChB,YACA,QAAQ,YAAY,MACpB,YACA,WACP;kBAEA,QAAQ;QACJ,CAAA,CACH;;MACN,qBAAC,OAAD;OAAK,OAAO;QAAE,SAAS;QAAQ,gBAAgB;QAAiB;iBAAhE,CACE,oBAAC,QAAD,EAAA,UAAM,cAAiB,CAAA,EACvB,qBAAC,QAAD,EAAA,UAAA,EAAQ,QAAQ,YAAY,KAAM,QAAQ,EAAE,EAAC,IAAQ,EAAA,CAAA,CACjD;;MACF;;IAGN,oBAAC,OAAD;KAAK,OAAO;MAAE,WAAW;MAAkB,YAAY;MAAO;eAC5D,qBAAC,OAAD;MACE,OAAO;OACL,SAAS;OACT,YAAY;OACZ,KAAK;OACN;gBALH,CAOE,oBAAC,QAAD,EACE,OAAO;OACL,OAAO;OACP,QAAQ;OACR,cAAc;OACd,iBAAiB,SAAS,YAAY;OACtC,SAAS;OACV,EACD,CAAA,EACF,oBAAC,QAAD;OAAM,OAAO,EAAE,YAAY,QAAQ;iBAChC,SAAS,qBAAqB;OAC1B,CAAA,CACH;;KACF,CAAA;IAGL,SAAS,SAAS,KACjB,qBAAC,OAAD;KACE,OAAO;MACL,WAAW;MACX,WAAW;MACX,YAAY;MACb;eALH,CAOE,oBAAC,OAAD;MACE,OAAO;OACL,OAAO;OACP,YAAY;OACZ,cAAc;OACf;gBACF;MAEK,CAAA,EACL,SAAS,KAAK,SAAS,UACtB,oBAAC,OAAD;MAEE,OAAO;OACL,UAAU;OACV,OAAO;OACP,cAAc;OACf;gBAEA;MACG,EARC,MAQD,CACN,CACE;;IAEJ;;EACD,CAAA"}
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 +1 @@
1
- {"version":3,"file":"usePerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/usePerformanceMonitor.ts"],"sourcesContent":["/**\n * React hook for Three.js performance monitoring\n * \n * Integrates PerformanceMonitor with useFrame for real-time FPS tracking\n */\n\nimport { useFrame, useThree } from '@react-three/fiber';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { PerformanceMonitor, PerformanceMetrics, PerformanceThresholds } from './PerformanceMonitor';\n\nexport interface UsePerformanceMonitorOptions {\n readonly enabled?: boolean;\n readonly thresholds?: Partial<PerformanceThresholds>;\n readonly updateInterval?: number; // Update interval in ms for React state\n}\n\nexport interface PerformanceMonitorState {\n readonly metrics: PerformanceMetrics;\n readonly isGood: boolean;\n readonly warnings: readonly string[];\n}\n\n/**\n * Hook for monitoring Three.js performance in real-time\n * \n * @param options Configuration options\n * @returns Current performance state\n * \n * @example\n * ```tsx\n * function CombatScene() {\n * const { metrics, isGood, warnings } = usePerformanceMonitor({\n * enabled: import.meta.env.DEV,\n * thresholds: { minAcceptableFps: 55 }\n * });\n * \n * return (\n * <>\n * {import.meta.env.DEV && (\n * <Html position={[0, 5, 0]}>\n * <div>FPS: {metrics.fps.toFixed(1)}</div>\n * </Html>\n * )}\n * {/* 3D content */}\n * </>\n * );\n * }\n * ```\n */\nexport function usePerformanceMonitor(\n options: UsePerformanceMonitorOptions = {}\n): PerformanceMonitorState {\n const { enabled = true, thresholds, updateInterval = 1000 } = options;\n\n const gl = useThree((state) => state.gl);\n \n // Memoize thresholds to prevent unnecessary monitor recreation\n const stableThresholds = useMemo(\n () => thresholds,\n [thresholds]\n );\n\n const monitor = useMemo(\n () => new PerformanceMonitor(stableThresholds),\n [stableThresholds]\n );\n\n const [state, setState] = useState<PerformanceMonitorState>(() => ({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n }));\n\n const lastUpdateRef = useRef(0);\n\n // Update performance monitor every frame\n useFrame(() => {\n if (!enabled) return;\n\n monitor.update(gl);\n\n // Update React state at specified interval\n const now = performance.now();\n if (now - lastUpdateRef.current >= updateInterval) {\n lastUpdateRef.current = now;\n \n setState({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n });\n }\n });\n\n // Reset monitor on unmount\n useEffect(() => {\n return () => {\n monitor.reset();\n };\n }, [monitor]);\n\n return state;\n}\n\nexport default usePerformanceMonitor;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,sBACd,UAAwC,EAAE,EACjB;CACzB,MAAM,EAAE,UAAU,MAAM,YAAY,iBAAiB,QAAS;CAE9D,MAAM,KAAK,UAAU,UAAU,MAAM,GAAG;CAGxC,MAAM,mBAAmB,cACjB,YACN,CAAC,WAAW,CACb;CAED,MAAM,UAAU,cACR,IAAI,mBAAmB,iBAAiB,EAC9C,CAAC,iBAAiB,CACnB;CAED,MAAM,CAAC,OAAO,YAAY,gBAAyC;EACjE,SAAS,QAAQ,WAAW,GAAG;EAC/B,QAAQ,QAAQ,mBAAmB;EACnC,UAAU,QAAQ,aAAa;EAChC,EAAE;CAEH,MAAM,gBAAgB,OAAO,EAAE;AAG/B,gBAAe;AACb,MAAI,CAAC,QAAS;AAEd,UAAQ,OAAO,GAAG;EAGlB,MAAM,MAAM,YAAY,KAAK;AAC7B,MAAI,MAAM,cAAc,WAAW,gBAAgB;AACjD,iBAAc,UAAU;AAExB,YAAS;IACP,SAAS,QAAQ,WAAW,GAAG;IAC/B,QAAQ,QAAQ,mBAAmB;IACnC,UAAU,QAAQ,aAAa;IAChC,CAAC;;GAEJ;AAGF,iBAAgB;AACd,eAAa;AACX,WAAQ,OAAO;;IAEhB,CAAC,QAAQ,CAAC;AAEb,QAAO"}
1
+ {"version":3,"file":"usePerformanceMonitor.js","names":[],"sources":["../../../src/utils/performance/usePerformanceMonitor.ts"],"sourcesContent":["/**\n * React hook for Three.js performance monitoring\n * \n * Integrates PerformanceMonitor with useFrame for real-time FPS tracking\n */\n\nimport { useFrame, useThree } from '@react-three/fiber';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { PerformanceMonitor, PerformanceMetrics, PerformanceThresholds } from './PerformanceMonitor';\n\nexport interface UsePerformanceMonitorOptions {\n readonly enabled?: boolean;\n readonly thresholds?: Partial<PerformanceThresholds>;\n readonly updateInterval?: number; // Update interval in ms for React state\n}\n\nexport interface PerformanceMonitorState {\n readonly metrics: PerformanceMetrics;\n readonly isGood: boolean;\n readonly warnings: readonly string[];\n}\n\n/**\n * Hook for monitoring Three.js performance in real-time\n * \n * @param options Configuration options\n * @returns Current performance state\n * \n * @example\n * ```tsx\n * function CombatScene() {\n * const { metrics, isGood, warnings } = usePerformanceMonitor({\n * enabled: import.meta.env.DEV,\n * thresholds: { minAcceptableFps: 55 }\n * });\n * \n * return (\n * <>\n * {import.meta.env.DEV && (\n * <Html position={[0, 5, 0]}>\n * <div>FPS: {metrics.fps.toFixed(1)}</div>\n * </Html>\n * )}\n * {/* 3D content */}\n * </>\n * );\n * }\n * ```\n */\nexport function usePerformanceMonitor(\n options: UsePerformanceMonitorOptions = {}\n): PerformanceMonitorState {\n const { enabled = true, thresholds, updateInterval = 1000 } = options;\n\n const gl = useThree((state) => state.gl);\n \n // Memoize thresholds to prevent unnecessary monitor recreation\n const stableThresholds = useMemo(\n () => thresholds,\n [thresholds]\n );\n\n const monitor = useMemo(\n () => new PerformanceMonitor(stableThresholds),\n [stableThresholds]\n );\n\n const [state, setState] = useState<PerformanceMonitorState>(() => ({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n }));\n\n const lastUpdateRef = useRef(0);\n\n // Update performance monitor every frame\n useFrame(() => {\n if (!enabled) return;\n\n monitor.update(gl);\n\n // Update React state at specified interval\n const now = performance.now();\n if (now - lastUpdateRef.current >= updateInterval) {\n lastUpdateRef.current = now;\n \n setState({\n metrics: monitor.getMetrics(gl),\n isGood: monitor.isPerformanceGood(),\n warnings: monitor.getWarnings(),\n });\n }\n });\n\n // Reset monitor on unmount\n useEffect(() => {\n return () => {\n monitor.reset();\n };\n }, [monitor]);\n\n return state;\n}\n\nexport default usePerformanceMonitor;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,sBACd,UAAwC,EAAE,EACjB;CACzB,MAAM,EAAE,UAAU,MAAM,YAAY,iBAAiB,QAAS;CAE9D,MAAM,KAAK,UAAU,UAAU,MAAM,GAAG;CAGxC,MAAM,mBAAmB,cACjB,YACN,CAAC,WAAW,CACb;CAED,MAAM,UAAU,cACR,IAAI,mBAAmB,iBAAiB,EAC9C,CAAC,iBAAiB,CACnB;CAED,MAAM,CAAC,OAAO,YAAY,gBAAyC;EACjE,SAAS,QAAQ,WAAW,GAAG;EAC/B,QAAQ,QAAQ,mBAAmB;EACnC,UAAU,QAAQ,aAAa;EAChC,EAAE;CAEH,MAAM,gBAAgB,OAAO,EAAE;CAG/B,eAAe;EACb,IAAI,CAAC,SAAS;EAEd,QAAQ,OAAO,GAAG;EAGlB,MAAM,MAAM,YAAY,KAAK;EAC7B,IAAI,MAAM,cAAc,WAAW,gBAAgB;GACjD,cAAc,UAAU;GAExB,SAAS;IACP,SAAS,QAAQ,WAAW,GAAG;IAC/B,QAAQ,QAAQ,mBAAmB;IACnC,UAAU,QAAQ,aAAa;IAChC,CAAC;;GAEJ;CAGF,gBAAgB;EACd,aAAa;GACX,QAAQ,OAAO;;IAEhB,CAAC,QAAQ,CAAC;CAEb,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"performanceOptimization.js","names":[],"sources":["../../src/utils/performanceOptimization.ts"],"sourcesContent":["/**\n * Performance Optimization Utilities\n * \n * Collection of utilities for optimizing React rendering performance:\n * - Memoization helpers for expensive calculations\n * - Shallow comparison utilities for React.memo\n * - GPU acceleration style helpers\n * - Performance measurement utilities\n * \n * @module utils/performanceOptimization\n * @category Performance\n * @korean 성능 최적화 유틸리티\n */\n\nimport React from 'react';\n\n/**\n * Shallow comparison for props\n * Used with React.memo for performance optimization\n * \n * @param prevProps - Previous props\n * @param nextProps - Next props\n * @returns true if props are equal (skip re-render), false otherwise\n */\nexport function shallowCompare<T extends Record<string, unknown>>(\n prevProps: T,\n nextProps: T\n): boolean {\n const prevKeys = Object.keys(prevProps);\n const nextKeys = Object.keys(nextProps);\n\n if (prevKeys.length !== nextKeys.length) {\n return false;\n }\n\n for (const key of prevKeys) {\n if (prevProps[key] !== nextProps[key]) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Create a memoized component with custom comparison\n * \n * @param component - Component to memoize\n * @param compareKeys - Top-level prop keys to compare for equality\n * @returns Memoized component\n * \n * @example\n * ```tsx\n * // If PlayerHUD receives props like { playerHealth, playerStamina }\n * const MemoizedHUD = memoizeComponent(PlayerHUD, ['playerHealth', 'playerStamina']);\n * ```\n */\nexport function memoizeComponent<P extends Record<string, unknown>>(\n component: React.FC<P>,\n compareKeys?: (keyof P)[]\n): React.NamedExoticComponent<P> {\n if (!compareKeys || compareKeys.length === 0) {\n return React.memo(component);\n }\n\n return React.memo(component, (prevProps, nextProps) => {\n for (const key of compareKeys) {\n if (prevProps[key] !== nextProps[key]) {\n return false; // Props changed, re-render\n }\n }\n return true; // Props same, skip re-render\n });\n}\n\n/**\n * GPU acceleration styles\n * Forces GPU layer creation for smooth animations\n */\nexport const GPU_ACCELERATION_STYLES = {\n /**\n * Force GPU layer with translateZ\n */\n transform: 'translateZ(0)',\n \n /**\n * Enable hardware acceleration\n */\n backfaceVisibility: 'hidden' as const,\n \n /**\n * Hint browser about upcoming changes\n */\n willChange: 'transform, opacity' as const,\n} as const;\n\n/**\n * Supported CSS properties for GPU-accelerated transitions\n * 제한된 전환 대상 속성 (transform, opacity)\n */\nexport type GPUTransitionProperty = 'transform' | 'opacity';\n\n/**\n * Create a valid GPU-accelerated CSS transition string\n * \n * Ensures correct syntax such as:\n * \"transform 0.2s ease, opacity 0.2s ease\"\n * instead of invalid:\n * \"transform, opacity 0.2s ease\"\n * \n * @param properties - CSS properties to animate (default: ['transform', 'opacity'])\n * @param duration - Transition duration (default: '0.2s')\n * @param timingFunction - Timing function (default: 'ease')\n * @returns Valid CSS transition shorthand string\n */\nexport function createGPUAcceleratedTransition(\n properties: readonly GPUTransitionProperty[] = ['transform', 'opacity'],\n duration: string = '0.2s',\n timingFunction: string = 'ease'\n): string {\n return properties\n .map((property) => `${property} ${duration} ${timingFunction}`)\n .join(', ');\n}\n\n/**\n * Apply GPU acceleration to CSS-in-JS style object\n * \n * @param styles - Base styles\n * @returns Styles with GPU acceleration\n */\nexport function withGPUAcceleration<T extends React.CSSProperties>(\n styles: T\n): T & typeof GPU_ACCELERATION_STYLES {\n return {\n ...styles,\n ...GPU_ACCELERATION_STYLES,\n };\n}\n\n/**\n * Performance-optimized animation styles\n * Uses only transform and opacity for 60fps animations\n * \n * @param translateX - X translation in px\n * @param translateY - Y translation in px\n * @param opacity - Opacity (0-1)\n * @param scale - Scale factor (default: 1)\n * @returns Optimized style object\n */\nexport function optimizedAnimationStyle(\n translateX = 0,\n translateY = 0,\n opacity = 1,\n scale = 1\n): React.CSSProperties {\n return {\n transform: `translate3d(${translateX}px, ${translateY}px, 0) scale(${scale})`,\n opacity,\n willChange: 'transform, opacity',\n backfaceVisibility: 'hidden',\n };\n}\n\n/**\n * Measure component render time\n * Only runs in development mode\n * \n * @param componentName - Name of component being measured\n * @param callback - Function to measure\n * @returns Result of callback\n */\nexport function measureRender<T>(\n componentName: string,\n callback: () => T\n): T {\n if (import.meta.env.DEV) {\n const start = performance.now();\n const result = callback();\n const end = performance.now();\n const duration = end - start;\n \n if (duration > 16.67) { // Slower than 60fps frame budget\n console.warn(\n `[Performance] ${componentName} render took ${duration.toFixed(2)}ms (>16.67ms budget)`\n );\n }\n \n return result;\n }\n \n return callback();\n}\n\n/**\n * Create a render counter for debugging\n * Tracks how many times a component renders\n * \n * Note: This is intentionally commented out due to React compiler\n * limitations with ref mutations during render. For production use,\n * consider using React DevTools Profiler instead.\n * \n * @param componentName - Name of component\n * @returns Render count (always returns 0)\n */\nexport function useRenderCount(componentName: string): number {\n // Disabled due to ESLint react-compiler rules\n // See: https://react.dev/reference/react/useRef#caveats\n if (import.meta.env.DEV) {\n console.log(`[Render] ${componentName} is rendering (counter disabled)`);\n }\n return 0;\n \n /* Original implementation disabled due to lint errors\n const renderCountRef = React.useRef(0);\n renderCountRef.current += 1;\n \n React.useEffect(() => {\n if (import.meta.env.DEV) {\n console.log(`[Render] ${componentName} rendered ${renderCountRef.current} times`);\n }\n }, [componentName]);\n \n return renderCountRef.current;\n */\n}\n\n/**\n * Batch state updates to reduce re-renders\n * \n * @param updates - Array of state update functions\n */\nexport function batchUpdates(updates: (() => void)[]): void {\n React.startTransition(() => {\n updates.forEach(update => update());\n });\n}\n\n/**\n * Check if selected object properties have changed using shallow/reference equality\n *\n * This compares only the top-level values at the specified keys using strict\n * equality (`===`). If a key refers to an object or array, only the reference\n * is compared, not its contents. This is suitable for React.memo-style\n * performance optimizations where prop references are kept stable.\n * \n * @param prev - Previous object\n * @param next - Next object\n * @param keys - Keys to check with shallow/reference equality\n * @returns true if any specified key value changed by reference\n */\nexport function hasPropsChanged<T extends Record<string, unknown>>(\n prev: T,\n next: T,\n keys: (keyof T)[]\n): boolean {\n for (const key of keys) {\n if (prev[key] !== next[key]) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Create a stable callback reference that doesn't change between renders\n * Similar to useCallback but with automatic dependency detection\n * \n * @param callback - Callback function\n * @returns Stable callback reference\n */\nexport function useStableCallback<T extends (...args: never[]) => unknown>(\n callback: T\n): T {\n const callbackRef = React.useRef(callback);\n \n // Update ref on each render\n React.useLayoutEffect(() => {\n callbackRef.current = callback;\n });\n \n // Return stable function that calls latest callback\n const stableCallback = React.useCallback(\n (...args: Parameters<T>) => callbackRef.current(...args),\n []\n ) as T;\n \n return stableCallback;\n}\n"],"mappings":";;;;;;AA+EA,IAAa,0BAA0B;;;;CAIrC,WAAW;;;;CAKX,oBAAoB;;;;CAKpB,YAAY;CACb;;;;;;;AAqCD,SAAgB,oBACd,QACoC;AACpC,QAAO;EACL,GAAG;EACH,GAAG;EACJ"}
1
+ {"version":3,"file":"performanceOptimization.js","names":[],"sources":["../../src/utils/performanceOptimization.ts"],"sourcesContent":["/**\n * Performance Optimization Utilities\n * \n * Collection of utilities for optimizing React rendering performance:\n * - Memoization helpers for expensive calculations\n * - Shallow comparison utilities for React.memo\n * - GPU acceleration style helpers\n * - Performance measurement utilities\n * \n * @module utils/performanceOptimization\n * @category Performance\n * @korean 성능 최적화 유틸리티\n */\n\nimport React from 'react';\n\n/**\n * Shallow comparison for props\n * Used with React.memo for performance optimization\n * \n * @param prevProps - Previous props\n * @param nextProps - Next props\n * @returns true if props are equal (skip re-render), false otherwise\n */\nexport function shallowCompare<T extends Record<string, unknown>>(\n prevProps: T,\n nextProps: T\n): boolean {\n const prevKeys = Object.keys(prevProps);\n const nextKeys = Object.keys(nextProps);\n\n if (prevKeys.length !== nextKeys.length) {\n return false;\n }\n\n for (const key of prevKeys) {\n if (prevProps[key] !== nextProps[key]) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Create a memoized component with custom comparison\n * \n * @param component - Component to memoize\n * @param compareKeys - Top-level prop keys to compare for equality\n * @returns Memoized component\n * \n * @example\n * ```tsx\n * // If PlayerHUD receives props like { playerHealth, playerStamina }\n * const MemoizedHUD = memoizeComponent(PlayerHUD, ['playerHealth', 'playerStamina']);\n * ```\n */\nexport function memoizeComponent<P extends Record<string, unknown>>(\n component: React.FC<P>,\n compareKeys?: (keyof P)[]\n): React.NamedExoticComponent<P> {\n if (!compareKeys || compareKeys.length === 0) {\n return React.memo(component);\n }\n\n return React.memo(component, (prevProps, nextProps) => {\n for (const key of compareKeys) {\n if (prevProps[key] !== nextProps[key]) {\n return false; // Props changed, re-render\n }\n }\n return true; // Props same, skip re-render\n });\n}\n\n/**\n * GPU acceleration styles\n * Forces GPU layer creation for smooth animations\n */\nexport const GPU_ACCELERATION_STYLES = {\n /**\n * Force GPU layer with translateZ\n */\n transform: 'translateZ(0)',\n \n /**\n * Enable hardware acceleration\n */\n backfaceVisibility: 'hidden' as const,\n \n /**\n * Hint browser about upcoming changes\n */\n willChange: 'transform, opacity' as const,\n} as const;\n\n/**\n * Supported CSS properties for GPU-accelerated transitions\n * 제한된 전환 대상 속성 (transform, opacity)\n */\nexport type GPUTransitionProperty = 'transform' | 'opacity';\n\n/**\n * Create a valid GPU-accelerated CSS transition string\n * \n * Ensures correct syntax such as:\n * \"transform 0.2s ease, opacity 0.2s ease\"\n * instead of invalid:\n * \"transform, opacity 0.2s ease\"\n * \n * @param properties - CSS properties to animate (default: ['transform', 'opacity'])\n * @param duration - Transition duration (default: '0.2s')\n * @param timingFunction - Timing function (default: 'ease')\n * @returns Valid CSS transition shorthand string\n */\nexport function createGPUAcceleratedTransition(\n properties: readonly GPUTransitionProperty[] = ['transform', 'opacity'],\n duration: string = '0.2s',\n timingFunction: string = 'ease'\n): string {\n return properties\n .map((property) => `${property} ${duration} ${timingFunction}`)\n .join(', ');\n}\n\n/**\n * Apply GPU acceleration to CSS-in-JS style object\n * \n * @param styles - Base styles\n * @returns Styles with GPU acceleration\n */\nexport function withGPUAcceleration<T extends React.CSSProperties>(\n styles: T\n): T & typeof GPU_ACCELERATION_STYLES {\n return {\n ...styles,\n ...GPU_ACCELERATION_STYLES,\n };\n}\n\n/**\n * Performance-optimized animation styles\n * Uses only transform and opacity for 60fps animations\n * \n * @param translateX - X translation in px\n * @param translateY - Y translation in px\n * @param opacity - Opacity (0-1)\n * @param scale - Scale factor (default: 1)\n * @returns Optimized style object\n */\nexport function optimizedAnimationStyle(\n translateX = 0,\n translateY = 0,\n opacity = 1,\n scale = 1\n): React.CSSProperties {\n return {\n transform: `translate3d(${translateX}px, ${translateY}px, 0) scale(${scale})`,\n opacity,\n willChange: 'transform, opacity',\n backfaceVisibility: 'hidden',\n };\n}\n\n/**\n * Measure component render time\n * Only runs in development mode\n * \n * @param componentName - Name of component being measured\n * @param callback - Function to measure\n * @returns Result of callback\n */\nexport function measureRender<T>(\n componentName: string,\n callback: () => T\n): T {\n if (import.meta.env.DEV) {\n const start = performance.now();\n const result = callback();\n const end = performance.now();\n const duration = end - start;\n \n if (duration > 16.67) { // Slower than 60fps frame budget\n console.warn(\n `[Performance] ${componentName} render took ${duration.toFixed(2)}ms (>16.67ms budget)`\n );\n }\n \n return result;\n }\n \n return callback();\n}\n\n/**\n * Create a render counter for debugging\n * Tracks how many times a component renders\n * \n * Note: This is intentionally commented out due to React compiler\n * limitations with ref mutations during render. For production use,\n * consider using React DevTools Profiler instead.\n * \n * @param componentName - Name of component\n * @returns Render count (always returns 0)\n */\nexport function useRenderCount(componentName: string): number {\n // Disabled due to ESLint react-compiler rules\n // See: https://react.dev/reference/react/useRef#caveats\n if (import.meta.env.DEV) {\n console.log(`[Render] ${componentName} is rendering (counter disabled)`);\n }\n return 0;\n \n /* Original implementation disabled due to lint errors\n const renderCountRef = React.useRef(0);\n renderCountRef.current += 1;\n \n React.useEffect(() => {\n if (import.meta.env.DEV) {\n console.log(`[Render] ${componentName} rendered ${renderCountRef.current} times`);\n }\n }, [componentName]);\n \n return renderCountRef.current;\n */\n}\n\n/**\n * Batch state updates to reduce re-renders\n * \n * @param updates - Array of state update functions\n */\nexport function batchUpdates(updates: (() => void)[]): void {\n React.startTransition(() => {\n updates.forEach(update => update());\n });\n}\n\n/**\n * Check if selected object properties have changed using shallow/reference equality\n *\n * This compares only the top-level values at the specified keys using strict\n * equality (`===`). If a key refers to an object or array, only the reference\n * is compared, not its contents. This is suitable for React.memo-style\n * performance optimizations where prop references are kept stable.\n * \n * @param prev - Previous object\n * @param next - Next object\n * @param keys - Keys to check with shallow/reference equality\n * @returns true if any specified key value changed by reference\n */\nexport function hasPropsChanged<T extends Record<string, unknown>>(\n prev: T,\n next: T,\n keys: (keyof T)[]\n): boolean {\n for (const key of keys) {\n if (prev[key] !== next[key]) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Create a stable callback reference that doesn't change between renders\n * Similar to useCallback but with automatic dependency detection\n * \n * @param callback - Callback function\n * @returns Stable callback reference\n */\nexport function useStableCallback<T extends (...args: never[]) => unknown>(\n callback: T\n): T {\n const callbackRef = React.useRef(callback);\n \n // Update ref on each render\n React.useLayoutEffect(() => {\n callbackRef.current = callback;\n });\n \n // Return stable function that calls latest callback\n const stableCallback = React.useCallback(\n (...args: Parameters<T>) => callbackRef.current(...args),\n []\n ) as T;\n \n return stableCallback;\n}\n"],"mappings":";;;;;;AA+EA,IAAa,0BAA0B;;;;CAIrC,WAAW;;;;CAKX,oBAAoB;;;;CAKpB,YAAY;CACb;;;;;;;AAqCD,SAAgB,oBACd,QACoC;CACpC,OAAO;EACL,GAAG;EACH,GAAG;EACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"player3DHelpers.js","names":[],"sources":["../../src/utils/player3DHelpers.ts"],"sourcesContent":["/**\n * Utility functions for Player3D component integration\n *\n * Converts PlayerState from combat system to Player3DUnifiedProps for rendering\n * with SkeletalPlayer3D (28-bone articulated body model).\n *\n * @module utils/player3DHelpers\n * @category Utilities\n * @korean 플레이어3D도우미\n */\n\nimport type { PlayerState } from \"../systems\";\nimport type { AnimationState } from \"../systems/animation/core/types\";\nimport type {\n BalanceState,\n Player3DUnifiedProps,\n PlayerAnimation,\n} from \"../types/player-visual\";\n\n/**\n * Static mapping from AnimationState to PlayerAnimation\n *\n * Stance guard animations map to \"idle\" since SkeletalPlayer3D\n * will handle the stance-specific guard rendering.\n * Tactical steps now use dedicated step animations with guard maintenance.\n *\n * @korean 애니메이션상태맵\n */\nconst ANIMATION_STATE_MAP: Record<AnimationState, PlayerAnimation> = {\n idle: \"idle\",\n walk: \"walk\",\n run: \"run\", // Now uses dedicated RUN_ANIMATION from BasicAnimations\n attack: \"attack\",\n defend: \"defend\",\n // Defensive animations (방어 애니메이션) - map to defend with variations handled by skeletal system\n defend_block_success: \"defend\",\n defend_parry: \"defend\",\n defend_guard_break: \"defend\",\n defend_recovery: \"defend\",\n hit: \"hit\",\n stance_change: \"stance_change\",\n stance_side_switch: \"stance_change\", // Map to stance_change animation (mirroring guard)\n ko: \"death\", // Map ko to death\n // Stance guard animations map to stance-specific idle animations with proper biomechanics\n stance_guard_geon: \"stance_geon\",\n stance_guard_tae: \"stance_tae\",\n stance_guard_li: \"stance_li\",\n stance_guard_jin: \"stance_jin\",\n stance_guard_son: \"stance_son\",\n stance_guard_gam: \"stance_gam\",\n stance_guard_gan: \"stance_gan\",\n stance_guard_gon: \"stance_gon\",\n // Tactical step animations now map to dedicated step animations\n step_forward: \"step_forward\",\n step_back: \"step_back\",\n step_left: \"step_left\",\n step_right: \"step_right\",\n step_forward_left: \"step_forward_left\",\n step_forward_right: \"step_forward_right\",\n step_back_left: \"step_back_left\",\n step_back_right: \"step_back_right\",\n // Fall animations: Now using dedicated FALL_*_ANIMATION from BasicAnimations\n fall_forward: \"fall_forward\",\n fall_backward: \"fall_backward\",\n fall_side_left: \"fall_side_left\",\n fall_side_right: \"fall_side_right\",\n // Ground states map to idle with minimal movement\n ground_prone: \"idle\",\n ground_supine: \"idle\",\n ground_side_left: \"idle\",\n ground_side_right: \"idle\",\n // 180-degree turn animations map to stance_change (body pivot animation)\n turn_left: \"stance_change\",\n turn_right: \"stance_change\",\n // Footwork patterns (보법) - Korean martial arts specialized footwork\n footwork_circular_left: \"walk\", // Lateral movement\n footwork_circular_right: \"walk\",\n footwork_pivot_left: \"walk\", // Rotation movement\n footwork_pivot_right: \"walk\",\n footwork_slide_forward: \"walk\", // Sliding movement\n footwork_slide_back: \"walk\",\n footwork_slide_left: \"walk\",\n footwork_slide_right: \"walk\",\n footwork_shuffle: \"walk\", // Quick adjustment\n // Recovery animations (기상 애니메이션) - Getting up from ground states\n // Map to idle for now, custom 3D recovery animations will be added in future\n recovery_prone_standup: \"idle\",\n recovery_supine_standup: \"idle\",\n recovery_roll: \"walk\", // Rolling motion approximated by walk\n recovery_defensive: \"defend\", // Guarded getup approximated by defend\n // Grappling animations (잡기 애니메이션) - map to attack/defend based on action\n grapple_entry: \"attack\", // Initiating grab uses attack animation\n grapple_control: \"defend\", // Maintaining control uses defensive stance\n grapple_struggle: \"hit\", // Struggling escape uses hit reaction\n grapple_escape: \"attack\", // Successful escape uses attack burst\n};\n\n/**\n * Convert AnimationState to PlayerAnimation\n *\n * Maps the animation system's state types to SkeletalPlayer3D's animation types.\n *\n * @param animState - Animation state from the animation system\n * @returns Corresponding PlayerAnimation type\n * @korean 애니메이션상태변환\n */\nexport function animationStateToPlayerAnimation(\n animState: AnimationState\n): PlayerAnimation {\n return ANIMATION_STATE_MAP[animState];\n}\n\n/**\n * Convert balance number (0-100) to BalanceState enum\n *\n * @param balance - Balance value from PlayerState (0-100)\n * @returns BalanceState enum value\n * @korean 균형상태변환\n */\nexport function getBalanceState(balance: number): BalanceState {\n if (balance >= 80) return \"READY\";\n if (balance >= 50) return \"SHAKEN\";\n if (balance >= 20) return \"VULNERABLE\";\n return \"HELPLESS\";\n}\n\n/**\n * Get current animation state from PlayerState\n *\n * Returns stance-specific idle animations when player is in idle/recovering state.\n * This ensures each trigram stance displays the correct guard pose with proper\n * leg positioning and breathing animation.\n *\n * @param player - Current player state\n * @returns PlayerAnimation enum value (stance-specific for idle states)\n * @korean 애니메이션상태가져오기\n */\nexport function getPlayerAnimation(player: PlayerState): PlayerAnimation {\n if (player.isStunned) return \"hit\";\n if (player.isBlocking) return \"defend\";\n if (player.isCountering) return \"counter\";\n\n // Check combat state (CombatState enum values are lowercase strings)\n switch (player.combatState) {\n case \"attacking\":\n return \"attack\";\n case \"defending\":\n return \"defend\";\n case \"stunned\":\n return \"hit\";\n case \"recovering\":\n case \"idle\":\n default:\n // Use stance-specific idle animation for proper guard pose\n // This ensures correct leg positioning for each trigram stance\n return `stance_${player.currentStance}` as PlayerAnimation;\n }\n}\n\n/**\n * Converts PlayerState to Player3DUnifiedProps for visual rendering.\n *\n * Note: This function converts base PlayerState properties used in combat.\n * Training-specific stats (misses, accuracy, comboCount) are optional in PlayerState\n * and handled separately in training contexts.\n *\n * @param player - The player state to convert\n * @param position - 3D position [x, y, z]\n * @param rotation - Rotation in radians\n * @param options - Display and behavior options\n * @returns Props for SkeletalPlayer3D component (28-bone articulated body model)\n * @korean 플레이어상태변환\n *\n * @example\n * ```tsx\n * const playerProps = convertPlayerStateToProps(\n * playerState,\n * [-3, 0, 0],\n * 0,\n * { isMobile: false, showVitalPoints: false }\n * );\n *\n * <SkeletalPlayer3D {...playerProps} />\n * ```\n */\nexport function convertPlayerStateToProps(\n player: PlayerState,\n position: [number, number, number],\n rotation: number,\n options: {\n readonly isMobile?: boolean;\n readonly facing?: \"left\" | \"right\";\n readonly scale?: number;\n readonly showDetails?: boolean;\n readonly showHealthBar?: boolean;\n readonly showStanceIndicator?: boolean;\n readonly onAnimationComplete?: () => void;\n // Facial expression options - 얼굴 표정 옵션\n readonly enableFacialExpressions?: boolean;\n readonly enableEyeTracking?: boolean;\n readonly opponentPosition?: [number, number, number];\n } = {}\n): Player3DUnifiedProps {\n return {\n playerId: player.id,\n archetype: player.archetype,\n stance: player.currentStance,\n position,\n rotation,\n\n // Health and resources\n health: player.health,\n maxHealth: player.maxHealth,\n stamina: player.stamina,\n ki: player.ki,\n\n // Combat states\n pain: player.pain,\n balance: getBalanceState(player.balance),\n consciousness: player.consciousness,\n\n // Combat flags\n isBlocking: player.isBlocking,\n isStunned: player.isStunned,\n isCountering: player.isCountering,\n\n // Animation (derived from combat state)\n currentAnimation: getPlayerAnimation(player),\n\n // Display options\n name: player.name,\n isMobile: options.isMobile ?? false,\n facing: options.facing ?? \"right\",\n scale: options.scale ?? 1,\n showDetails: options.showDetails,\n showHealthBar: options.showHealthBar,\n showStanceIndicator: options.showStanceIndicator,\n onAnimationComplete: options.onAnimationComplete,\n\n // Facial expression options - 얼굴 표정 옵션\n enableFacialExpressions: options.enableFacialExpressions ?? false,\n enableEyeTracking: options.enableEyeTracking ?? true,\n opponentPosition: options.opponentPosition,\n };\n}\n"],"mappings":";;;;;;;;;;AA4BA,IAAM,sBAA+D;CACnE,MAAM;CACN,MAAM;CACN,KAAK;CACL,QAAQ;CACR,QAAQ;CAER,sBAAsB;CACtB,cAAc;CACd,oBAAoB;CACpB,iBAAiB;CACjB,KAAK;CACL,eAAe;CACf,oBAAoB;CACpB,IAAI;CAEJ,mBAAmB;CACnB,kBAAkB;CAClB,iBAAiB;CACjB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAElB,cAAc;CACd,WAAW;CACX,WAAW;CACX,YAAY;CACZ,mBAAmB;CACnB,oBAAoB;CACpB,gBAAgB;CAChB,iBAAiB;CAEjB,cAAc;CACd,eAAe;CACf,gBAAgB;CAChB,iBAAiB;CAEjB,cAAc;CACd,eAAe;CACf,kBAAkB;CAClB,mBAAmB;CAEnB,WAAW;CACX,YAAY;CAEZ,wBAAwB;CACxB,yBAAyB;CACzB,qBAAqB;CACrB,sBAAsB;CACtB,wBAAwB;CACxB,qBAAqB;CACrB,qBAAqB;CACrB,sBAAsB;CACtB,kBAAkB;CAGlB,wBAAwB;CACxB,yBAAyB;CACzB,eAAe;CACf,oBAAoB;CAEpB,eAAe;CACf,iBAAiB;CACjB,kBAAkB;CAClB,gBAAgB;CACjB;;;;;;;;;;AAWD,SAAgB,gCACd,WACiB;AACjB,QAAO,oBAAoB;;;;;;;;;AAU7B,SAAgB,gBAAgB,SAA+B;AAC7D,KAAI,WAAW,GAAI,QAAO;AAC1B,KAAI,WAAW,GAAI,QAAO;AAC1B,KAAI,WAAW,GAAI,QAAO;AAC1B,QAAO;;;;;;;;;;;;;AAcT,SAAgB,mBAAmB,QAAsC;AACvE,KAAI,OAAO,UAAW,QAAO;AAC7B,KAAI,OAAO,WAAY,QAAO;AAC9B,KAAI,OAAO,aAAc,QAAO;AAGhC,SAAQ,OAAO,aAAf;EACE,KAAK,YACH,QAAO;EACT,KAAK,YACH,QAAO;EACT,KAAK,UACH,QAAO;EAGT,QAGE,QAAO,UAAU,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B9B,SAAgB,0BACd,QACA,UACA,UACA,UAYI,EAAE,EACgB;AACtB,QAAO;EACL,UAAU,OAAO;EACjB,WAAW,OAAO;EAClB,QAAQ,OAAO;EACf;EACA;EAGA,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,IAAI,OAAO;EAGX,MAAM,OAAO;EACb,SAAS,gBAAgB,OAAO,QAAQ;EACxC,eAAe,OAAO;EAGtB,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,cAAc,OAAO;EAGrB,kBAAkB,mBAAmB,OAAO;EAG5C,MAAM,OAAO;EACb,UAAU,QAAQ,YAAY;EAC9B,QAAQ,QAAQ,UAAU;EAC1B,OAAO,QAAQ,SAAS;EACxB,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,qBAAqB,QAAQ;EAC7B,qBAAqB,QAAQ;EAG7B,yBAAyB,QAAQ,2BAA2B;EAC5D,mBAAmB,QAAQ,qBAAqB;EAChD,kBAAkB,QAAQ;EAC3B"}
1
+ {"version":3,"file":"player3DHelpers.js","names":[],"sources":["../../src/utils/player3DHelpers.ts"],"sourcesContent":["/**\n * Utility functions for Player3D component integration\n *\n * Converts PlayerState from combat system to Player3DUnifiedProps for rendering\n * with SkeletalPlayer3D (28-bone articulated body model).\n *\n * @module utils/player3DHelpers\n * @category Utilities\n * @korean 플레이어3D도우미\n */\n\nimport type { PlayerState } from \"../systems\";\nimport type { AnimationState } from \"../systems/animation/core/types\";\nimport type {\n BalanceState,\n Player3DUnifiedProps,\n PlayerAnimation,\n} from \"../types/player-visual\";\n\n/**\n * Static mapping from AnimationState to PlayerAnimation\n *\n * Stance guard animations map to \"idle\" since SkeletalPlayer3D\n * will handle the stance-specific guard rendering.\n * Tactical steps now use dedicated step animations with guard maintenance.\n *\n * @korean 애니메이션상태맵\n */\nconst ANIMATION_STATE_MAP: Record<AnimationState, PlayerAnimation> = {\n idle: \"idle\",\n walk: \"walk\",\n run: \"run\", // Now uses dedicated RUN_ANIMATION from BasicAnimations\n attack: \"attack\",\n defend: \"defend\",\n // Defensive animations (방어 애니메이션) - map to defend with variations handled by skeletal system\n defend_block_success: \"defend\",\n defend_parry: \"defend\",\n defend_guard_break: \"defend\",\n defend_recovery: \"defend\",\n hit: \"hit\",\n stance_change: \"stance_change\",\n stance_side_switch: \"stance_change\", // Map to stance_change animation (mirroring guard)\n ko: \"death\", // Map ko to death\n // Stance guard animations map to stance-specific idle animations with proper biomechanics\n stance_guard_geon: \"stance_geon\",\n stance_guard_tae: \"stance_tae\",\n stance_guard_li: \"stance_li\",\n stance_guard_jin: \"stance_jin\",\n stance_guard_son: \"stance_son\",\n stance_guard_gam: \"stance_gam\",\n stance_guard_gan: \"stance_gan\",\n stance_guard_gon: \"stance_gon\",\n // Tactical step animations now map to dedicated step animations\n step_forward: \"step_forward\",\n step_back: \"step_back\",\n step_left: \"step_left\",\n step_right: \"step_right\",\n step_forward_left: \"step_forward_left\",\n step_forward_right: \"step_forward_right\",\n step_back_left: \"step_back_left\",\n step_back_right: \"step_back_right\",\n // Fall animations: Now using dedicated FALL_*_ANIMATION from BasicAnimations\n fall_forward: \"fall_forward\",\n fall_backward: \"fall_backward\",\n fall_side_left: \"fall_side_left\",\n fall_side_right: \"fall_side_right\",\n // Ground states map to idle with minimal movement\n ground_prone: \"idle\",\n ground_supine: \"idle\",\n ground_side_left: \"idle\",\n ground_side_right: \"idle\",\n // 180-degree turn animations map to stance_change (body pivot animation)\n turn_left: \"stance_change\",\n turn_right: \"stance_change\",\n // Footwork patterns (보법) - Korean martial arts specialized footwork\n footwork_circular_left: \"walk\", // Lateral movement\n footwork_circular_right: \"walk\",\n footwork_pivot_left: \"walk\", // Rotation movement\n footwork_pivot_right: \"walk\",\n footwork_slide_forward: \"walk\", // Sliding movement\n footwork_slide_back: \"walk\",\n footwork_slide_left: \"walk\",\n footwork_slide_right: \"walk\",\n footwork_shuffle: \"walk\", // Quick adjustment\n // Recovery animations (기상 애니메이션) - Getting up from ground states\n // Map to idle for now, custom 3D recovery animations will be added in future\n recovery_prone_standup: \"idle\",\n recovery_supine_standup: \"idle\",\n recovery_roll: \"walk\", // Rolling motion approximated by walk\n recovery_defensive: \"defend\", // Guarded getup approximated by defend\n // Grappling animations (잡기 애니메이션) - map to attack/defend based on action\n grapple_entry: \"attack\", // Initiating grab uses attack animation\n grapple_control: \"defend\", // Maintaining control uses defensive stance\n grapple_struggle: \"hit\", // Struggling escape uses hit reaction\n grapple_escape: \"attack\", // Successful escape uses attack burst\n};\n\n/**\n * Convert AnimationState to PlayerAnimation\n *\n * Maps the animation system's state types to SkeletalPlayer3D's animation types.\n *\n * @param animState - Animation state from the animation system\n * @returns Corresponding PlayerAnimation type\n * @korean 애니메이션상태변환\n */\nexport function animationStateToPlayerAnimation(\n animState: AnimationState\n): PlayerAnimation {\n return ANIMATION_STATE_MAP[animState];\n}\n\n/**\n * Convert balance number (0-100) to BalanceState enum\n *\n * @param balance - Balance value from PlayerState (0-100)\n * @returns BalanceState enum value\n * @korean 균형상태변환\n */\nexport function getBalanceState(balance: number): BalanceState {\n if (balance >= 80) return \"READY\";\n if (balance >= 50) return \"SHAKEN\";\n if (balance >= 20) return \"VULNERABLE\";\n return \"HELPLESS\";\n}\n\n/**\n * Get current animation state from PlayerState\n *\n * Returns stance-specific idle animations when player is in idle/recovering state.\n * This ensures each trigram stance displays the correct guard pose with proper\n * leg positioning and breathing animation.\n *\n * @param player - Current player state\n * @returns PlayerAnimation enum value (stance-specific for idle states)\n * @korean 애니메이션상태가져오기\n */\nexport function getPlayerAnimation(player: PlayerState): PlayerAnimation {\n if (player.isStunned) return \"hit\";\n if (player.isBlocking) return \"defend\";\n if (player.isCountering) return \"counter\";\n\n // Check combat state (CombatState enum values are lowercase strings)\n switch (player.combatState) {\n case \"attacking\":\n return \"attack\";\n case \"defending\":\n return \"defend\";\n case \"stunned\":\n return \"hit\";\n case \"recovering\":\n case \"idle\":\n default:\n // Use stance-specific idle animation for proper guard pose\n // This ensures correct leg positioning for each trigram stance\n return `stance_${player.currentStance}` as PlayerAnimation;\n }\n}\n\n/**\n * Converts PlayerState to Player3DUnifiedProps for visual rendering.\n *\n * Note: This function converts base PlayerState properties used in combat.\n * Training-specific stats (misses, accuracy, comboCount) are optional in PlayerState\n * and handled separately in training contexts.\n *\n * @param player - The player state to convert\n * @param position - 3D position [x, y, z]\n * @param rotation - Rotation in radians\n * @param options - Display and behavior options\n * @returns Props for SkeletalPlayer3D component (28-bone articulated body model)\n * @korean 플레이어상태변환\n *\n * @example\n * ```tsx\n * const playerProps = convertPlayerStateToProps(\n * playerState,\n * [-3, 0, 0],\n * 0,\n * { isMobile: false, showVitalPoints: false }\n * );\n *\n * <SkeletalPlayer3D {...playerProps} />\n * ```\n */\nexport function convertPlayerStateToProps(\n player: PlayerState,\n position: [number, number, number],\n rotation: number,\n options: {\n readonly isMobile?: boolean;\n readonly facing?: \"left\" | \"right\";\n readonly scale?: number;\n readonly showDetails?: boolean;\n readonly showHealthBar?: boolean;\n readonly showStanceIndicator?: boolean;\n readonly onAnimationComplete?: () => void;\n // Facial expression options - 얼굴 표정 옵션\n readonly enableFacialExpressions?: boolean;\n readonly enableEyeTracking?: boolean;\n readonly opponentPosition?: [number, number, number];\n } = {}\n): Player3DUnifiedProps {\n return {\n playerId: player.id,\n archetype: player.archetype,\n stance: player.currentStance,\n position,\n rotation,\n\n // Health and resources\n health: player.health,\n maxHealth: player.maxHealth,\n stamina: player.stamina,\n ki: player.ki,\n\n // Combat states\n pain: player.pain,\n balance: getBalanceState(player.balance),\n consciousness: player.consciousness,\n\n // Combat flags\n isBlocking: player.isBlocking,\n isStunned: player.isStunned,\n isCountering: player.isCountering,\n\n // Animation (derived from combat state)\n currentAnimation: getPlayerAnimation(player),\n\n // Display options\n name: player.name,\n isMobile: options.isMobile ?? false,\n facing: options.facing ?? \"right\",\n scale: options.scale ?? 1,\n showDetails: options.showDetails,\n showHealthBar: options.showHealthBar,\n showStanceIndicator: options.showStanceIndicator,\n onAnimationComplete: options.onAnimationComplete,\n\n // Facial expression options - 얼굴 표정 옵션\n enableFacialExpressions: options.enableFacialExpressions ?? false,\n enableEyeTracking: options.enableEyeTracking ?? true,\n opponentPosition: options.opponentPosition,\n };\n}\n"],"mappings":";;;;;;;;;;AA4BA,IAAM,sBAA+D;CACnE,MAAM;CACN,MAAM;CACN,KAAK;CACL,QAAQ;CACR,QAAQ;CAER,sBAAsB;CACtB,cAAc;CACd,oBAAoB;CACpB,iBAAiB;CACjB,KAAK;CACL,eAAe;CACf,oBAAoB;CACpB,IAAI;CAEJ,mBAAmB;CACnB,kBAAkB;CAClB,iBAAiB;CACjB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAElB,cAAc;CACd,WAAW;CACX,WAAW;CACX,YAAY;CACZ,mBAAmB;CACnB,oBAAoB;CACpB,gBAAgB;CAChB,iBAAiB;CAEjB,cAAc;CACd,eAAe;CACf,gBAAgB;CAChB,iBAAiB;CAEjB,cAAc;CACd,eAAe;CACf,kBAAkB;CAClB,mBAAmB;CAEnB,WAAW;CACX,YAAY;CAEZ,wBAAwB;CACxB,yBAAyB;CACzB,qBAAqB;CACrB,sBAAsB;CACtB,wBAAwB;CACxB,qBAAqB;CACrB,qBAAqB;CACrB,sBAAsB;CACtB,kBAAkB;CAGlB,wBAAwB;CACxB,yBAAyB;CACzB,eAAe;CACf,oBAAoB;CAEpB,eAAe;CACf,iBAAiB;CACjB,kBAAkB;CAClB,gBAAgB;CACjB;;;;;;;;;;AAWD,SAAgB,gCACd,WACiB;CACjB,OAAO,oBAAoB;;;;;;;;;AAU7B,SAAgB,gBAAgB,SAA+B;CAC7D,IAAI,WAAW,IAAI,OAAO;CAC1B,IAAI,WAAW,IAAI,OAAO;CAC1B,IAAI,WAAW,IAAI,OAAO;CAC1B,OAAO;;;;;;;;;;;;;AAcT,SAAgB,mBAAmB,QAAsC;CACvE,IAAI,OAAO,WAAW,OAAO;CAC7B,IAAI,OAAO,YAAY,OAAO;CAC9B,IAAI,OAAO,cAAc,OAAO;CAGhC,QAAQ,OAAO,aAAf;EACE,KAAK,aACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK,WACH,OAAO;EAGT,SAGE,OAAO,UAAU,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B9B,SAAgB,0BACd,QACA,UACA,UACA,UAYI,EAAE,EACgB;CACtB,OAAO;EACL,UAAU,OAAO;EACjB,WAAW,OAAO;EAClB,QAAQ,OAAO;EACf;EACA;EAGA,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,IAAI,OAAO;EAGX,MAAM,OAAO;EACb,SAAS,gBAAgB,OAAO,QAAQ;EACxC,eAAe,OAAO;EAGtB,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,cAAc,OAAO;EAGrB,kBAAkB,mBAAmB,OAAO;EAG5C,MAAM,OAAO;EACb,UAAU,QAAQ,YAAY;EAC9B,QAAQ,QAAQ,UAAU;EAC1B,OAAO,QAAQ,SAAS;EACxB,aAAa,QAAQ;EACrB,eAAe,QAAQ;EACvB,qBAAqB,QAAQ;EAC7B,qBAAqB,QAAQ;EAG7B,yBAAyB,QAAQ,2BAA2B;EAC5D,mBAAmB,QAAQ,qBAAqB;EAChD,kBAAkB,QAAQ;EAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"playerUtils.js","names":[],"sources":["../../src/utils/playerUtils.ts"],"sourcesContent":["/**\n * Player state utilities and helper functions\n */\n\nimport { getArchetypePhysicalAttributes } from \"../data/archetypePhysicalAttributes\";\nimport { PLAYER_ARCHETYPES_DATA, PlayerState, StatusEffect } from \"../systems\";\nimport {\n calculateAngleToTarget,\n createDefaultBodyFacing,\n} from \"../systems/animation\";\nimport type { BodyFacing } from \"../systems/animation/core/types\";\nimport {\n BodyPart,\n BodyPartHealth,\n BodyPartMaxHealth,\n} from \"../systems/bodypart/types\";\nimport { PlayerArchetype, Position, TrigramStance } from \"../types\";\nimport { CombatState } from \"../types/common\";\nimport { ARCHETYPE_ASSETS } from \"../types/constants\";\n\n/**\n * Default body part health values (100 HP each)\n * @korean 기본 신체부위 체력\n */\nconst DEFAULT_BODY_PART_HEALTH: BodyPartHealth = {\n [BodyPart.HEAD]: 100,\n [BodyPart.NECK]: 100,\n [BodyPart.TORSO_UPPER]: 100,\n [BodyPart.TORSO_LOWER]: 100,\n [BodyPart.ARM_LEFT]: 100,\n [BodyPart.ARM_RIGHT]: 100,\n [BodyPart.LEG_LEFT]: 100,\n [BodyPart.LEG_RIGHT]: 100,\n};\n\n/**\n * Default body part max health values\n * @korean 기본 신체부위 최대체력\n */\nconst DEFAULT_BODY_PART_MAX_HEALTH: BodyPartMaxHealth = {\n [BodyPart.HEAD]: 100,\n [BodyPart.NECK]: 100,\n [BodyPart.TORSO_UPPER]: 100,\n [BodyPart.TORSO_LOWER]: 100,\n [BodyPart.ARM_LEFT]: 100,\n [BodyPart.ARM_RIGHT]: 100,\n [BodyPart.LEG_LEFT]: 100,\n [BodyPart.LEG_RIGHT]: 100,\n};\n\n/**\n * Create a complete PlayerState from archetype and player index\n */\nexport function createPlayerFromArchetype(\n archetype: PlayerArchetype,\n playerIndex: number,\n): PlayerState {\n const archetypeData = PLAYER_ARCHETYPES_DATA[archetype];\n\n const basePosition: Position = {\n x: playerIndex === 0 ? 300 : 500,\n y: 400,\n };\n\n return {\n id: `player_${playerIndex + 1}`,\n name: archetypeData.name,\n archetype,\n\n // Physical attributes loaded from archetype defaults\n physicalAttributes: getArchetypePhysicalAttributes(archetype),\n\n // Combat stats\n health: archetypeData.baseHealth,\n maxHealth: archetypeData.baseHealth,\n bodyPartHealth: { ...DEFAULT_BODY_PART_HEALTH },\n bodyPartMaxHealth: { ...DEFAULT_BODY_PART_MAX_HEALTH },\n ki: archetypeData.baseKi,\n maxKi: archetypeData.baseKi,\n stamina: archetypeData.baseStamina,\n maxStamina: archetypeData.baseStamina,\n energy: 100,\n maxEnergy: 100,\n\n // Combat attributes\n attackPower: archetypeData.stats.attackPower,\n defense: archetypeData.stats.defense,\n speed: archetypeData.stats.speed,\n technique: archetypeData.stats.technique,\n pain: 0,\n consciousness: 100,\n balance: 100,\n momentum: 0,\n\n // Combat state\n currentStance: archetypeData.coreStance,\n combatState: CombatState.IDLE,\n position: basePosition,\n // Default to orthodox/southpaw stance\n // Player 1 starts orthodox, Player 2 starts southpaw for facing each other\n stanceSide: playerIndex === 0 ? \"orthodox\" : \"southpaw\",\n isBlocking: false,\n isStunned: false,\n isCountering: false,\n lastActionTime: 0,\n recoveryTime: 0,\n lastStanceChangeTime: 0,\n\n // Status and effects\n statusEffects: [],\n activeEffects: [],\n\n // Vital points state\n vitalPoints: [],\n\n // Match statistics\n totalDamageReceived: 0,\n totalDamageDealt: 0,\n hitsTaken: 0,\n hitsLanded: 0,\n perfectStrikes: 0,\n vitalPointHits: 0,\n };\n}\n\n/**\n * Update player state with partial updates\n */\nexport function updatePlayerState(\n player: PlayerState,\n updates: Partial<PlayerState>,\n): PlayerState {\n return {\n ...player,\n ...updates,\n // Ensure vital constraints\n health: Math.max(\n 0,\n Math.min(updates.health ?? player.health, player.maxHealth),\n ),\n ki: Math.max(0, Math.min(updates.ki ?? player.ki, player.maxKi)),\n stamina: Math.max(\n 0,\n Math.min(updates.stamina ?? player.stamina, player.maxStamina),\n ),\n consciousness: Math.max(\n 0,\n Math.min(updates.consciousness ?? player.consciousness, 100),\n ),\n balance: Math.max(0, Math.min(updates.balance ?? player.balance, 100)),\n };\n}\n\n/**\n * Apply damage to player\n */\nexport function applyDamage(\n player: PlayerState,\n damage: number,\n _damageType?: string, // Fix: Add underscore for unused parameter\n): PlayerState {\n const newHealth = Math.max(0, player.health - damage);\n const isKnockedOut = newHealth <= 0;\n\n return updatePlayerState(player, {\n health: newHealth,\n totalDamageReceived: player.totalDamageReceived + damage,\n hitsTaken: player.hitsTaken + 1,\n combatState: isKnockedOut ? CombatState.STUNNED : player.combatState,\n consciousness: isKnockedOut ? 0 : player.consciousness,\n });\n}\n\n/**\n * Apply status effect to player\n */\nexport function applyStatusEffect(\n player: PlayerState,\n effect: StatusEffect,\n): PlayerState {\n const existingEffectIndex = player.statusEffects.findIndex(\n (e) => e.type === effect.type && !e.stackable,\n );\n\n let newEffects: StatusEffect[];\n if (existingEffectIndex >= 0 && !effect.stackable) {\n // Replace existing non-stackable effect\n newEffects = [...player.statusEffects];\n newEffects[existingEffectIndex] = effect;\n } else {\n // Add new effect\n newEffects = [...player.statusEffects, effect];\n }\n\n return updatePlayerState(player, {\n statusEffects: newEffects,\n activeEffects: [...player.activeEffects, effect.type],\n });\n}\n\n/**\n * Get vital point by ID (fix return type)\n */\nexport function getVitalPointByOnPlayerId(\n player: PlayerState,\n vitalPointId: string,\n): {\n readonly id: string;\n readonly isHit: boolean;\n readonly damage: number;\n readonly lastHitTime: number;\n} | null {\n return player.vitalPoints.find((vp) => vp.id === vitalPointId) ?? null;\n}\n\n/**\n * Check if player can act\n */\nexport function canPlayerAct(player: PlayerState): boolean {\n if (player.health <= 0) return false;\n if (player.consciousness <= 0) return false;\n if (player.combatState === CombatState.STUNNED) return false;\n if (player.isStunned) return false;\n return true;\n}\n\n/**\n * Get player's current stance effectiveness against opponent\n */\nexport function getStanceEffectiveness(\n _playerStance: TrigramStance, // Fix: Add underscore to unused parameter\n _opponentStance: TrigramStance, // Fix: Add underscore to unused parameter\n): number {\n // Basic implementation - could be enhanced with stance matrix\n return 1.0;\n}\n\n/**\n * Check if player has enough resources for action\n */\nexport function hasEnoughResources(\n player: PlayerState,\n kiCost: number,\n staminaCost: number,\n): boolean {\n return player.ki >= kiCost && player.stamina >= staminaCost;\n}\n\n/**\n * Get player archetype bonuses\n */\nexport function getArchetypeBonuses(archetype: PlayerArchetype): {\n attackBonus: number;\n defenseBonus: number;\n speedBonus: number;\n techniqueBonus: number;\n} {\n const data = PLAYER_ARCHETYPES_DATA[archetype];\n return {\n attackBonus: data.stats.attackPower * 0.1,\n defenseBonus: data.stats.defense * 0.1,\n speedBonus: data.stats.speed * 0.1,\n techniqueBonus: data.stats.technique * 0.1,\n };\n}\n\n/**\n * Calculate player's current combat effectiveness\n */\nexport function calculateCombatEffectiveness(player: PlayerState): number {\n const healthFactor = player.health / player.maxHealth;\n const staminaFactor = player.stamina / player.maxStamina;\n const consciousnessFactor = player.consciousness / 100;\n const balanceFactor = player.balance / 100;\n\n return (\n (healthFactor + staminaFactor + consciousnessFactor + balanceFactor) / 4\n );\n}\n\n/**\n * Remove expired status effects\n */\nexport function updateStatusEffects(\n player: PlayerState,\n currentTime: number,\n): PlayerState {\n const activeEffects = player.statusEffects.filter(\n (effect) => effect.endTime > currentTime,\n );\n\n return updatePlayerState(player, {\n statusEffects: activeEffects,\n activeEffects: activeEffects.map((e) => e.type),\n });\n}\n\n/**\n * Reset player to starting state\n */\nexport function resetPlayerState(\n archetype: PlayerArchetype,\n playerIndex: number,\n): PlayerState {\n return createPlayerFromArchetype(archetype, playerIndex);\n}\n\n/**\n * Get archetype asset paths (image, theme music, etc.)\n *\n * Note: PlayerArchetype enum values are already lowercase (e.g., MUSA = \"musa\"),\n * so the toLowerCase() call is defensive programming for type safety.\n */\nexport function getArchetypeAssets(archetype: PlayerArchetype): {\n readonly id: string;\n readonly image: string;\n readonly theme: string;\n readonly themeId: string;\n readonly name_korean: string;\n readonly name_english: string;\n readonly textureKey: string;\n} {\n const archetypeId = archetype.toLowerCase();\n const asset = ARCHETYPE_ASSETS[archetypeId as keyof typeof ARCHETYPE_ASSETS];\n\n if (!asset) {\n console.warn(`No assets found for archetype: ${archetype}`);\n return {\n id: \"unknown\",\n image: \"/assets/visual/logo/black-trigram-256.png\",\n theme: \"/assets/audio/music/intro_theme.mp3\",\n themeId: \"intro_theme\",\n name_korean: \"알 수 없음\",\n name_english: \"Unknown\",\n textureKey: archetype,\n };\n }\n\n return asset;\n}\n\n/**\n * Initialize body facing for a player based on opponent position\n *\n * Calculates initial facing angle to point toward opponent.\n *\n * @param playerPosition - Player's position\n * @param opponentPosition - Opponent's position\n * @returns Initial BodyFacing state\n * @korean 몸향하기초기화\n *\n * @example\n * ```typescript\n * const player = {\n * ...basePlayer,\n * bodyFacing: initializeBodyFacing(\n * { x: 300, y: 400 },\n * { x: 500, y: 400 }\n * ),\n * };\n * ```\n */\nexport function initializeBodyFacing(\n playerPosition: Position,\n opponentPosition: Position,\n): BodyFacing {\n // Calculate initial angle to face opponent\n const initialAngle = calculateAngleToTarget(playerPosition, opponentPosition);\n\n return createDefaultBodyFacing(initialAngle);\n}\n\n/**\n * Toggle the player's stance side (orthodox ↔ southpaw)\n *\n * Switches between orthodox (left foot forward) and southpaw (right foot forward).\n * This is used when executing stance switches or certain techniques.\n *\n * @param player - Current player state\n * @returns Updated player state with toggled stance side\n * @korean 자세측면전환\n *\n * @example\n * ```typescript\n * // Switch from orthodox to southpaw\n * const newState = toggleStanceSide(player);\n * // newState.stanceSide === 'southpaw'\n * ```\n */\nexport function toggleStanceSide(player: PlayerState): PlayerState {\n const newStanceSide = player.stanceSide === \"orthodox\" ? \"southpaw\" : \"orthodox\";\n\n return {\n ...player,\n stanceSide: newStanceSide,\n };\n}\n\n/**\n * Get the lead foot from stance side, defaulting to orthodox if not set\n *\n * @param player - Player state\n * @returns Lead foot ('left' for orthodox, 'right' for southpaw)\n * @korean 자세측면에서선발발가져오기\n */\nexport function getPlayerLeadFoot(player: PlayerState): \"left\" | \"right\" {\n return player.stanceSide === \"southpaw\" ? \"right\" : \"left\"; // Default to orthodox\n}\n\n/**\n * Get the rear (power) foot for a player\n *\n * @param player - Player state\n * @returns Rear foot ('right' for orthodox, 'left' for southpaw)\n * @korean 후발발가져오기\n */\nexport function getPlayerRearFoot(player: PlayerState): \"left\" | \"right\" {\n return player.stanceSide === \"southpaw\" ? \"left\" : \"right\";\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"playerUtils.js","names":[],"sources":["../../src/utils/playerUtils.ts"],"sourcesContent":["/**\n * Player state utilities and helper functions\n */\n\nimport { getArchetypePhysicalAttributes } from \"../data/archetypePhysicalAttributes\";\nimport { PLAYER_ARCHETYPES_DATA, PlayerState, StatusEffect } from \"../systems\";\nimport {\n calculateAngleToTarget,\n createDefaultBodyFacing,\n} from \"../systems/animation\";\nimport type { BodyFacing } from \"../systems/animation/core/types\";\nimport {\n BodyPart,\n BodyPartHealth,\n BodyPartMaxHealth,\n} from \"../systems/bodypart/types\";\nimport { PlayerArchetype, Position, TrigramStance } from \"../types\";\nimport { CombatState } from \"../types/common\";\nimport { ARCHETYPE_ASSETS } from \"../types/constants\";\n\n/**\n * Default body part health values (100 HP each)\n * @korean 기본 신체부위 체력\n */\nconst DEFAULT_BODY_PART_HEALTH: BodyPartHealth = {\n [BodyPart.HEAD]: 100,\n [BodyPart.NECK]: 100,\n [BodyPart.TORSO_UPPER]: 100,\n [BodyPart.TORSO_LOWER]: 100,\n [BodyPart.ARM_LEFT]: 100,\n [BodyPart.ARM_RIGHT]: 100,\n [BodyPart.LEG_LEFT]: 100,\n [BodyPart.LEG_RIGHT]: 100,\n};\n\n/**\n * Default body part max health values\n * @korean 기본 신체부위 최대체력\n */\nconst DEFAULT_BODY_PART_MAX_HEALTH: BodyPartMaxHealth = {\n [BodyPart.HEAD]: 100,\n [BodyPart.NECK]: 100,\n [BodyPart.TORSO_UPPER]: 100,\n [BodyPart.TORSO_LOWER]: 100,\n [BodyPart.ARM_LEFT]: 100,\n [BodyPart.ARM_RIGHT]: 100,\n [BodyPart.LEG_LEFT]: 100,\n [BodyPart.LEG_RIGHT]: 100,\n};\n\n/**\n * Create a complete PlayerState from archetype and player index\n */\nexport function createPlayerFromArchetype(\n archetype: PlayerArchetype,\n playerIndex: number,\n): PlayerState {\n const archetypeData = PLAYER_ARCHETYPES_DATA[archetype];\n\n const basePosition: Position = {\n x: playerIndex === 0 ? 300 : 500,\n y: 400,\n };\n\n return {\n id: `player_${playerIndex + 1}`,\n name: archetypeData.name,\n archetype,\n\n // Physical attributes loaded from archetype defaults\n physicalAttributes: getArchetypePhysicalAttributes(archetype),\n\n // Combat stats\n health: archetypeData.baseHealth,\n maxHealth: archetypeData.baseHealth,\n bodyPartHealth: { ...DEFAULT_BODY_PART_HEALTH },\n bodyPartMaxHealth: { ...DEFAULT_BODY_PART_MAX_HEALTH },\n ki: archetypeData.baseKi,\n maxKi: archetypeData.baseKi,\n stamina: archetypeData.baseStamina,\n maxStamina: archetypeData.baseStamina,\n energy: 100,\n maxEnergy: 100,\n\n // Combat attributes\n attackPower: archetypeData.stats.attackPower,\n defense: archetypeData.stats.defense,\n speed: archetypeData.stats.speed,\n technique: archetypeData.stats.technique,\n pain: 0,\n consciousness: 100,\n balance: 100,\n momentum: 0,\n\n // Combat state\n currentStance: archetypeData.coreStance,\n combatState: CombatState.IDLE,\n position: basePosition,\n // Default to orthodox/southpaw stance\n // Player 1 starts orthodox, Player 2 starts southpaw for facing each other\n stanceSide: playerIndex === 0 ? \"orthodox\" : \"southpaw\",\n isBlocking: false,\n isStunned: false,\n isCountering: false,\n lastActionTime: 0,\n recoveryTime: 0,\n lastStanceChangeTime: 0,\n\n // Status and effects\n statusEffects: [],\n activeEffects: [],\n\n // Vital points state\n vitalPoints: [],\n\n // Match statistics\n totalDamageReceived: 0,\n totalDamageDealt: 0,\n hitsTaken: 0,\n hitsLanded: 0,\n perfectStrikes: 0,\n vitalPointHits: 0,\n };\n}\n\n/**\n * Update player state with partial updates\n */\nexport function updatePlayerState(\n player: PlayerState,\n updates: Partial<PlayerState>,\n): PlayerState {\n return {\n ...player,\n ...updates,\n // Ensure vital constraints\n health: Math.max(\n 0,\n Math.min(updates.health ?? player.health, player.maxHealth),\n ),\n ki: Math.max(0, Math.min(updates.ki ?? player.ki, player.maxKi)),\n stamina: Math.max(\n 0,\n Math.min(updates.stamina ?? player.stamina, player.maxStamina),\n ),\n consciousness: Math.max(\n 0,\n Math.min(updates.consciousness ?? player.consciousness, 100),\n ),\n balance: Math.max(0, Math.min(updates.balance ?? player.balance, 100)),\n };\n}\n\n/**\n * Apply damage to player\n */\nexport function applyDamage(\n player: PlayerState,\n damage: number,\n _damageType?: string, // Fix: Add underscore for unused parameter\n): PlayerState {\n const newHealth = Math.max(0, player.health - damage);\n const isKnockedOut = newHealth <= 0;\n\n return updatePlayerState(player, {\n health: newHealth,\n totalDamageReceived: player.totalDamageReceived + damage,\n hitsTaken: player.hitsTaken + 1,\n combatState: isKnockedOut ? CombatState.STUNNED : player.combatState,\n consciousness: isKnockedOut ? 0 : player.consciousness,\n });\n}\n\n/**\n * Apply status effect to player\n */\nexport function applyStatusEffect(\n player: PlayerState,\n effect: StatusEffect,\n): PlayerState {\n const existingEffectIndex = player.statusEffects.findIndex(\n (e) => e.type === effect.type && !e.stackable,\n );\n\n let newEffects: StatusEffect[];\n if (existingEffectIndex >= 0 && !effect.stackable) {\n // Replace existing non-stackable effect\n newEffects = [...player.statusEffects];\n newEffects[existingEffectIndex] = effect;\n } else {\n // Add new effect\n newEffects = [...player.statusEffects, effect];\n }\n\n return updatePlayerState(player, {\n statusEffects: newEffects,\n activeEffects: [...player.activeEffects, effect.type],\n });\n}\n\n/**\n * Get vital point by ID (fix return type)\n */\nexport function getVitalPointByOnPlayerId(\n player: PlayerState,\n vitalPointId: string,\n): {\n readonly id: string;\n readonly isHit: boolean;\n readonly damage: number;\n readonly lastHitTime: number;\n} | null {\n return player.vitalPoints.find((vp) => vp.id === vitalPointId) ?? null;\n}\n\n/**\n * Check if player can act\n */\nexport function canPlayerAct(player: PlayerState): boolean {\n if (player.health <= 0) return false;\n if (player.consciousness <= 0) return false;\n if (player.combatState === CombatState.STUNNED) return false;\n if (player.isStunned) return false;\n return true;\n}\n\n/**\n * Get player's current stance effectiveness against opponent\n */\nexport function getStanceEffectiveness(\n _playerStance: TrigramStance, // Fix: Add underscore to unused parameter\n _opponentStance: TrigramStance, // Fix: Add underscore to unused parameter\n): number {\n // Basic implementation - could be enhanced with stance matrix\n return 1.0;\n}\n\n/**\n * Check if player has enough resources for action\n */\nexport function hasEnoughResources(\n player: PlayerState,\n kiCost: number,\n staminaCost: number,\n): boolean {\n return player.ki >= kiCost && player.stamina >= staminaCost;\n}\n\n/**\n * Get player archetype bonuses\n */\nexport function getArchetypeBonuses(archetype: PlayerArchetype): {\n attackBonus: number;\n defenseBonus: number;\n speedBonus: number;\n techniqueBonus: number;\n} {\n const data = PLAYER_ARCHETYPES_DATA[archetype];\n return {\n attackBonus: data.stats.attackPower * 0.1,\n defenseBonus: data.stats.defense * 0.1,\n speedBonus: data.stats.speed * 0.1,\n techniqueBonus: data.stats.technique * 0.1,\n };\n}\n\n/**\n * Calculate player's current combat effectiveness\n */\nexport function calculateCombatEffectiveness(player: PlayerState): number {\n const healthFactor = player.health / player.maxHealth;\n const staminaFactor = player.stamina / player.maxStamina;\n const consciousnessFactor = player.consciousness / 100;\n const balanceFactor = player.balance / 100;\n\n return (\n (healthFactor + staminaFactor + consciousnessFactor + balanceFactor) / 4\n );\n}\n\n/**\n * Remove expired status effects\n */\nexport function updateStatusEffects(\n player: PlayerState,\n currentTime: number,\n): PlayerState {\n const activeEffects = player.statusEffects.filter(\n (effect) => effect.endTime > currentTime,\n );\n\n return updatePlayerState(player, {\n statusEffects: activeEffects,\n activeEffects: activeEffects.map((e) => e.type),\n });\n}\n\n/**\n * Reset player to starting state\n */\nexport function resetPlayerState(\n archetype: PlayerArchetype,\n playerIndex: number,\n): PlayerState {\n return createPlayerFromArchetype(archetype, playerIndex);\n}\n\n/**\n * Get archetype asset paths (image, theme music, etc.)\n *\n * Note: PlayerArchetype enum values are already lowercase (e.g., MUSA = \"musa\"),\n * so the toLowerCase() call is defensive programming for type safety.\n */\nexport function getArchetypeAssets(archetype: PlayerArchetype): {\n readonly id: string;\n readonly image: string;\n readonly theme: string;\n readonly themeId: string;\n readonly name_korean: string;\n readonly name_english: string;\n readonly textureKey: string;\n} {\n const archetypeId = archetype.toLowerCase();\n const asset = ARCHETYPE_ASSETS[archetypeId as keyof typeof ARCHETYPE_ASSETS];\n\n if (!asset) {\n console.warn(`No assets found for archetype: ${archetype}`);\n return {\n id: \"unknown\",\n image: \"/assets/visual/logo/black-trigram-256.png\",\n theme: \"/assets/audio/music/intro_theme.mp3\",\n themeId: \"intro_theme\",\n name_korean: \"알 수 없음\",\n name_english: \"Unknown\",\n textureKey: archetype,\n };\n }\n\n return asset;\n}\n\n/**\n * Initialize body facing for a player based on opponent position\n *\n * Calculates initial facing angle to point toward opponent.\n *\n * @param playerPosition - Player's position\n * @param opponentPosition - Opponent's position\n * @returns Initial BodyFacing state\n * @korean 몸향하기초기화\n *\n * @example\n * ```typescript\n * const player = {\n * ...basePlayer,\n * bodyFacing: initializeBodyFacing(\n * { x: 300, y: 400 },\n * { x: 500, y: 400 }\n * ),\n * };\n * ```\n */\nexport function initializeBodyFacing(\n playerPosition: Position,\n opponentPosition: Position,\n): BodyFacing {\n // Calculate initial angle to face opponent\n const initialAngle = calculateAngleToTarget(playerPosition, opponentPosition);\n\n return createDefaultBodyFacing(initialAngle);\n}\n\n/**\n * Toggle the player's stance side (orthodox ↔ southpaw)\n *\n * Switches between orthodox (left foot forward) and southpaw (right foot forward).\n * This is used when executing stance switches or certain techniques.\n *\n * @param player - Current player state\n * @returns Updated player state with toggled stance side\n * @korean 자세측면전환\n *\n * @example\n * ```typescript\n * // Switch from orthodox to southpaw\n * const newState = toggleStanceSide(player);\n * // newState.stanceSide === 'southpaw'\n * ```\n */\nexport function toggleStanceSide(player: PlayerState): PlayerState {\n const newStanceSide = player.stanceSide === \"orthodox\" ? \"southpaw\" : \"orthodox\";\n\n return {\n ...player,\n stanceSide: newStanceSide,\n };\n}\n\n/**\n * Get the lead foot from stance side, defaulting to orthodox if not set\n *\n * @param player - Player state\n * @returns Lead foot ('left' for orthodox, 'right' for southpaw)\n * @korean 자세측면에서선발발가져오기\n */\nexport function getPlayerLeadFoot(player: PlayerState): \"left\" | \"right\" {\n return player.stanceSide === \"southpaw\" ? \"right\" : \"left\"; // Default to orthodox\n}\n\n/**\n * Get the rear (power) foot for a player\n *\n * @param player - Player state\n * @returns Rear foot ('right' for orthodox, 'left' for southpaw)\n * @korean 후발발가져오기\n */\nexport function getPlayerRearFoot(player: PlayerState): \"left\" | \"right\" {\n return player.stanceSide === \"southpaw\" ? \"left\" : \"right\";\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"responsiveLayout.js","names":[],"sources":["../../src/utils/responsiveLayout.ts"],"sourcesContent":["/**\n * Responsive Layout Utility Functions\n * \n * Helper functions for responsive layout calculations and touch target validation\n * \n * @module utils/responsiveLayout\n * @category Mobile UI\n * @korean 반응형레이아웃유틸\n */\n\n/**\n * Minimum touch target size according to iOS Human Interface Guidelines\n */\nexport const MIN_TOUCH_TARGET_SIZE = 44;\n\n/**\n * Convert a CSS percentage string (for example \"70%\") into a decimal ratio.\n *\n * Invalid or non-finite percentage values fall back to `1` so responsive\n * layout calculations preserve full-width behaviour instead of producing NaN.\n *\n * @param value - CSS percentage string\n * @returns Decimal ratio (e.g. \"70%\" -> 0.7)\n */\nexport function parsePercentageToRatio(value: string): number {\n const trimmedValue = value.trim();\n if (!trimmedValue.endsWith('%')) {\n return 1;\n }\n\n const percent = Number.parseFloat(trimmedValue.slice(0, -1));\n return Number.isFinite(percent) ? percent / 100 : 1;\n}\n\n/**\n * Validate if an element meets minimum touch target requirements\n * \n * @param width - Element width in pixels\n * @param height - Element height in pixels\n * @param minSize - Minimum size (default: 44px iOS guideline)\n * @returns True if element meets minimum requirements\n * \n * @example\n * ```typescript\n * const isValid = isValidTouchTarget(50, 50); // true\n * const isTooSmall = isValidTouchTarget(30, 30); // false\n * ```\n */\nexport function isValidTouchTarget(\n width: number,\n height: number,\n minSize: number = MIN_TOUCH_TARGET_SIZE\n): boolean {\n return width >= minSize && height >= minSize;\n}\n\n/**\n * Calculate optimal font size for given viewport width\n * Ensures minimum readable font sizes on mobile\n * \n * @param viewportWidth - Viewport width in pixels\n * @param baseSize - Base font size for desktop (default: 16)\n * @param minSize - Minimum font size for mobile (default: 14)\n * @returns Calculated font size in pixels\n * \n * @example\n * ```typescript\n * const fontSize = calculateFontSize(375, 16, 14); // 14px for mobile\n * const fontSize = calculateFontSize(1920, 16, 14); // 16px for desktop\n * ```\n */\nexport function calculateFontSize(\n viewportWidth: number,\n baseSize: number = 16,\n minSize: number = 14\n): number {\n if (viewportWidth < 768) {\n // Mobile: use minimum size or scale down slightly\n return Math.max(minSize, Math.floor(baseSize * 0.875));\n } else if (viewportWidth < 1024) {\n // Tablet: scale proportionally\n return Math.max(minSize, Math.floor(baseSize * 0.9375));\n }\n // Desktop: use base size\n return baseSize;\n}\n\n/**\n * Calculate element position within safe area\n * Ensures elements don't overlap notch or home indicator\n * \n * @param value - Position value in pixels\n * @param safeAreaInset - Safe area inset for that edge\n * @returns Adjusted position value\n * \n * @example\n * ```typescript\n * const top = calculateSafePosition(10, 44); // 54px (10 + 44)\n * const bottom = calculateSafePosition(20, 34); // 54px (20 + 34)\n * ```\n */\nexport function calculateSafePosition(\n value: number,\n safeAreaInset: number\n): number {\n return value + safeAreaInset;\n}\n\n/**\n * Calculate optimal HUD height for given viewport\n * Scales based on device type and orientation\n * \n * @param viewportWidth - Viewport width in pixels\n * @param isLandscape - Whether in landscape orientation\n * @returns HUD height in pixels\n * \n * @example\n * ```typescript\n * const hudHeight = calculateHUDHeight(375, false); // ~80px for mobile portrait\n * const hudHeight = calculateHUDHeight(667, true); // ~60px for mobile landscape\n * ```\n */\nexport function calculateHUDHeight(\n viewportWidth: number,\n isLandscape: boolean\n): number {\n const isMobile = viewportWidth < 768;\n const isSmallMobile = viewportWidth <= 375; // Changed to <= to match 375px iPhone SE\n\n if (isLandscape && isMobile) {\n // Landscape: minimize HUD to maximize gameplay area\n return isSmallMobile ? 60 : 70;\n }\n\n if (isMobile) {\n // Portrait mobile: compact but readable\n return isSmallMobile ? 80 : 95;\n }\n\n // Desktop/tablet: larger HUD with more information\n return 120;\n}\n\n/**\n * Calculate optimal control bar height for mobile\n * \n * @param isMobile - Whether on mobile device\n * @param isLandscape - Whether in landscape orientation\n * @returns Control bar height in pixels\n */\nexport function calculateControlsHeight(\n isMobile: boolean,\n isLandscape: boolean\n): number {\n if (!isMobile) {\n return 0; // Desktop uses keyboard\n }\n\n if (isLandscape) {\n return 100; // Minimal controls in landscape\n }\n\n return 130; // Full touch controls in portrait\n}\n\n/**\n * Calculate spacing between HUD elements\n * Ensures proper touch targets and visual hierarchy\n * \n * @param isMobile - Whether on mobile device\n * @param density - Spacing density ('compact' | 'normal' | 'spacious')\n * @returns Spacing value in pixels\n */\nexport function calculateSpacing(\n isMobile: boolean,\n density: 'compact' | 'normal' | 'spacious' = 'normal'\n): number {\n const baseSpacing = isMobile ? 8 : 12;\n\n switch (density) {\n case 'compact':\n return Math.floor(baseSpacing * 0.75);\n case 'spacious':\n return Math.ceil(baseSpacing * 1.5);\n case 'normal':\n default:\n return baseSpacing;\n }\n}\n\n/**\n * Calculate optimal progress bar dimensions\n * Ensures bars are touch-friendly on mobile\n * \n * @param isMobile - Whether on mobile device\n * @param type - Bar type ('health' | 'ki' | 'stamina')\n * @returns Bar dimensions { width, height }\n */\nexport function calculateProgressBarSize(\n isMobile: boolean,\n type: 'health' | 'ki' | 'stamina'\n): { width: number; height: number } {\n if (isMobile) {\n // Mobile: touch-friendly sizes\n const height = type === 'health' ? 48 : 40; // Health bar larger\n return {\n width: 160,\n height,\n };\n }\n\n // Desktop: more detailed display\n const height = type === 'health' ? 55 : 45;\n return {\n width: 220,\n height,\n };\n}\n\n/**\n * Get optimal grid layout for trigram stance selector\n * \n * @param isMobile - Whether on mobile device\n * @param isLandscape - Whether in landscape orientation\n * @returns Grid configuration { columns, rows, gap }\n */\nexport function getStanceSelectorLayout(\n isMobile: boolean,\n isLandscape: boolean\n): { columns: number; rows: number; gap: number } {\n if (isMobile) {\n if (isLandscape) {\n // Landscape: horizontal layout\n return { columns: 4, rows: 2, gap: 8 };\n }\n // Portrait: compact grid\n return { columns: 4, rows: 2, gap: 10 };\n }\n\n // Desktop: spacious layout\n return { columns: 4, rows: 2, gap: 15 };\n}\n\n/**\n * Resolution breakpoints for responsive design\n * @korean 반응형 중단점\n */\nexport interface ResolutionBreakpoints {\n readonly mobile: number; // 768px\n readonly tablet: number; // 1280px\n readonly desktop: number; // 1920px\n readonly ultrawide: number; // 2560px\n}\n\n/**\n * Standard breakpoints for responsive sizing\n */\nexport const BREAKPOINTS: ResolutionBreakpoints = Object.freeze({\n mobile: 768,\n tablet: 1280,\n desktop: 1920,\n ultrawide: 2560,\n});\n\n/**\n * Get responsive size based on screen width\n * Scales linearly between breakpoints\n * \n * @param width - Screen width in pixels\n * @param sizes - Size values at each breakpoint\n * @returns Interpolated size value\n * \n * @example\n * ```typescript\n * const fontSize = getResponsiveSize(1024, { mobile: 12, tablet: 14, desktop: 16 });\n * // Returns ~13.3 (interpolated between mobile and tablet)\n * ```\n */\nexport function getResponsiveSize(\n width: number,\n sizes: { mobile: number; tablet: number; desktop: number }\n): number {\n if (width < BREAKPOINTS.mobile) {\n return sizes.mobile;\n }\n if (width < BREAKPOINTS.tablet) {\n const ratio = (width - BREAKPOINTS.mobile) / (BREAKPOINTS.tablet - BREAKPOINTS.mobile);\n return sizes.mobile + (sizes.tablet - sizes.mobile) * ratio;\n }\n if (width < BREAKPOINTS.desktop) {\n const ratio = (width - BREAKPOINTS.tablet) / (BREAKPOINTS.desktop - BREAKPOINTS.tablet);\n return sizes.tablet + (sizes.desktop - sizes.tablet) * ratio;\n }\n return sizes.desktop;\n}\n\n/**\n * Calculate HUD height as percentage of screen height\n * Ensures minimum and maximum bounds for usability\n * \n * @param height - Screen height in pixels\n * @param percentage - Target percentage (0.0 - 1.0)\n * @returns Calculated HUD height with bounds applied\n * \n * @example\n * ```typescript\n * const hudHeight = getHUDHeight(1080, 0.08); // ~86px (8% of 1080)\n * const tooSmall = getHUDHeight(400, 0.08); // 40px (minimum applied)\n * const tooLarge = getHUDHeight(3000, 0.08); // 120px (maximum applied)\n * ```\n */\nexport function getHUDHeight(height: number, percentage: number): number {\n return Math.max(40, Math.min(120, height * percentage));\n}\n\n/**\n * Calculate padding based on resolution\n * \n * @param width - Screen width in pixels\n * @returns Padding value in pixels\n * \n * @example\n * ```typescript\n * const padding = getResponsivePadding(375); // 8px (mobile)\n * const padding = getResponsivePadding(1920); // 16px (desktop)\n * ```\n */\nexport function getResponsivePadding(width: number): number {\n return getResponsiveSize(width, { mobile: 8, tablet: 12, desktop: 16 });\n}\n\n/**\n * Calculate font size based on resolution\n * \n * @param width - Screen width in pixels\n * @returns Font size in pixels\n * \n * @example\n * ```typescript\n * const fontSize = getResponsiveFontSize(375); // 12px (mobile)\n * const fontSize = getResponsiveFontSize(1920); // 16px (desktop)\n * ```\n */\nexport function getResponsiveFontSize(width: number): number {\n return getResponsiveSize(width, { mobile: 12, tablet: 14, desktop: 16 });\n}\n\n/**\n * Determine if mobile controls should be shown\n * NOTE: This is the ONLY valid use of isMobile prop\n * \n * @param width - Screen width in pixels\n * @param isMobile - Optional mobile device flag\n * @returns Whether to show mobile controls\n * \n * @example\n * ```typescript\n * const showControls = shouldShowMobileControls(500, false); // true (narrow screen)\n * const showControls = shouldShowMobileControls(1920, false); // false (wide screen)\n * const showControls = shouldShowMobileControls(1920, true); // true (mobile device)\n * ```\n */\nexport function shouldShowMobileControls(width: number, isMobile: boolean = false): boolean {\n return isMobile || width < BREAKPOINTS.mobile;\n}\n"],"mappings":";;;;;;;;;;AAwBA,SAAgB,uBAAuB,OAAuB;CAC5D,MAAM,eAAe,MAAM,MAAM;AACjC,KAAI,CAAC,aAAa,SAAS,IAAI,CAC7B,QAAO;CAGT,MAAM,UAAU,OAAO,WAAW,aAAa,MAAM,GAAG,GAAG,CAAC;AAC5D,QAAO,OAAO,SAAS,QAAQ,GAAG,UAAU,MAAM;;;;;AAkOpD,IAAa,cAAqC,OAAO,OAAO;CAC9D,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,WAAW;CACZ,CAAC;;;;;;;;;;;;;;;AAgBF,SAAgB,kBACd,OACA,OACQ;AACR,KAAI,QAAQ,YAAY,OACtB,QAAO,MAAM;AAEf,KAAI,QAAQ,YAAY,QAAQ;EAC9B,MAAM,SAAS,QAAQ,YAAY,WAAW,YAAY,SAAS,YAAY;AAC/E,SAAO,MAAM,UAAU,MAAM,SAAS,MAAM,UAAU;;AAExD,KAAI,QAAQ,YAAY,SAAS;EAC/B,MAAM,SAAS,QAAQ,YAAY,WAAW,YAAY,UAAU,YAAY;AAChF,SAAO,MAAM,UAAU,MAAM,UAAU,MAAM,UAAU;;AAEzD,QAAO,MAAM;;;;;;;;;;;;;;;;;AAkBf,SAAgB,aAAa,QAAgB,YAA4B;AACvE,QAAO,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK,SAAS,WAAW,CAAC;;;;;;;;;;;;;;AAezD,SAAgB,qBAAqB,OAAuB;AAC1D,QAAO,kBAAkB,OAAO;EAAE,QAAQ;EAAG,QAAQ;EAAI,SAAS;EAAI,CAAC;;;;;;;;;;;;;;AAezE,SAAgB,sBAAsB,OAAuB;AAC3D,QAAO,kBAAkB,OAAO;EAAE,QAAQ;EAAI,QAAQ;EAAI,SAAS;EAAI,CAAC;;;;;;;;;;;;;;;;;AAkB1E,SAAgB,yBAAyB,OAAe,WAAoB,OAAgB;AAC1F,QAAO,YAAY,QAAQ,YAAY"}
1
+ {"version":3,"file":"responsiveLayout.js","names":[],"sources":["../../src/utils/responsiveLayout.ts"],"sourcesContent":["/**\n * Responsive Layout Utility Functions\n * \n * Helper functions for responsive layout calculations and touch target validation\n * \n * @module utils/responsiveLayout\n * @category Mobile UI\n * @korean 반응형레이아웃유틸\n */\n\n/**\n * Minimum touch target size according to iOS Human Interface Guidelines\n */\nexport const MIN_TOUCH_TARGET_SIZE = 44;\n\n/**\n * Convert a CSS percentage string (for example \"70%\") into a decimal ratio.\n *\n * Invalid or non-finite percentage values fall back to `1` so responsive\n * layout calculations preserve full-width behaviour instead of producing NaN.\n *\n * @param value - CSS percentage string\n * @returns Decimal ratio (e.g. \"70%\" -> 0.7)\n */\nexport function parsePercentageToRatio(value: string): number {\n const trimmedValue = value.trim();\n if (!trimmedValue.endsWith('%')) {\n return 1;\n }\n\n const percent = Number.parseFloat(trimmedValue.slice(0, -1));\n return Number.isFinite(percent) ? percent / 100 : 1;\n}\n\n/**\n * Validate if an element meets minimum touch target requirements\n * \n * @param width - Element width in pixels\n * @param height - Element height in pixels\n * @param minSize - Minimum size (default: 44px iOS guideline)\n * @returns True if element meets minimum requirements\n * \n * @example\n * ```typescript\n * const isValid = isValidTouchTarget(50, 50); // true\n * const isTooSmall = isValidTouchTarget(30, 30); // false\n * ```\n */\nexport function isValidTouchTarget(\n width: number,\n height: number,\n minSize: number = MIN_TOUCH_TARGET_SIZE\n): boolean {\n return width >= minSize && height >= minSize;\n}\n\n/**\n * Calculate optimal font size for given viewport width\n * Ensures minimum readable font sizes on mobile\n * \n * @param viewportWidth - Viewport width in pixels\n * @param baseSize - Base font size for desktop (default: 16)\n * @param minSize - Minimum font size for mobile (default: 14)\n * @returns Calculated font size in pixels\n * \n * @example\n * ```typescript\n * const fontSize = calculateFontSize(375, 16, 14); // 14px for mobile\n * const fontSize = calculateFontSize(1920, 16, 14); // 16px for desktop\n * ```\n */\nexport function calculateFontSize(\n viewportWidth: number,\n baseSize: number = 16,\n minSize: number = 14\n): number {\n if (viewportWidth < 768) {\n // Mobile: use minimum size or scale down slightly\n return Math.max(minSize, Math.floor(baseSize * 0.875));\n } else if (viewportWidth < 1024) {\n // Tablet: scale proportionally\n return Math.max(minSize, Math.floor(baseSize * 0.9375));\n }\n // Desktop: use base size\n return baseSize;\n}\n\n/**\n * Calculate element position within safe area\n * Ensures elements don't overlap notch or home indicator\n * \n * @param value - Position value in pixels\n * @param safeAreaInset - Safe area inset for that edge\n * @returns Adjusted position value\n * \n * @example\n * ```typescript\n * const top = calculateSafePosition(10, 44); // 54px (10 + 44)\n * const bottom = calculateSafePosition(20, 34); // 54px (20 + 34)\n * ```\n */\nexport function calculateSafePosition(\n value: number,\n safeAreaInset: number\n): number {\n return value + safeAreaInset;\n}\n\n/**\n * Calculate optimal HUD height for given viewport\n * Scales based on device type and orientation\n * \n * @param viewportWidth - Viewport width in pixels\n * @param isLandscape - Whether in landscape orientation\n * @returns HUD height in pixels\n * \n * @example\n * ```typescript\n * const hudHeight = calculateHUDHeight(375, false); // ~80px for mobile portrait\n * const hudHeight = calculateHUDHeight(667, true); // ~60px for mobile landscape\n * ```\n */\nexport function calculateHUDHeight(\n viewportWidth: number,\n isLandscape: boolean\n): number {\n const isMobile = viewportWidth < 768;\n const isSmallMobile = viewportWidth <= 375; // Changed to <= to match 375px iPhone SE\n\n if (isLandscape && isMobile) {\n // Landscape: minimize HUD to maximize gameplay area\n return isSmallMobile ? 60 : 70;\n }\n\n if (isMobile) {\n // Portrait mobile: compact but readable\n return isSmallMobile ? 80 : 95;\n }\n\n // Desktop/tablet: larger HUD with more information\n return 120;\n}\n\n/**\n * Calculate optimal control bar height for mobile\n * \n * @param isMobile - Whether on mobile device\n * @param isLandscape - Whether in landscape orientation\n * @returns Control bar height in pixels\n */\nexport function calculateControlsHeight(\n isMobile: boolean,\n isLandscape: boolean\n): number {\n if (!isMobile) {\n return 0; // Desktop uses keyboard\n }\n\n if (isLandscape) {\n return 100; // Minimal controls in landscape\n }\n\n return 130; // Full touch controls in portrait\n}\n\n/**\n * Calculate spacing between HUD elements\n * Ensures proper touch targets and visual hierarchy\n * \n * @param isMobile - Whether on mobile device\n * @param density - Spacing density ('compact' | 'normal' | 'spacious')\n * @returns Spacing value in pixels\n */\nexport function calculateSpacing(\n isMobile: boolean,\n density: 'compact' | 'normal' | 'spacious' = 'normal'\n): number {\n const baseSpacing = isMobile ? 8 : 12;\n\n switch (density) {\n case 'compact':\n return Math.floor(baseSpacing * 0.75);\n case 'spacious':\n return Math.ceil(baseSpacing * 1.5);\n case 'normal':\n default:\n return baseSpacing;\n }\n}\n\n/**\n * Calculate optimal progress bar dimensions\n * Ensures bars are touch-friendly on mobile\n * \n * @param isMobile - Whether on mobile device\n * @param type - Bar type ('health' | 'ki' | 'stamina')\n * @returns Bar dimensions { width, height }\n */\nexport function calculateProgressBarSize(\n isMobile: boolean,\n type: 'health' | 'ki' | 'stamina'\n): { width: number; height: number } {\n if (isMobile) {\n // Mobile: touch-friendly sizes\n const height = type === 'health' ? 48 : 40; // Health bar larger\n return {\n width: 160,\n height,\n };\n }\n\n // Desktop: more detailed display\n const height = type === 'health' ? 55 : 45;\n return {\n width: 220,\n height,\n };\n}\n\n/**\n * Get optimal grid layout for trigram stance selector\n * \n * @param isMobile - Whether on mobile device\n * @param isLandscape - Whether in landscape orientation\n * @returns Grid configuration { columns, rows, gap }\n */\nexport function getStanceSelectorLayout(\n isMobile: boolean,\n isLandscape: boolean\n): { columns: number; rows: number; gap: number } {\n if (isMobile) {\n if (isLandscape) {\n // Landscape: horizontal layout\n return { columns: 4, rows: 2, gap: 8 };\n }\n // Portrait: compact grid\n return { columns: 4, rows: 2, gap: 10 };\n }\n\n // Desktop: spacious layout\n return { columns: 4, rows: 2, gap: 15 };\n}\n\n/**\n * Resolution breakpoints for responsive design\n * @korean 반응형 중단점\n */\nexport interface ResolutionBreakpoints {\n readonly mobile: number; // 768px\n readonly tablet: number; // 1280px\n readonly desktop: number; // 1920px\n readonly ultrawide: number; // 2560px\n}\n\n/**\n * Standard breakpoints for responsive sizing\n */\nexport const BREAKPOINTS: ResolutionBreakpoints = Object.freeze({\n mobile: 768,\n tablet: 1280,\n desktop: 1920,\n ultrawide: 2560,\n});\n\n/**\n * Get responsive size based on screen width\n * Scales linearly between breakpoints\n * \n * @param width - Screen width in pixels\n * @param sizes - Size values at each breakpoint\n * @returns Interpolated size value\n * \n * @example\n * ```typescript\n * const fontSize = getResponsiveSize(1024, { mobile: 12, tablet: 14, desktop: 16 });\n * // Returns ~13.3 (interpolated between mobile and tablet)\n * ```\n */\nexport function getResponsiveSize(\n width: number,\n sizes: { mobile: number; tablet: number; desktop: number }\n): number {\n if (width < BREAKPOINTS.mobile) {\n return sizes.mobile;\n }\n if (width < BREAKPOINTS.tablet) {\n const ratio = (width - BREAKPOINTS.mobile) / (BREAKPOINTS.tablet - BREAKPOINTS.mobile);\n return sizes.mobile + (sizes.tablet - sizes.mobile) * ratio;\n }\n if (width < BREAKPOINTS.desktop) {\n const ratio = (width - BREAKPOINTS.tablet) / (BREAKPOINTS.desktop - BREAKPOINTS.tablet);\n return sizes.tablet + (sizes.desktop - sizes.tablet) * ratio;\n }\n return sizes.desktop;\n}\n\n/**\n * Calculate HUD height as percentage of screen height\n * Ensures minimum and maximum bounds for usability\n * \n * @param height - Screen height in pixels\n * @param percentage - Target percentage (0.0 - 1.0)\n * @returns Calculated HUD height with bounds applied\n * \n * @example\n * ```typescript\n * const hudHeight = getHUDHeight(1080, 0.08); // ~86px (8% of 1080)\n * const tooSmall = getHUDHeight(400, 0.08); // 40px (minimum applied)\n * const tooLarge = getHUDHeight(3000, 0.08); // 120px (maximum applied)\n * ```\n */\nexport function getHUDHeight(height: number, percentage: number): number {\n return Math.max(40, Math.min(120, height * percentage));\n}\n\n/**\n * Calculate padding based on resolution\n * \n * @param width - Screen width in pixels\n * @returns Padding value in pixels\n * \n * @example\n * ```typescript\n * const padding = getResponsivePadding(375); // 8px (mobile)\n * const padding = getResponsivePadding(1920); // 16px (desktop)\n * ```\n */\nexport function getResponsivePadding(width: number): number {\n return getResponsiveSize(width, { mobile: 8, tablet: 12, desktop: 16 });\n}\n\n/**\n * Calculate font size based on resolution\n * \n * @param width - Screen width in pixels\n * @returns Font size in pixels\n * \n * @example\n * ```typescript\n * const fontSize = getResponsiveFontSize(375); // 12px (mobile)\n * const fontSize = getResponsiveFontSize(1920); // 16px (desktop)\n * ```\n */\nexport function getResponsiveFontSize(width: number): number {\n return getResponsiveSize(width, { mobile: 12, tablet: 14, desktop: 16 });\n}\n\n/**\n * Determine if mobile controls should be shown\n * NOTE: This is the ONLY valid use of isMobile prop\n * \n * @param width - Screen width in pixels\n * @param isMobile - Optional mobile device flag\n * @returns Whether to show mobile controls\n * \n * @example\n * ```typescript\n * const showControls = shouldShowMobileControls(500, false); // true (narrow screen)\n * const showControls = shouldShowMobileControls(1920, false); // false (wide screen)\n * const showControls = shouldShowMobileControls(1920, true); // true (mobile device)\n * ```\n */\nexport function shouldShowMobileControls(width: number, isMobile: boolean = false): boolean {\n return isMobile || width < BREAKPOINTS.mobile;\n}\n"],"mappings":";;;;;;;;;;AAwBA,SAAgB,uBAAuB,OAAuB;CAC5D,MAAM,eAAe,MAAM,MAAM;CACjC,IAAI,CAAC,aAAa,SAAS,IAAI,EAC7B,OAAO;CAGT,MAAM,UAAU,OAAO,WAAW,aAAa,MAAM,GAAG,GAAG,CAAC;CAC5D,OAAO,OAAO,SAAS,QAAQ,GAAG,UAAU,MAAM;;;;;AAkOpD,IAAa,cAAqC,OAAO,OAAO;CAC9D,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,WAAW;CACZ,CAAC;;;;;;;;;;;;;;;AAgBF,SAAgB,kBACd,OACA,OACQ;CACR,IAAI,QAAQ,YAAY,QACtB,OAAO,MAAM;CAEf,IAAI,QAAQ,YAAY,QAAQ;EAC9B,MAAM,SAAS,QAAQ,YAAY,WAAW,YAAY,SAAS,YAAY;EAC/E,OAAO,MAAM,UAAU,MAAM,SAAS,MAAM,UAAU;;CAExD,IAAI,QAAQ,YAAY,SAAS;EAC/B,MAAM,SAAS,QAAQ,YAAY,WAAW,YAAY,UAAU,YAAY;EAChF,OAAO,MAAM,UAAU,MAAM,UAAU,MAAM,UAAU;;CAEzD,OAAO,MAAM;;;;;;;;;;;;;;;;;AAkBf,SAAgB,aAAa,QAAgB,YAA4B;CACvE,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK,SAAS,WAAW,CAAC;;;;;;;;;;;;;;AAezD,SAAgB,qBAAqB,OAAuB;CAC1D,OAAO,kBAAkB,OAAO;EAAE,QAAQ;EAAG,QAAQ;EAAI,SAAS;EAAI,CAAC;;;;;;;;;;;;;;AAezE,SAAgB,sBAAsB,OAAuB;CAC3D,OAAO,kBAAkB,OAAO;EAAE,QAAQ;EAAI,QAAQ;EAAI,SAAS;EAAI,CAAC;;;;;;;;;;;;;;;;;AAkB1E,SAAgB,yBAAyB,OAAe,WAAoB,OAAgB;CAC1F,OAAO,YAAY,QAAQ,YAAY"}
@@ -1 +1 @@
1
- {"version":3,"file":"responsiveLayoutHelpers.js","names":[],"sources":["../../src/utils/responsiveLayoutHelpers.ts"],"sourcesContent":["/**\n * Responsive Layout Helpers\n * \n * Centralized utilities for calculating responsive layout constants\n * across different screen components. Uses the centralized ResponsiveScaling\n * system for consistent scaling patterns.\n * \n * @module utils/responsiveLayoutHelpers\n * @category Layout\n * @korean 반응형레이아웃도우미\n */\n\nimport { getScreenSize } from '../systems/ResponsiveScaling';\nimport type { ScreenSize } from '../systems/ResponsiveScaling';\n\n/** Desktop arena width as a proportion of viewport width. */\nconst DESKTOP_ARENA_WIDTH_RATIO = 0.8;\n\n/**\n * Maximum desktop arena width in CSS pixels.\n *\n * Caps ultra-wide/8K displays to protect WebGL fill-rate while preserving a\n * large, readable 4K desktop arena.\n */\nconst DESKTOP_ARENA_MAX_WIDTH_PX = 2560;\n\n/**\n * Calculate maximum desktop arena width for combat/training screens.\n *\n * @param width - Viewport width in CSS pixels\n * @returns Width budget for the 4:3 desktop arena\n *\n * @public\n */\nexport function getDesktopArenaWidthBudget(width: number): number {\n return Math.min(width * DESKTOP_ARENA_WIDTH_RATIO, DESKTOP_ARENA_MAX_WIDTH_PX);\n}\n\n/**\n * Base layout values for different screen sizes\n * These serve as reference values that scale proportionally\n */\nconst BASE_LAYOUT_VALUES = {\n // Base padding values (desktop reference)\n padding: {\n mobile: 20,\n tablet: 25,\n desktop: 30,\n large: 32,\n xlarge: 35,\n },\n // Base header height values\n headerHeight: {\n mobile: 90,\n tablet: 100,\n desktop: 110,\n large: 115,\n xlarge: 120,\n },\n // Base footer height values\n footerHeight: {\n mobile: 75,\n tablet: 85,\n desktop: 90,\n large: 95,\n xlarge: 100,\n },\n // Base section spacing values\n sectionSpacing: {\n mobile: 15,\n tablet: 18,\n desktop: 20,\n large: 22,\n xlarge: 25,\n },\n // Base button area values\n buttonArea: {\n mobile: 75,\n tablet: 85,\n desktop: 95,\n large: 102,\n xlarge: 110,\n },\n} as const;\n\n/**\n * Calculate responsive padding value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated padding in pixels\n * \n * @example\n * ```typescript\n * const padding = getResponsivePadding('xlarge'); // 35\n * const padding = getResponsivePadding('mobile'); // 20\n * ```\n * \n * @public\n */\nexport function getResponsivePadding(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.padding[screenSize];\n}\n\n/**\n * Calculate responsive header height value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated header height in pixels\n * \n * @example\n * ```typescript\n * const headerHeight = getResponsiveHeaderHeight('xlarge'); // 120\n * const headerHeight = getResponsiveHeaderHeight('mobile'); // 90\n * ```\n * \n * @public\n */\nexport function getResponsiveHeaderHeight(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.headerHeight[screenSize];\n}\n\n/**\n * Calculate responsive footer height value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated footer height in pixels\n * \n * @example\n * ```typescript\n * const footerHeight = getResponsiveFooterHeight('xlarge'); // 100\n * const footerHeight = getResponsiveFooterHeight('mobile'); // 75\n * ```\n * \n * @public\n */\nexport function getResponsiveFooterHeight(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.footerHeight[screenSize];\n}\n\n/**\n * Calculate responsive section spacing value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated section spacing in pixels\n * \n * @example\n * ```typescript\n * const spacing = getResponsiveSectionSpacing('xlarge'); // 25\n * const spacing = getResponsiveSectionSpacing('mobile'); // 15\n * ```\n * \n * @public\n */\nexport function getResponsiveSectionSpacing(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.sectionSpacing[screenSize];\n}\n\n/**\n * Calculate responsive button area value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated button area in pixels\n * \n * @example\n * ```typescript\n * const buttonArea = getResponsiveButtonArea('xlarge'); // 110\n * const buttonArea = getResponsiveButtonArea('mobile'); // 75\n * ```\n * \n * @public\n */\nexport function getResponsiveButtonArea(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.buttonArea[screenSize];\n}\n\n/**\n * Get all layout constants for a given screen size\n * Convenient helper that returns all layout values at once\n * \n * @param width - Screen width in pixels\n * @returns Object with all layout constant values\n * \n * @example\n * ```typescript\n * const layout = getLayoutConstants(3840); // 4K display\n * // {\n * // padding: 35,\n * // headerHeight: 120,\n * // footerHeight: 100,\n * // sectionSpacing: 25,\n * // buttonArea: 110\n * // }\n * ```\n * \n * @public\n */\nexport function getLayoutConstants(width: number) {\n const screenSize = getScreenSize(width);\n \n return {\n padding: getResponsivePadding(screenSize),\n headerHeight: getResponsiveHeaderHeight(screenSize),\n footerHeight: getResponsiveFooterHeight(screenSize),\n sectionSpacing: getResponsiveSectionSpacing(screenSize),\n buttonArea: getResponsiveButtonArea(screenSize),\n };\n}\n\n/**\n * Get combat-specific layout constants for a given screen size\n * \n * Optimized for narrow devices (<450px), with extra-small device support\n * explicitly tuned for ultra-small screens (<380px) like iPhone SE, old\n * Android phones, and budget smartphones.\n * \n * Now properly handles high-resolution mobile devices (2K+, Super HD) by\n * checking isMobile flag to ensure they get mobile-optimized layout values\n * regardless of pixel width.\n * \n * @param width - Screen width in pixels\n * @param isMobile - Optional: Whether device is mobile (from user-agent detection)\n * @returns Object with combat layout constant values\n * \n * @example\n * ```typescript\n * // Extra-small mobile (iPhone SE)\n * const layout = getCombatLayoutConstants(375, true);\n * // { padding: 8, hudHeight: 85, controlsHeight: 150, ... }\n * \n * // High-res mobile (Motorola Edge 60 Pro)\n * const layoutHD = getCombatLayoutConstants(2712, true);\n * // { padding: 10, hudHeight: 95, controlsHeight: 160, ... } (mobile values!)\n * \n * // Desktop\n * const layoutDesktop = getCombatLayoutConstants(1920, false);\n * // { padding: 10, hudHeight: 135, controlsHeight: 175, ... } (desktop values)\n * ```\n * \n * @public\n */\nexport function getCombatLayoutConstants(width: number, isMobile?: boolean) {\n // For mobile devices, force 'mobile' screen size regardless of pixel width\n // This ensures high-res mobile devices (2K+) get mobile-optimized layouts\n const screenSize = isMobile ? 'mobile' : getScreenSize(width);\n \n // Extra-small detection for low-end mobile devices (<380px)\n const isExtraSmall = isMobile && width < 380;\n \n // Combat screen uses different base values for compact HUD\n const hudHeightMap = {\n mobile: isExtraSmall ? 85 : 95,\n tablet: 100,\n desktop: 130,\n large: 135,\n xlarge: 140,\n };\n \n // Note: Tablet optimizations - controlsHeight and footerHeight are intentionally\n // smaller on tablets than mobile for better landscape orientation ergonomics.\n // Mobile (portrait) needs taller controls for thumb reach, while tablets\n // (often landscape) can use more compact controls with better screen utilization.\n const controlsHeightMap = {\n mobile: isExtraSmall ? 150 : 160, // Taller for portrait thumb reach\n tablet: 140, // Optimized for landscape - more compact\n desktop: 170,\n large: 175,\n xlarge: 180,\n };\n \n const footerHeightMap = {\n mobile: 34, // Adequate for portrait orientation\n tablet: 30, // Optimized for landscape - more compact\n desktop: 35,\n large: 37,\n xlarge: 40,\n };\n \n const healthBarHeightMap = {\n mobile: 48,\n tablet: 50,\n desktop: 65,\n large: 67,\n xlarge: 70,\n };\n \n // Touch target heights - WCAG AA compliance (minimum 44px)\n const buttonHeightMap = {\n mobile: isExtraSmall ? 48 : 55, // Minimum 48px for extra-small\n tablet: 55,\n desktop: 60,\n large: 60,\n xlarge: 60,\n };\n \n return {\n padding: isExtraSmall ? 8 : 10, // Reduced padding for extra-small\n hudHeight: hudHeightMap[screenSize],\n controlsHeight: controlsHeightMap[screenSize],\n footerHeight: footerHeightMap[screenSize],\n healthBarHeight: healthBarHeightMap[screenSize],\n buttonHeight: buttonHeightMap[screenSize],\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,IAAM,4BAA4B;;;;;;;AAQlC,IAAM,6BAA6B;;;;;;;;;AAUnC,SAAgB,2BAA2B,OAAuB;AAChE,QAAO,KAAK,IAAI,QAAQ,2BAA2B,2BAA2B;;;;;;AAOhF,IAAM,qBAAqB;CAEzB,SAAS;EACP,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,cAAc;EACZ,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,cAAc;EACZ,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,gBAAgB;EACd,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,YAAY;EACV,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CACF;;;;;;;;;;;;;;;AAgBD,SAAgB,qBAAqB,YAAgC;AACnE,QAAO,mBAAmB,QAAQ;;;;;;;;;;;;;;;;AAiBpC,SAAgB,0BAA0B,YAAgC;AACxE,QAAO,mBAAmB,aAAa;;;;;;;;;;;;;;;;AAiBzC,SAAgB,0BAA0B,YAAgC;AACxE,QAAO,mBAAmB,aAAa;;;;;;;;;;;;;;;;AAiBzC,SAAgB,4BAA4B,YAAgC;AAC1E,QAAO,mBAAmB,eAAe;;;;;;;;;;;;;;;;AAiB3C,SAAgB,wBAAwB,YAAgC;AACtE,QAAO,mBAAmB,WAAW;;;;;;;;;;;;;;;;;;;;;;;AAwBvC,SAAgB,mBAAmB,OAAe;CAChD,MAAM,aAAa,cAAc,MAAM;AAEvC,QAAO;EACL,SAAS,qBAAqB,WAAW;EACzC,cAAc,0BAA0B,WAAW;EACnD,cAAc,0BAA0B,WAAW;EACnD,gBAAgB,4BAA4B,WAAW;EACvD,YAAY,wBAAwB,WAAW;EAChD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,SAAgB,yBAAyB,OAAe,UAAoB;CAG1E,MAAM,aAAa,WAAW,WAAW,cAAc,MAAM;CAG7D,MAAM,eAAe,YAAY,QAAQ;CAGzC,MAAM,eAAe;EACnB,QAAQ,eAAe,KAAK;EAC5B,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAMD,MAAM,oBAAoB;EACxB,QAAQ,eAAe,MAAM;EAC7B,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,MAAM,kBAAkB;EACtB,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,MAAM,qBAAqB;EACzB,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAGD,MAAM,kBAAkB;EACtB,QAAQ,eAAe,KAAK;EAC5B,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;AAED,QAAO;EACL,SAAS,eAAe,IAAI;EAC5B,WAAW,aAAa;EACxB,gBAAgB,kBAAkB;EAClC,cAAc,gBAAgB;EAC9B,iBAAiB,mBAAmB;EACpC,cAAc,gBAAgB;EAC/B"}
1
+ {"version":3,"file":"responsiveLayoutHelpers.js","names":[],"sources":["../../src/utils/responsiveLayoutHelpers.ts"],"sourcesContent":["/**\n * Responsive Layout Helpers\n * \n * Centralized utilities for calculating responsive layout constants\n * across different screen components. Uses the centralized ResponsiveScaling\n * system for consistent scaling patterns.\n * \n * @module utils/responsiveLayoutHelpers\n * @category Layout\n * @korean 반응형레이아웃도우미\n */\n\nimport { getScreenSize } from '../systems/ResponsiveScaling';\nimport type { ScreenSize } from '../systems/ResponsiveScaling';\n\n/** Desktop arena width as a proportion of viewport width. */\nconst DESKTOP_ARENA_WIDTH_RATIO = 0.8;\n\n/**\n * Maximum desktop arena width in CSS pixels.\n *\n * Caps ultra-wide/8K displays to protect WebGL fill-rate while preserving a\n * large, readable 4K desktop arena.\n */\nconst DESKTOP_ARENA_MAX_WIDTH_PX = 2560;\n\n/**\n * Calculate maximum desktop arena width for combat/training screens.\n *\n * @param width - Viewport width in CSS pixels\n * @returns Width budget for the 4:3 desktop arena\n *\n * @public\n */\nexport function getDesktopArenaWidthBudget(width: number): number {\n return Math.min(width * DESKTOP_ARENA_WIDTH_RATIO, DESKTOP_ARENA_MAX_WIDTH_PX);\n}\n\n/**\n * Base layout values for different screen sizes\n * These serve as reference values that scale proportionally\n */\nconst BASE_LAYOUT_VALUES = {\n // Base padding values (desktop reference)\n padding: {\n mobile: 20,\n tablet: 25,\n desktop: 30,\n large: 32,\n xlarge: 35,\n },\n // Base header height values\n headerHeight: {\n mobile: 90,\n tablet: 100,\n desktop: 110,\n large: 115,\n xlarge: 120,\n },\n // Base footer height values\n footerHeight: {\n mobile: 75,\n tablet: 85,\n desktop: 90,\n large: 95,\n xlarge: 100,\n },\n // Base section spacing values\n sectionSpacing: {\n mobile: 15,\n tablet: 18,\n desktop: 20,\n large: 22,\n xlarge: 25,\n },\n // Base button area values\n buttonArea: {\n mobile: 75,\n tablet: 85,\n desktop: 95,\n large: 102,\n xlarge: 110,\n },\n} as const;\n\n/**\n * Calculate responsive padding value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated padding in pixels\n * \n * @example\n * ```typescript\n * const padding = getResponsivePadding('xlarge'); // 35\n * const padding = getResponsivePadding('mobile'); // 20\n * ```\n * \n * @public\n */\nexport function getResponsivePadding(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.padding[screenSize];\n}\n\n/**\n * Calculate responsive header height value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated header height in pixels\n * \n * @example\n * ```typescript\n * const headerHeight = getResponsiveHeaderHeight('xlarge'); // 120\n * const headerHeight = getResponsiveHeaderHeight('mobile'); // 90\n * ```\n * \n * @public\n */\nexport function getResponsiveHeaderHeight(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.headerHeight[screenSize];\n}\n\n/**\n * Calculate responsive footer height value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated footer height in pixels\n * \n * @example\n * ```typescript\n * const footerHeight = getResponsiveFooterHeight('xlarge'); // 100\n * const footerHeight = getResponsiveFooterHeight('mobile'); // 75\n * ```\n * \n * @public\n */\nexport function getResponsiveFooterHeight(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.footerHeight[screenSize];\n}\n\n/**\n * Calculate responsive section spacing value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated section spacing in pixels\n * \n * @example\n * ```typescript\n * const spacing = getResponsiveSectionSpacing('xlarge'); // 25\n * const spacing = getResponsiveSectionSpacing('mobile'); // 15\n * ```\n * \n * @public\n */\nexport function getResponsiveSectionSpacing(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.sectionSpacing[screenSize];\n}\n\n/**\n * Calculate responsive button area value\n * \n * @param screenSize - Current screen size category\n * @returns Calculated button area in pixels\n * \n * @example\n * ```typescript\n * const buttonArea = getResponsiveButtonArea('xlarge'); // 110\n * const buttonArea = getResponsiveButtonArea('mobile'); // 75\n * ```\n * \n * @public\n */\nexport function getResponsiveButtonArea(screenSize: ScreenSize): number {\n return BASE_LAYOUT_VALUES.buttonArea[screenSize];\n}\n\n/**\n * Get all layout constants for a given screen size\n * Convenient helper that returns all layout values at once\n * \n * @param width - Screen width in pixels\n * @returns Object with all layout constant values\n * \n * @example\n * ```typescript\n * const layout = getLayoutConstants(3840); // 4K display\n * // {\n * // padding: 35,\n * // headerHeight: 120,\n * // footerHeight: 100,\n * // sectionSpacing: 25,\n * // buttonArea: 110\n * // }\n * ```\n * \n * @public\n */\nexport function getLayoutConstants(width: number) {\n const screenSize = getScreenSize(width);\n \n return {\n padding: getResponsivePadding(screenSize),\n headerHeight: getResponsiveHeaderHeight(screenSize),\n footerHeight: getResponsiveFooterHeight(screenSize),\n sectionSpacing: getResponsiveSectionSpacing(screenSize),\n buttonArea: getResponsiveButtonArea(screenSize),\n };\n}\n\n/**\n * Get combat-specific layout constants for a given screen size\n * \n * Optimized for narrow devices (<450px), with extra-small device support\n * explicitly tuned for ultra-small screens (<380px) like iPhone SE, old\n * Android phones, and budget smartphones.\n * \n * Now properly handles high-resolution mobile devices (2K+, Super HD) by\n * checking isMobile flag to ensure they get mobile-optimized layout values\n * regardless of pixel width.\n * \n * @param width - Screen width in pixels\n * @param isMobile - Optional: Whether device is mobile (from user-agent detection)\n * @returns Object with combat layout constant values\n * \n * @example\n * ```typescript\n * // Extra-small mobile (iPhone SE)\n * const layout = getCombatLayoutConstants(375, true);\n * // { padding: 8, hudHeight: 85, controlsHeight: 150, ... }\n * \n * // High-res mobile (Motorola Edge 60 Pro)\n * const layoutHD = getCombatLayoutConstants(2712, true);\n * // { padding: 10, hudHeight: 95, controlsHeight: 160, ... } (mobile values!)\n * \n * // Desktop\n * const layoutDesktop = getCombatLayoutConstants(1920, false);\n * // { padding: 10, hudHeight: 135, controlsHeight: 175, ... } (desktop values)\n * ```\n * \n * @public\n */\nexport function getCombatLayoutConstants(width: number, isMobile?: boolean) {\n // For mobile devices, force 'mobile' screen size regardless of pixel width\n // This ensures high-res mobile devices (2K+) get mobile-optimized layouts\n const screenSize = isMobile ? 'mobile' : getScreenSize(width);\n \n // Extra-small detection for low-end mobile devices (<380px)\n const isExtraSmall = isMobile && width < 380;\n \n // Combat screen uses different base values for compact HUD\n const hudHeightMap = {\n mobile: isExtraSmall ? 85 : 95,\n tablet: 100,\n desktop: 130,\n large: 135,\n xlarge: 140,\n };\n \n // Note: Tablet optimizations - controlsHeight and footerHeight are intentionally\n // smaller on tablets than mobile for better landscape orientation ergonomics.\n // Mobile (portrait) needs taller controls for thumb reach, while tablets\n // (often landscape) can use more compact controls with better screen utilization.\n const controlsHeightMap = {\n mobile: isExtraSmall ? 150 : 160, // Taller for portrait thumb reach\n tablet: 140, // Optimized for landscape - more compact\n desktop: 170,\n large: 175,\n xlarge: 180,\n };\n \n const footerHeightMap = {\n mobile: 34, // Adequate for portrait orientation\n tablet: 30, // Optimized for landscape - more compact\n desktop: 35,\n large: 37,\n xlarge: 40,\n };\n \n const healthBarHeightMap = {\n mobile: 48,\n tablet: 50,\n desktop: 65,\n large: 67,\n xlarge: 70,\n };\n \n // Touch target heights - WCAG AA compliance (minimum 44px)\n const buttonHeightMap = {\n mobile: isExtraSmall ? 48 : 55, // Minimum 48px for extra-small\n tablet: 55,\n desktop: 60,\n large: 60,\n xlarge: 60,\n };\n \n return {\n padding: isExtraSmall ? 8 : 10, // Reduced padding for extra-small\n hudHeight: hudHeightMap[screenSize],\n controlsHeight: controlsHeightMap[screenSize],\n footerHeight: footerHeightMap[screenSize],\n healthBarHeight: healthBarHeightMap[screenSize],\n buttonHeight: buttonHeightMap[screenSize],\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,IAAM,4BAA4B;;;;;;;AAQlC,IAAM,6BAA6B;;;;;;;;;AAUnC,SAAgB,2BAA2B,OAAuB;CAChE,OAAO,KAAK,IAAI,QAAQ,2BAA2B,2BAA2B;;;;;;AAOhF,IAAM,qBAAqB;CAEzB,SAAS;EACP,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,cAAc;EACZ,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,cAAc;EACZ,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,gBAAgB;EACd,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,YAAY;EACV,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CACF;;;;;;;;;;;;;;;AAgBD,SAAgB,qBAAqB,YAAgC;CACnE,OAAO,mBAAmB,QAAQ;;;;;;;;;;;;;;;;AAiBpC,SAAgB,0BAA0B,YAAgC;CACxE,OAAO,mBAAmB,aAAa;;;;;;;;;;;;;;;;AAiBzC,SAAgB,0BAA0B,YAAgC;CACxE,OAAO,mBAAmB,aAAa;;;;;;;;;;;;;;;;AAiBzC,SAAgB,4BAA4B,YAAgC;CAC1E,OAAO,mBAAmB,eAAe;;;;;;;;;;;;;;;;AAiB3C,SAAgB,wBAAwB,YAAgC;CACtE,OAAO,mBAAmB,WAAW;;;;;;;;;;;;;;;;;;;;;;;AAwBvC,SAAgB,mBAAmB,OAAe;CAChD,MAAM,aAAa,cAAc,MAAM;CAEvC,OAAO;EACL,SAAS,qBAAqB,WAAW;EACzC,cAAc,0BAA0B,WAAW;EACnD,cAAc,0BAA0B,WAAW;EACnD,gBAAgB,4BAA4B,WAAW;EACvD,YAAY,wBAAwB,WAAW;EAChD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,SAAgB,yBAAyB,OAAe,UAAoB;CAG1E,MAAM,aAAa,WAAW,WAAW,cAAc,MAAM;CAG7D,MAAM,eAAe,YAAY,QAAQ;CAGzC,MAAM,eAAe;EACnB,QAAQ,eAAe,KAAK;EAC5B,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAMD,MAAM,oBAAoB;EACxB,QAAQ,eAAe,MAAM;EAC7B,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,MAAM,kBAAkB;EACtB,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,MAAM,qBAAqB;EACzB,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAGD,MAAM,kBAAkB;EACtB,QAAQ,eAAe,KAAK;EAC5B,QAAQ;EACR,SAAS;EACT,OAAO;EACP,QAAQ;EACT;CAED,OAAO;EACL,SAAS,eAAe,IAAI;EAC5B,WAAW,aAAa;EACxB,gBAAgB,kBAAkB;EAClC,cAAc,gBAAgB;EAC9B,iBAAiB,mBAAmB;EACpC,cAAc,gBAAgB;EAC/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"responsiveOrientationConstants.js","names":[],"sources":["../../src/utils/responsiveOrientationConstants.ts"],"sourcesContent":["/**\n * Shared constants for orientation-aware responsive layout.\n *\n * Extracted so that `useCombatLayout` and `useTrainingLayout` compute the\n * same portrait \"bottom band\" and share a single source of truth for the\n * portrait-forcing hysteresis and the reserved control-column heights.\n *\n * 반응형 레이아웃 상수\n *\n * @module utils/responsiveOrientationConstants\n * @category Layout\n */\n\n/**\n * Hysteresis factor for portrait detection.\n *\n * A viewport is treated as portrait when `height > width × FACTOR`. The\n * factor is < 1.0 so near-square viewports (e.g. `1024×1000`) settle into\n * one orientation and don't flap on every pixel of resize.\n *\n * @public\n */\nexport const PORTRAIT_HYSTERESIS_FACTOR = 0.9;\n\n/**\n * Maximum width at which a portrait viewport is force-promoted to the mobile\n * layout branch even if the user-agent reports a desktop browser. Matches the\n * tablet breakpoint used elsewhere in the codebase (1024px).\n *\n * This makes devtools emulation of a rotated phone/tablet behave identically\n * to a real device.\n *\n * @public\n */\nexport const PORTRAIT_FORCE_MAX_WIDTH_PX = 1024;\n\n/**\n * Height reserved at the bottom of a mobile portrait viewport for the\n * on-screen virtual controls (D-Pad + action buttons rendered by\n * `MobileControlsWrapper`). Used as a conservative upper bound so the\n * 3D arena never ends up drawn behind the controls.\n *\n * Two values are provided so that very small phones (iPhone SE class,\n * width < 380) can still fit a playable arena.\n *\n * Combat uses the larger 200/160 band because its Mobile controls stack\n * D-Pad + action buttons + the persistent technique bar. Training uses\n * the smaller 180/140 band because its on-screen controls are lighter.\n *\n * @public\n */\nexport const MOBILE_CONTROLS_RESERVED_HEIGHT_PX = {\n /** D-Pad + action buttons + technique bar on combat (standard phones) */\n combatStandard: 200,\n /** Combat controls on extra-small phones (width < 380) */\n combatExtraSmall: 160,\n /** Training on-screen controls (standard phones) */\n trainingStandard: 180,\n /** Training on-screen controls (extra-small phones) */\n trainingExtraSmall: 140,\n} as const;\n\n/**\n * Height reserved at the bottom of mobile landscape viewports.\n *\n * Landscape devices have less vertical room than portrait phones, so these\n * compact values reserve only the visible bottom technique/control band while\n * still keeping the 3D arena above touch controls.\n *\n * @public\n */\nexport const LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX = {\n /** Combat bottom band on landscape phones */\n combatStandard: 120,\n /** Combat bottom band on extra-small landscape phones */\n combatExtraSmall: 110,\n /** Training bottom band on landscape phones */\n trainingStandard: 120,\n /** Training bottom band on extra-small landscape phones */\n trainingExtraSmall: 110,\n} as const;\n\n/**\n * Total bottom clearance to reserve in portrait mode = control/technique\n * bar height + footer height + the on-screen virtual controls band.\n *\n * @param controlsHeight - layout constant for technique/control bar\n * @param footerHeight - layout constant for footer\n * @param isExtraSmall - true when the viewport is < 380px wide\n * @param variant - \"combat\" or \"training\" (differs in control band size)\n *\n * @public\n */\nexport function portraitMobileControlsBottomBand(\n controlsHeight: number,\n footerHeight: number,\n isExtraSmall: boolean,\n variant: \"combat\" | \"training\",\n): number {\n const band =\n variant === \"combat\"\n ? isExtraSmall\n ? MOBILE_CONTROLS_RESERVED_HEIGHT_PX.combatExtraSmall\n : MOBILE_CONTROLS_RESERVED_HEIGHT_PX.combatStandard\n : isExtraSmall\n ? MOBILE_CONTROLS_RESERVED_HEIGHT_PX.trainingExtraSmall\n : MOBILE_CONTROLS_RESERVED_HEIGHT_PX.trainingStandard;\n\n return controlsHeight + footerHeight + band;\n}\n\n/**\n * Bottom clearance for mobile landscape viewports.\n *\n * @param isExtraSmall - true when the viewport is < 380px wide\n * @param variant - \"combat\" or \"training\"\n *\n * @public\n */\nexport function landscapeMobileControlsBottomClearance(\n isExtraSmall: boolean,\n variant: \"combat\" | \"training\",\n): number {\n if (variant === \"combat\") {\n return isExtraSmall\n ? LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.combatExtraSmall\n : LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.combatStandard;\n }\n\n return isExtraSmall\n ? LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.trainingExtraSmall\n : LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.trainingStandard;\n}\n\n/**\n * Orientation-aware bottom clearance for mobile combat/training arenas.\n *\n * Keeps callers from duplicating portrait-vs-landscape clearance rules while\n * preserving the compact landscape band and the full portrait touch-control\n * reservation.\n *\n * @param controlsHeight - layout constant for technique/control bar\n * @param footerHeight - layout constant for footer\n * @param isExtraSmall - true when the viewport is < 380px wide\n * @param isPortrait - true for portrait orientation\n * @param variant - \"combat\" or \"training\"\n *\n * @public\n */\nexport function mobileControlsBottomClearance(\n controlsHeight: number,\n footerHeight: number,\n isExtraSmall: boolean,\n isPortrait: boolean,\n variant: \"combat\" | \"training\",\n): number {\n return isPortrait\n ? portraitMobileControlsBottomBand(\n controlsHeight,\n footerHeight,\n isExtraSmall,\n variant,\n )\n : landscapeMobileControlsBottomClearance(isExtraSmall, variant);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,6BAA6B;;;;;;;;;;;AAY1C,IAAa,8BAA8B;;;;;;;;;;;;;;;;AAiB3C,IAAa,qCAAqC;;CAEhD,gBAAgB;;CAEhB,kBAAkB;;CAElB,kBAAkB;;CAElB,oBAAoB;CACrB;;;;;;;;;;AAWD,IAAa,gDAAgD;;CAE3D,gBAAgB;;CAEhB,kBAAkB;;CAElB,kBAAkB;;CAElB,oBAAoB;CACrB;;;;;;;;;;;;AAaD,SAAgB,iCACd,gBACA,cACA,cACA,SACQ;CACR,MAAM,OACJ,YAAY,WACR,eACE,mCAAmC,mBACnC,mCAAmC,iBACrC,eACE,mCAAmC,qBACnC,mCAAmC;AAE3C,QAAO,iBAAiB,eAAe;;;;;;;;;;AAWzC,SAAgB,uCACd,cACA,SACQ;AACR,KAAI,YAAY,SACd,QAAO,eACH,8CAA8C,mBAC9C,8CAA8C;AAGpD,QAAO,eACH,8CAA8C,qBAC9C,8CAA8C;;;;;;;;;;;;;;;;;AAkBpD,SAAgB,8BACd,gBACA,cACA,cACA,YACA,SACQ;AACR,QAAO,aACH,iCACE,gBACA,cACA,cACA,QACD,GACD,uCAAuC,cAAc,QAAQ"}
1
+ {"version":3,"file":"responsiveOrientationConstants.js","names":[],"sources":["../../src/utils/responsiveOrientationConstants.ts"],"sourcesContent":["/**\n * Shared constants for orientation-aware responsive layout.\n *\n * Extracted so that `useCombatLayout` and `useTrainingLayout` compute the\n * same portrait \"bottom band\" and share a single source of truth for the\n * portrait-forcing hysteresis and the reserved control-column heights.\n *\n * 반응형 레이아웃 상수\n *\n * @module utils/responsiveOrientationConstants\n * @category Layout\n */\n\n/**\n * Hysteresis factor for portrait detection.\n *\n * A viewport is treated as portrait when `height > width × FACTOR`. The\n * factor is < 1.0 so near-square viewports (e.g. `1024×1000`) settle into\n * one orientation and don't flap on every pixel of resize.\n *\n * @public\n */\nexport const PORTRAIT_HYSTERESIS_FACTOR = 0.9;\n\n/**\n * Maximum width at which a portrait viewport is force-promoted to the mobile\n * layout branch even if the user-agent reports a desktop browser. Matches the\n * tablet breakpoint used elsewhere in the codebase (1024px).\n *\n * This makes devtools emulation of a rotated phone/tablet behave identically\n * to a real device.\n *\n * @public\n */\nexport const PORTRAIT_FORCE_MAX_WIDTH_PX = 1024;\n\n/**\n * Height reserved at the bottom of a mobile portrait viewport for the\n * on-screen virtual controls (D-Pad + action buttons rendered by\n * `MobileControlsWrapper`). Used as a conservative upper bound so the\n * 3D arena never ends up drawn behind the controls.\n *\n * Two values are provided so that very small phones (iPhone SE class,\n * width < 380) can still fit a playable arena.\n *\n * Combat uses the larger 200/160 band because its Mobile controls stack\n * D-Pad + action buttons + the persistent technique bar. Training uses\n * the smaller 180/140 band because its on-screen controls are lighter.\n *\n * @public\n */\nexport const MOBILE_CONTROLS_RESERVED_HEIGHT_PX = {\n /** D-Pad + action buttons + technique bar on combat (standard phones) */\n combatStandard: 200,\n /** Combat controls on extra-small phones (width < 380) */\n combatExtraSmall: 160,\n /** Training on-screen controls (standard phones) */\n trainingStandard: 180,\n /** Training on-screen controls (extra-small phones) */\n trainingExtraSmall: 140,\n} as const;\n\n/**\n * Height reserved at the bottom of mobile landscape viewports.\n *\n * Landscape devices have less vertical room than portrait phones, so these\n * compact values reserve only the visible bottom technique/control band while\n * still keeping the 3D arena above touch controls.\n *\n * @public\n */\nexport const LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX = {\n /** Combat bottom band on landscape phones */\n combatStandard: 120,\n /** Combat bottom band on extra-small landscape phones */\n combatExtraSmall: 110,\n /** Training bottom band on landscape phones */\n trainingStandard: 120,\n /** Training bottom band on extra-small landscape phones */\n trainingExtraSmall: 110,\n} as const;\n\n/**\n * Total bottom clearance to reserve in portrait mode = control/technique\n * bar height + footer height + the on-screen virtual controls band.\n *\n * @param controlsHeight - layout constant for technique/control bar\n * @param footerHeight - layout constant for footer\n * @param isExtraSmall - true when the viewport is < 380px wide\n * @param variant - \"combat\" or \"training\" (differs in control band size)\n *\n * @public\n */\nexport function portraitMobileControlsBottomBand(\n controlsHeight: number,\n footerHeight: number,\n isExtraSmall: boolean,\n variant: \"combat\" | \"training\",\n): number {\n const band =\n variant === \"combat\"\n ? isExtraSmall\n ? MOBILE_CONTROLS_RESERVED_HEIGHT_PX.combatExtraSmall\n : MOBILE_CONTROLS_RESERVED_HEIGHT_PX.combatStandard\n : isExtraSmall\n ? MOBILE_CONTROLS_RESERVED_HEIGHT_PX.trainingExtraSmall\n : MOBILE_CONTROLS_RESERVED_HEIGHT_PX.trainingStandard;\n\n return controlsHeight + footerHeight + band;\n}\n\n/**\n * Bottom clearance for mobile landscape viewports.\n *\n * @param isExtraSmall - true when the viewport is < 380px wide\n * @param variant - \"combat\" or \"training\"\n *\n * @public\n */\nexport function landscapeMobileControlsBottomClearance(\n isExtraSmall: boolean,\n variant: \"combat\" | \"training\",\n): number {\n if (variant === \"combat\") {\n return isExtraSmall\n ? LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.combatExtraSmall\n : LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.combatStandard;\n }\n\n return isExtraSmall\n ? LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.trainingExtraSmall\n : LANDSCAPE_MOBILE_CONTROLS_BOTTOM_CLEARANCE_PX.trainingStandard;\n}\n\n/**\n * Orientation-aware bottom clearance for mobile combat/training arenas.\n *\n * Keeps callers from duplicating portrait-vs-landscape clearance rules while\n * preserving the compact landscape band and the full portrait touch-control\n * reservation.\n *\n * @param controlsHeight - layout constant for technique/control bar\n * @param footerHeight - layout constant for footer\n * @param isExtraSmall - true when the viewport is < 380px wide\n * @param isPortrait - true for portrait orientation\n * @param variant - \"combat\" or \"training\"\n *\n * @public\n */\nexport function mobileControlsBottomClearance(\n controlsHeight: number,\n footerHeight: number,\n isExtraSmall: boolean,\n isPortrait: boolean,\n variant: \"combat\" | \"training\",\n): number {\n return isPortrait\n ? portraitMobileControlsBottomBand(\n controlsHeight,\n footerHeight,\n isExtraSmall,\n variant,\n )\n : landscapeMobileControlsBottomClearance(isExtraSmall, variant);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,6BAA6B;;;;;;;;;;;AAY1C,IAAa,8BAA8B;;;;;;;;;;;;;;;;AAiB3C,IAAa,qCAAqC;;CAEhD,gBAAgB;;CAEhB,kBAAkB;;CAElB,kBAAkB;;CAElB,oBAAoB;CACrB;;;;;;;;;;AAWD,IAAa,gDAAgD;;CAE3D,gBAAgB;;CAEhB,kBAAkB;;CAElB,kBAAkB;;CAElB,oBAAoB;CACrB;;;;;;;;;;;;AAaD,SAAgB,iCACd,gBACA,cACA,cACA,SACQ;CACR,MAAM,OACJ,YAAY,WACR,eACE,mCAAmC,mBACnC,mCAAmC,iBACrC,eACE,mCAAmC,qBACnC,mCAAmC;CAE3C,OAAO,iBAAiB,eAAe;;;;;;;;;;AAWzC,SAAgB,uCACd,cACA,SACQ;CACR,IAAI,YAAY,UACd,OAAO,eACH,8CAA8C,mBAC9C,8CAA8C;CAGpD,OAAO,eACH,8CAA8C,qBAC9C,8CAA8C;;;;;;;;;;;;;;;;;AAkBpD,SAAgB,8BACd,gBACA,cACA,cACA,YACA,SACQ;CACR,OAAO,aACH,iCACE,gBACA,cACA,cACA,QACD,GACD,uCAAuC,cAAc,QAAQ"}