blacktrigram 0.7.48 → 0.7.49

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 (38) hide show
  1. package/lib/components/screens/combat/CombatScreen3D.d.ts.map +1 -1
  2. package/lib/components/screens/combat/CombatScreen3D.js +7 -14
  3. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  4. package/lib/components/screens/combat/components/hud/CombatBottomHUD.d.ts.map +1 -1
  5. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js +2 -2
  6. package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
  7. package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js +1 -1
  8. package/lib/components/screens/combat/components/hud/CombatTopHUD.d.ts.map +1 -1
  9. package/lib/components/screens/combat/components/hud/CombatTopHUD.js +2 -1
  10. package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
  11. package/lib/components/screens/combat/hooks/useCombatLayout.d.ts.map +1 -1
  12. package/lib/components/screens/combat/hooks/useCombatLayout.js +11 -5
  13. package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
  14. package/lib/components/screens/controls/ControlsScreen3D.js +1 -1
  15. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  16. package/lib/components/screens/philosophy/PhilosophyScreen3D.js +1 -1
  17. package/lib/components/screens/training/TrainingScreen3D.d.ts.map +1 -1
  18. package/lib/components/screens/training/TrainingScreen3D.js +2 -11
  19. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  20. package/lib/components/screens/training/components/hud/TrainingBottomHUD.d.ts.map +1 -1
  21. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js +2 -2
  22. package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
  23. package/lib/components/screens/training/hooks/useTrainingLayout.d.ts.map +1 -1
  24. package/lib/components/screens/training/hooks/useTrainingLayout.js +11 -5
  25. package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
  26. package/lib/components/shared/ui/SplashScreen.js +2 -2
  27. package/lib/hooks/useHUDLayout.d.ts.map +1 -1
  28. package/lib/hooks/useHUDLayout.js +3 -2
  29. package/lib/hooks/useHUDLayout.js.map +1 -1
  30. package/lib/types/constants/layout.d.ts +21 -0
  31. package/lib/types/constants/layout.d.ts.map +1 -1
  32. package/lib/types/constants/layout.js +22 -1
  33. package/lib/types/constants/layout.js.map +1 -1
  34. package/lib/utils/responsiveLayoutHelpers.d.ts +7 -0
  35. package/lib/utils/responsiveLayoutHelpers.d.ts.map +1 -1
  36. package/lib/utils/responsiveLayoutHelpers.js +16 -2
  37. package/lib/utils/responsiveLayoutHelpers.js.map +1 -1
  38. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"TrainingScreen3D.js","names":[],"sources":["../../../../src/components/screens/training/TrainingScreen3D.tsx"],"sourcesContent":["/**\n * TrainingScreen3D - Three.js-based training screen\n *\n * Refactored to use consolidated hooks matching CombatScreen architecture.\n * Provides 3D training dummy with vital point targeting and UI overlays.\n *\n * UI Rendering: All HUD elements are rendered in an absolute-positioned div\n * OUTSIDE the Canvas, matching CombatScreen's reliable rendering pattern.\n * This eliminates the need for Html overlays inside Three.js and ensures\n * HUDs appear immediately without waiting for Canvas initialization.\n *\n * Architecture (Consolidated in PR #1394 + Issue #1398):\n * - TrainingLeftHUD: Anatomy controls, guard indicator\n * - TrainingRightHUD: Training stats, mode selector, vital point selection\n * - TrainingTopHUD: Training controls, archetype selector, return button\n * - TrainingBottomHUD: Technique bar, feedback messages, mobile controls\n * - VitalPointOverlayControlsPure: Vital point overlay controls (pure DOM)\n *\n * All UI components render as pure DOM in the HUD overlay div (lines 1230+).\n * NO Html components from @react-three/drei are used inside the Canvas.\n * This ensures clean separation of 3D rendering and UI layers.\n *\n * @korean 훈련화면3D - 훈련 상태 훅을 사용한 리팩토링된 3D 훈련 화면\n */\n\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { AccelerationUpdater } from \"../../../systems/movement/helpers/AccelerationUpdater\";\nimport {\n isRunningSpeed,\n STEP_DISTANCE_THRESHOLDS,\n} from \"../../../systems/movement/helpers/accelerationUtils\";\nimport {\n Bloom,\n EffectComposer,\n Noise,\n Vignette,\n} from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { useCombatAudio } from \"../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../data/archetypePhysicalAttributes\";\nimport { usePlayerAnimation } from \"../../../hooks/usePlayerAnimation\";\nimport { useTechniqueSelection } from \"../../../hooks/useTechniqueSelection\";\nimport { GestureEvent } from \"../../../hooks/useTouchControls\";\nimport { useWebGLContextLossHandler } from \"../../../hooks/useWebGLContextLossHandler\";\nimport { PlayerState } from \"../../../systems\";\nimport {\n AnimationEvents,\n AnimationState,\n AnimationType,\n resolveTechniqueAnimation,\n} from \"../../../systems/animation\";\nimport { getAnimationForTechniqueOrDefault } from \"../../../systems/animation/core/TechniqueAnimationMapping\";\nimport { physicalReachCalculator } from \"../../../systems/physics\";\nimport {\n MovementType,\n SpeedModifierSystem,\n} from \"../../../systems/physics/SpeedModifierSystem\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport {\n CombatState,\n PlayerArchetype,\n Position,\n Technique,\n TrigramStance,\n} from \"../../../types\";\nimport { getPerformanceSettings } from \"../../../types/constants\";\nimport { getMobileControlsBottom } from \"../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { DEFAULT_BODY_RADIUS_METERS } from \"../../../types/physicsConstants\";\nimport { usePlayerMovement } from \"../../../utils/inputSystem\";\nimport { calculateDistance3D } from \"../../../utils/math\";\nimport { createCameraConfig } from \"../../../utils/sharedPhysicsConfig\";\nimport {\n animationStateToPlayerAnimation,\n convertPlayerStateToProps,\n} from \"../../../utils/player3DHelpers\";\nimport { useKoreanTheme } from \"../../shared/base/useKoreanTheme\";\nimport {\n GestureRecognizerPure,\n StanceWheelPure,\n} from \"../../shared/mobile\";\nimport {\n MobileControlsOverlay,\n type ButtonEventType,\n type Direction,\n type DPadEventType,\n} from \"../../shared/mobile/MobileControlsPure\";\nimport {\n Player3DWithTransitions,\n VitalPointMarkers3D,\n type BodyRegionFilter,\n} from \"../../shared/three\";\nimport { StanceChangeIndicator } from \"../../shared/three/indicators/StanceChangeIndicator\";\nimport { CombatArena3D } from \"../../shared/three/scene/CombatArena3D\";\nimport { VitalPointOverlayControlsPure } from \"../../shared/ui/VitalPointOverlayControlsPure\";\nimport AnatomyOverlay3D, {\n type AnatomyLayer,\n} from \"./components/AnatomyOverlay3D\";\nimport FootPlacementMarkers3D from \"./components/FootPlacementMarkers3D\";\nimport HitFeedbackEffect3D from \"./components/HitFeedbackEffect3D\";\nimport type { DifficultyMode } from \"./components/TrainingDummy3D\";\nimport TrainingDummy3D from \"./components/TrainingDummy3D\";\nimport {\n TrainingBottomHUD,\n TrainingLeftHUD,\n TrainingRightHUD,\n TrainingTopHUD,\n} from \"./components/hud\";\nimport { useAttackMovement } from \"./hooks/useAttackMovement\";\nimport useTrainingActions from \"./hooks/useTrainingActions\";\nimport { useTrainingLayout } from \"./hooks/useTrainingLayout\";\nimport useTrainingState from \"./hooks/useTrainingState\";\n\n/**\n * AnimationUpdater - Component that updates player animation at 60fps\n *\n * @korean 훈련애니메이션업데이터 - 60fps로 플레이어 애니메이션을 업데이트하는 컴포넌트\n */\ninterface TrainingAnimationUpdaterProps {\n readonly playerAnimation: ReturnType<typeof usePlayerAnimation>;\n}\n\nconst TrainingAnimationUpdater: React.FC<TrainingAnimationUpdaterProps> = ({\n playerAnimation,\n}) => {\n useFrame((_state, delta) => {\n playerAnimation.update(delta);\n });\n\n return null;\n};\n\n/**\n * Props for the TrainingScreen3D component\n */\nexport interface TrainingScreen3DProps {\n /** Callback to update player state */\n readonly onPlayerUpdate: (updates: Partial<PlayerState>) => void;\n /** Callback when returning to menu */\n readonly onReturnToMenu: () => void;\n /** Canvas width in pixels. Defaults to 1200 */\n readonly width?: number;\n /** Canvas height in pixels. Defaults to 800 */\n readonly height?: number;\n /** Initial archetype from IntroScreen selection. Defaults to MUSA */\n readonly initialArchetype?: PlayerArchetype;\n}\n\n/**\n * TrainingScreen3D Component\n * Three.js-based training screen with 3D dummy and Html UI\n *\n * Uses consolidated hooks for state management matching CombatScreen architecture.\n */\nexport const TrainingScreen3D: React.FC<TrainingScreen3DProps> = ({\n onPlayerUpdate,\n onReturnToMenu,\n width = 1200,\n height = 800,\n initialArchetype = PlayerArchetype.MUSA,\n}) => {\n\n\n const { state: trainingState, actions: trainingActions } = useTrainingState();\n\n const audio = useAudio();\n \n const { playBoneImpactSound, playAttackSound, playStanceChangeSound } =\n useCombatAudio();\n\n const { trainingAreaBounds, isMobile, isPortrait, screenSize } =\n useTrainingLayout(width, height);\n\n const theme = useKoreanTheme({\n variant: \"primary\",\n size: \"md\",\n isMobile,\n });\n\n const positionScale = React.useMemo(() => {\n if (isMobile) {\n return 1.0;\n }\n\n switch (screenSize) {\n case \"mobile\":\n return 1.0; // Mobile already has special handling\n case \"tablet\":\n return 1.0;\n case \"desktop\":\n return 1.0;\n case \"large\":\n return 1.25;\n case \"xlarge\":\n return 1.5; // 4K displays need 1.5x offsets\n default:\n return 1.0;\n }\n }, [isMobile, screenSize]);\n\n const difficulty: DifficultyMode = \"normal\";\n const vitalPointCount = 70; // Show all 70 vital points\n\n const [selectedArchetype, setSelectedArchetype] =\n React.useState<PlayerArchetype>(initialArchetype);\n\n const [overlayVisible, setOverlayVisible] = React.useState(false);\n const [severityFilters, setSeverityFilters] = React.useState<\n import(\"../../../types/common\").VitalPointSeverity[]\n >([]);\n const [regionFilter, setRegionFilter] =\n React.useState<BodyRegionFilter>(\"all\");\n const [searchQuery, setSearchQuery] = React.useState(\"\");\n const [showLabels, setShowLabels] = React.useState(true);\n const [animated, setAnimated] = React.useState(true);\n const [scale, setScale] = React.useState(1.2);\n\n\n const [attackAnimation, setAttackAnimation] = React.useState<\n string | undefined\n >(undefined);\n\n React.useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"v\" || e.key === \"V\") {\n setOverlayVisible((prev) => !prev);\n audio.playSFX(\"menu_select\");\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, [audio]);\n\n\n const contextLossCountRef = useRef(0);\n\n useWebGLContextLossHandler({\n onContextLost: () => {\n console.warn(\"⚠️ WebGL context lost in TrainingScreen\");\n contextLossCountRef.current += 1;\n },\n onContextRestored: () => {\n console.log(\"✓ WebGL context restored in TrainingScreen\");\n },\n autoRestore: true,\n });\n\n\n const speedModifierSystem = useMemo(() => new SpeedModifierSystem(), []);\n\n const [speedModifiers, setSpeedModifiers] = useState({\n finalSpeed: 6.0, // BASE_WALK_SPEED (6.0 m/s for responsive combat)\n baseSpeed: 6.0,\n finalAcceleration: 12.0, // BASE_ACCELERATION (12.0 m/s² for quick response)\n });\n\n const [walkRunSpeeds, setWalkRunSpeeds] = useState({\n walkSpeed: 6.0,\n runSpeed: 10.0,\n });\n\n\n const initialPositionMeters = useMemo<Position>(\n () => ({\n x: trainingAreaBounds.worldWidthMeters * 0.0, // Centered laterally\n y: 0, // Centered vertically\n }),\n [trainingAreaBounds],\n );\n\n const handlePositionChange = useCallback(\n (newPosition: Position) => {\n onPlayerUpdate({ position: newPosition });\n },\n [onPlayerUpdate],\n );\n\n const movementBounds = useMemo(\n () => ({\n worldWidthMeters: trainingAreaBounds.worldWidthMeters,\n worldDepthMeters: trainingAreaBounds.worldDepthMeters,\n }),\n [trainingAreaBounds.worldWidthMeters, trainingAreaBounds.worldDepthMeters],\n );\n\n \n const movementTimeRef = useRef(0);\n const lastDirectionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n \n const [accelerationBasedSpeed, setAccelerationBasedSpeed] = useState(\n walkRunSpeeds.walkSpeed\n );\n \n const isRunning = isRunningSpeed(accelerationBasedSpeed, walkRunSpeeds.runSpeed);\n\n const { playerPosition, isMoving, velocity } = usePlayerMovement({\n enabled: true, // Always allow movement in training screen\n bounds: movementBounds, // Use memoized bounds object\n onPositionChange: handlePositionChange, // Use memoized callback\n initialPositionMeters,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n legInjuryFactor: 0, // No injury in training mode\n isRunning, // Use computed acceleration-based running state\n maxSpeedOverride: accelerationBasedSpeed,\n accelerationOverride: speedModifiers.finalAcceleration,\n });\n\n const player3DPosition = useMemo<[number, number, number]>(() => {\n return [playerPosition.x, 0, playerPosition.y];\n }, [playerPosition]);\n\n const dummyPosition = useMemo<[number, number, number]>(\n () => [trainingAreaBounds.worldWidthMeters * 0.15, 0, 0],\n [trainingAreaBounds.worldWidthMeters],\n );\n\n const centerToCenterDistance = useMemo(\n () => calculateDistance3D(player3DPosition, dummyPosition),\n [player3DPosition, dummyPosition],\n );\n\n const distanceToDummy = useMemo(\n () => Math.max(0, centerToCenterDistance - DEFAULT_BODY_RADIUS_METERS),\n [centerToCenterDistance],\n );\n\n const lastFacingRotationRef = useRef<number>(0);\n\n const playerRotation = useMemo(() => {\n if (isMoving && velocity && (velocity.x !== 0 || velocity.y !== 0)) {\n return Math.atan2(velocity.x, velocity.y);\n } else {\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return Math.atan2(dx, dz);\n }\n }, [isMoving, velocity, player3DPosition, dummyPosition]);\n\n useEffect(() => {\n lastFacingRotationRef.current = playerRotation;\n }, [playerRotation]);\n\n\n const [currentLaterality, setCurrentLaterality] = useState<\"left\" | \"right\">(\"right\");\n \n const stepCounterRef = useRef(0);\n const lastPositionRef = useRef<Position>(playerPosition);\n \n useEffect(() => {\n if (!isMoving) {\n stepCounterRef.current = 0;\n lastPositionRef.current = playerPosition;\n return;\n }\n\n const dx = playerPosition.x - lastPositionRef.current.x;\n const dy = playerPosition.y - lastPositionRef.current.y;\n const distanceMoved = Math.sqrt(dx * dx + dy * dy);\n \n const stepThreshold = isRunning \n ? STEP_DISTANCE_THRESHOLDS.RUN \n : STEP_DISTANCE_THRESHOLDS.WALK;\n stepCounterRef.current += distanceMoved;\n \n const stepsCrossed = Math.floor(stepCounterRef.current / stepThreshold);\n if (stepsCrossed > 0) {\n if (stepsCrossed % 2 === 1) {\n setCurrentLaterality(prev => prev === \"right\" ? \"left\" : \"right\");\n }\n stepCounterRef.current -= stepsCrossed * stepThreshold;\n }\n \n lastPositionRef.current = playerPosition;\n }, [playerPosition, isMoving, isRunning]);\n\n\n const pendingAttackRef = useRef<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>(null);\n\n const handleDummyHitRef = useRef<\n (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean\n >(() => false);\n\n const playerAnimationRef = useRef<ReturnType<\n typeof usePlayerAnimation\n > | null>(null);\n\n const playerAnimationEvents = useMemo<AnimationEvents>(\n () => ({\n onFrame: (frame, state) => {\n if (state === \"attack\" && frame === 6 && pendingAttackRef.current) {\n const attackData = pendingAttackRef.current;\n handleDummyHitRef.current(attackData.vitalPoint, {\n animationType: attackData.animationType,\n techniqueId: attackData.techniqueId,\n });\n pendingAttackRef.current = null;\n }\n },\n onAnimationComplete: (state) => {\n if (state === \"stance_change\") {\n playStanceChangeSound();\n const currentStance =\n TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex];\n if (currentStance && playerAnimationRef.current) {\n playerAnimationRef.current.transitionToStanceGuard(currentStance);\n }\n }\n },\n }),\n [playStanceChangeSound, trainingState.currentStanceIndex],\n );\n\n const playerAnimation = usePlayerAnimation({\n events: playerAnimationEvents,\n });\n\n useEffect(() => {\n playerAnimationRef.current = playerAnimation;\n }, [playerAnimation]);\n\n\n const currentStance = useMemo(\n () => TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n [trainingState.currentStanceIndex],\n );\n\n const [previousStanceIndex, setPreviousStanceIndex] = useState<number>(0);\n\n const currentTechniqueAnimationTypeRef = useRef<AnimationType>(\n AnimationType.JAB,\n );\n\n\n const trainingPlayerState = useMemo<PlayerState>(() => {\n return {\n id: \"training-player\",\n name: { korean: \"훈련생\", english: \"Trainee\" },\n archetype: selectedArchetype,\n health: 100,\n maxHealth: 100,\n ki: 100,\n maxKi: 100,\n stamina: 100,\n maxStamina: 100,\n energy: 100,\n maxEnergy: 100,\n attackPower: 10,\n defense: 10,\n speed: 10,\n technique: 10,\n pain: 0,\n consciousness: 100,\n balance: 100,\n momentum: 0,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n combatState: CombatState.IDLE,\n position: playerPosition,\n isBlocking: false,\n isStunned: false,\n isCountering: false,\n lastActionTime: 0,\n recoveryTime: 0,\n lastStanceChangeTime: 0,\n statusEffects: [],\n activeEffects: [],\n vitalPoints: [],\n totalDamageReceived: 0,\n totalDamageDealt: 0,\n hitsTaken: 0,\n hitsLanded: trainingState.stats.hits,\n perfectStrikes: trainingState.perfectStrikes,\n vitalPointHits: 0,\n misses: trainingState.stats.misses,\n accuracy: trainingState.stats.accuracy,\n comboCount: trainingState.stats.combo,\n };\n }, [playerPosition, trainingState, selectedArchetype]);\n\n useEffect(() => {\n const updateSpeedModifiers = () => {\n const walkModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.WALKING,\n false, // isCrouching\n );\n\n const runModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.RUNNING,\n false, // isCrouching\n );\n\n setSpeedModifiers({\n finalSpeed: walkModifiers.finalSpeed,\n baseSpeed: walkModifiers.baseSpeed,\n finalAcceleration: walkModifiers.finalAcceleration,\n });\n\n setWalkRunSpeeds({\n walkSpeed: walkModifiers.finalSpeed,\n runSpeed: runModifiers.finalSpeed,\n });\n };\n\n updateSpeedModifiers();\n\n const intervalId = setInterval(updateSpeedModifiers, 200);\n\n return () => clearInterval(intervalId);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [trainingPlayerState]); // speedModifierSystem is memoized and never changes\n\n\n const isPlayerAttacking = useMemo(\n () => playerAnimation?.currentState === \"attack\",\n [playerAnimation],\n );\n\n const attackDirection = useMemo(() => {\n if (!isPlayerAttacking) {\n return new THREE.Vector3(0, 0, 1); // Default forward direction\n }\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return new THREE.Vector3(dx, 0, dz).normalize();\n }, [dummyPosition, player3DPosition, isPlayerAttacking]);\n\n const {\n currentPosition: player3DPositionWithAttackMovement,\n } = useAttackMovement({\n isAttacking: isPlayerAttacking,\n // eslint-disable-next-line react-hooks/refs -- ref value is set synchronously before isAttacking becomes true; hook only reads this at attack start\n animationType: currentTechniqueAnimationTypeRef.current,\n currentStance: trainingPlayerState.currentStance,\n basePosition: player3DPosition,\n attackDirection,\n animationDuration: 0.4,\n });\n\n const finalPlayer3DPosition = isPlayerAttacking\n ? player3DPositionWithAttackMovement\n : player3DPosition;\n\n\n const handleAttackRef = useRef<(() => void) | null>(null);\n\n const techniqueSelection = useTechniqueSelection({\n player: trainingPlayerState,\n enabled: trainingState.isTraining,\n onTechniqueExecute: useCallback(\n (technique: Technique) => {\n trainingActions.setFeedback(\n `${technique.name.korean} 사용! | Used ${technique.name.english}!`,\n );\n\n const animationName = resolveTechniqueAnimation(technique);\n setAttackAnimation(animationName);\n\n\n handleAttackRef.current?.();\n },\n [trainingActions],\n ),\n });\n\n const selectedTechniqueId = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0 || selectedIdx < 0 || selectedIdx >= techniques.length) {\n return undefined;\n }\n return techniques[selectedIdx]?.id;\n }, [techniqueSelection.availableTechniques, techniqueSelection.selectedIndex]);\n\n const {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n } = useTrainingActions({\n state: trainingState,\n actions: trainingActions,\n playerPosition,\n player3DPosition,\n dummyPosition,\n playerArchetype: selectedArchetype,\n playerStance: currentStance,\n currentTechniqueAnimationTypeRef, // Ref for technique's animation type\n audio,\n playBoneImpactSound, // Pass bone impact audio function from useCombatAudio\n playAttackSound, // Pass attack sound function from useCombatAudio\n selectedTechniqueId, // Pass selected technique ID for intensity-based attack sounds\n onPlayerUpdate: (updates) => {\n onPlayerUpdate(updates);\n },\n playerAnimation: {\n transitionTo: playerAnimation.transitionTo,\n transitionToAttack: playerAnimation.transitionToAttack,\n transitionToStanceGuard: playerAnimation.transitionToStanceGuard,\n currentState: playerAnimation.currentState,\n },\n pendingAttackRef, // Share the ref with animation events\n });\n\n useEffect(() => {\n handleAttackRef.current = handleAttack;\n }, [handleAttack]);\n\n useEffect(() => {\n handleDummyHitRef.current = handleDummyHit;\n }, [handleDummyHit]);\n\n const handleStanceChangeWithVisualFeedback = useCallback(\n (stanceIndex: number) => {\n setPreviousStanceIndex(trainingState.currentStanceIndex);\n handleStanceChange(stanceIndex);\n },\n [handleStanceChange, trainingState.currentStanceIndex],\n );\n\n\n const prevIsMovingRef = useRef<boolean>(isMoving);\n const prevIsRunningRef = useRef<boolean>(isRunning);\n const prevStanceRef = useRef<TrigramStance>(currentStance);\n \n useEffect(() => {\n const isMovingChanged = prevIsMovingRef.current !== isMoving;\n const isRunningChanged = prevIsRunningRef.current !== isRunning;\n const stanceChanged = prevStanceRef.current !== currentStance;\n \n if (isMovingChanged || isRunningChanged) {\n if (isMoving) {\n if (isRunning) {\n playerAnimation.transitionTo(AnimationState.RUN);\n } else {\n playerAnimation.transitionTo(AnimationState.WALK);\n }\n } else if (playerAnimation.currentState === AnimationState.WALK || \n playerAnimation.currentState === AnimationState.RUN) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevIsMovingRef.current = isMoving;\n prevIsRunningRef.current = isRunning;\n }\n \n if (stanceChanged && !isMoving) {\n if (playerAnimation.currentState === AnimationState.IDLE || \n playerAnimation.isInStanceGuard()) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevStanceRef.current = currentStance;\n }\n }, [isMoving, isRunning, currentStance, playerAnimation]);\n\n\n const cooldownsMap = useMemo(() => {\n const map = new Map<string, number>();\n techniqueSelection.activeCooldowns.forEach((cd) => {\n map.set(cd.techniqueId, cd.remaining);\n });\n return map;\n }, [techniqueSelection.activeCooldowns]);\n\n const { currentTechniqueReach, currentAnimationType } = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const currentTechnique =\n techniques[Math.min(selectedIdx, techniques.length - 1)];\n if (!currentTechnique) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const animConfig = getAnimationForTechniqueOrDefault(currentTechnique.id);\n const physicalAttributes =\n getArchetypePhysicalAttributes(selectedArchetype);\n const reach = physicalReachCalculator.calculateMaxReach(\n physicalAttributes,\n animConfig.type,\n currentStance,\n );\n return {\n currentTechniqueReach: reach,\n currentAnimationType: animConfig.type,\n };\n }, [\n techniqueSelection.availableTechniques,\n techniqueSelection.selectedIndex,\n selectedArchetype,\n currentStance,\n ]);\n\n useEffect(() => {\n currentTechniqueAnimationTypeRef.current = currentAnimationType;\n }, [currentAnimationType]);\n\n\n const activeMobileKeyRef = useRef<string | null>(null);\n\n const mobileControlsEnabled = isMobile;\n\n const handleMobileMove = useCallback(\n (direction: Direction | null, eventType: DPadEventType) => {\n const directionMap: Record<Direction, string> = {\n up: \"w\",\n \"up-right\": \"w\",\n right: \"d\",\n \"down-right\": \"s\",\n down: \"s\",\n \"down-left\": \"s\",\n left: \"a\",\n \"up-left\": \"w\",\n };\n\n if (eventType === \"start\" && direction) {\n if (\n activeMobileKeyRef.current &&\n activeMobileKeyRef.current !== directionMap[direction]\n ) {\n const prevKey = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key: prevKey,\n code: `Key${prevKey.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n }\n\n const key = directionMap[direction];\n activeMobileKeyRef.current = key;\n window.dispatchEvent(\n new KeyboardEvent(\"keydown\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n } else if (eventType === \"end\") {\n if (activeMobileKeyRef.current) {\n const key = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n activeMobileKeyRef.current = null;\n }\n }\n },\n [],\n );\n\n const handleMobileAttack = useCallback(() => {\n handleAttack();\n }, [handleAttack]);\n\n const handleMobileBlock = useCallback(\n (eventType: ButtonEventType) => {\n if (eventType === \"start\") {\n audio.playSFX(\"block\");\n }\n },\n [audio],\n );\n\n const handleMobileGesture = useCallback(\n (gesture: GestureEvent) => {\n switch (gesture.type) {\n case \"swipe-right\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"d\" }));\n break;\n case \"swipe-left\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"a\" }));\n break;\n case \"swipe-up\":\n if (trainingState.isTraining) {\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \" \" }));\n }\n break;\n case \"swipe-down\":\n trainingActions.resetDummy();\n break;\n case \"two-finger-tap\":\n trainingActions.setTrainingMode(\n trainingState.trainingMode === \"vital_point\"\n ? \"basics\"\n : \"vital_point\",\n );\n audio.playSFX(\"menu_select\");\n break;\n }\n },\n [trainingState, trainingActions, audio],\n );\n\n const handleMobileStanceChange = useCallback(\n (stanceIndex: number) => {\n handleStanceChangeWithVisualFeedback(stanceIndex);\n },\n [handleStanceChangeWithVisualFeedback],\n );\n\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n const key = event.key.toLowerCase();\n\n if (key === \"escape\") {\n onReturnToMenu();\n return;\n }\n\n if (key >= \"1\" && key <= \"8\") {\n const stanceIndex = parseInt(key) - 1;\n handleStanceChangeWithVisualFeedback(stanceIndex);\n event.preventDefault();\n return;\n }\n\n if (key === \" \") {\n handleAttack();\n event.preventDefault();\n return;\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onReturnToMenu, handleStanceChangeWithVisualFeedback, handleAttack]);\n\n\n const hasMountedRef = useRef(false);\n\n useEffect(() => {\n let audioStarted = false;\n\n const startMusic = async () => {\n try {\n await audio.fadeIn(\"cyberpunk_fusion\", 2000);\n audioStarted = true;\n } catch (err) {\n console.warn(\"Failed to start training music:\", err);\n trainingActions.setFeedback(\n \"오디오 초기화 실패 | Audio initialization failed\",\n );\n }\n };\n\n void startMusic();\n\n return () => {\n if (audioStarted) {\n void audio\n .fadeOut(2000)\n .then(() => audio.stopMusic())\n .catch((err) => console.warn(\"Failed to stop training music:\", err));\n }\n };\n }, [audio, trainingActions]);\n\n useEffect(() => {\n if (!hasMountedRef.current) {\n hasMountedRef.current = true;\n handleStartTraining();\n }\n }, [handleStartTraining]);\n\n\n useEffect(() => {\n if (trainingState.showFeedback) {\n const timer = setTimeout(() => trainingActions.hideFeedback(), 1500);\n return () => clearTimeout(timer);\n }\n }, [trainingState.showFeedback, trainingState.feedback, trainingActions]);\n\n useEffect(() => {\n if (!trainingState.isTraining || !trainingState.sessionStartTime) return;\n\n const interval = setInterval(() => {\n trainingActions.updateSessionDuration(\n Math.floor((Date.now() - (trainingState.sessionStartTime ?? 0)) / 1000),\n );\n }, 1000);\n\n return () => clearInterval(interval);\n }, [\n trainingState.isTraining,\n trainingState.sessionStartTime,\n trainingActions,\n ]);\n\n const prevTrainingModeRef = useRef<typeof trainingState.trainingMode>(\n trainingState.trainingMode,\n );\n const isFirstModeEffectRef = useRef<boolean>(true);\n const isTrainingRef = useRef<boolean>(trainingState.isTraining);\n const modeChangeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n isTrainingRef.current = trainingState.isTraining;\n }, [trainingState.isTraining]);\n\n const handleStartTrainingRef = useRef(handleStartTraining);\n const handleStopTrainingRef = useRef(handleStopTraining);\n\n useEffect(() => {\n handleStartTrainingRef.current = handleStartTraining;\n handleStopTrainingRef.current = handleStopTraining;\n }, [handleStartTraining, handleStopTraining]);\n\n useEffect(() => {\n if (isFirstModeEffectRef.current) {\n isFirstModeEffectRef.current = false;\n prevTrainingModeRef.current = trainingState.trainingMode;\n return;\n }\n\n const previousMode = prevTrainingModeRef.current;\n const modeChanged = previousMode !== trainingState.trainingMode;\n\n if (!modeChanged) {\n return;\n }\n\n prevTrainingModeRef.current = trainingState.trainingMode;\n\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n\n if (isTrainingRef.current) {\n handleStopTrainingRef.current();\n }\n\n modeChangeTimerRef.current = setTimeout(() => {\n handleStartTrainingRef.current();\n modeChangeTimerRef.current = null;\n }, 100);\n\n return () => {\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n };\n }, [trainingState.trainingMode]); // Only depend on training mode to avoid unnecessary re-runs\n\n\n const handleEffectComplete = useCallback(\n (effectId: number) => {\n trainingActions.removeHitEffect(effectId);\n },\n [trainingActions],\n );\n\n\n const handleAnatomyLayerToggle = useCallback(\n (layer: AnatomyLayer) => {\n trainingActions.toggleAnatomyLayer(layer);\n audio.playSFX(\"menu_click\");\n },\n [trainingActions, audio],\n );\n\n const handleVitalPointClick = useCallback(\n (pointId: string) => {\n trainingActions.setSelectedVitalPoint(pointId);\n audio.playSFX(\"menu_select\");\n },\n [trainingActions, audio],\n );\n\n\n const cameraConfig = useMemo(() => {\n const base = createCameraConfig(isMobile);\n if (!isPortrait) return base;\n return {\n ...base,\n fov: Math.min(80, base.fov + 15),\n position: [base.position[0], base.position[1], base.position[2] + 4] as [\n number,\n number,\n number,\n ],\n };\n }, [isMobile, isPortrait]);\n\n\n const performanceSettings = useMemo(() => {\n return getPerformanceSettings(width, isMobile);\n }, [width, isMobile]);\n\n\n return (\n <div\n style={{\n width: `${width}px`,\n height: `${height}px`,\n position: \"relative\",\n overflow: \"hidden\", // Prevent content from extending beyond container\n }}\n data-testid=\"training-screen-3d\"\n >\n <Canvas\n style={{ width: `${width}px`, height: `${height}px` }}\n gl={{\n antialias: performanceSettings.antialias,\n alpha: false,\n powerPreference: \"high-performance\",\n failIfMajorPerformanceCaveat: false, // Don't fail in software renderer\n preserveDrawingBuffer: true, // Help with context stability\n }}\n dpr={performanceSettings.dpr}\n shadows={false} // Temporarily disable shadows\n onCreated={({ gl }) => {\n gl.setClearColor(theme.colors.UI_BACKGROUND_DARK, 1);\n }}\n camera={cameraConfig}\n >\n {/* Lighting - base lighting, arena provides additional */}\n <ambientLight intensity={0.6} />\n <directionalLight position={[10, 10, 5]} intensity={1.2} />\n\n {/* Combat Arena 3D Environment - uses physics-based world dimensions */}\n <CombatArena3D\n lighting=\"cyberpunk\"\n scale={trainingAreaBounds.scale}\n worldWidthMeters={trainingAreaBounds.worldWidthMeters}\n worldDepthMeters={trainingAreaBounds.worldDepthMeters}\n />\n\n {/* Animation updater - 60fps updates */}\n <TrainingAnimationUpdater playerAnimation={playerAnimation} />\n\n {/* Acceleration updater - tracks movement time and updates speed */}\n <AccelerationUpdater\n isMoving={isMoving}\n velocity={velocity}\n movementTimeRef={movementTimeRef}\n lastDirectionRef={lastDirectionRef}\n onSpeedUpdate={setAccelerationBasedSpeed}\n walkSpeed={walkRunSpeeds.walkSpeed}\n runSpeed={walkRunSpeeds.runSpeed}\n />\n\n {/* Training dummy at fixed position */}\n <TrainingDummy3D\n position={dummyPosition}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n isTraining={trainingState.isTraining}\n health={trainingState.dummyHealth}\n onVitalPointHit={handleDummyHit}\n onDefeated={handleDummyDefeated}\n difficulty={difficulty}\n vitalPointCount={vitalPointCount}\n isMobile={isMobile}\n />\n\n {/* Anatomy overlay for educational visualization */}\n {trainingState.visibleAnatomyLayers.length > 0 && (\n <AnatomyOverlay3D\n position={dummyPosition}\n visibleLayers={trainingState.visibleAnatomyLayers}\n opacity={0.6}\n isMobile={isMobile}\n />\n )}\n\n {/* Vital Point Overlay - Show all 70 points on dummy */}\n {overlayVisible && (\n <VitalPointMarkers3D\n position={dummyPosition}\n visible={overlayVisible}\n severityFilter={severityFilters}\n regionFilter={regionFilter}\n searchQuery={searchQuery}\n showLabels={showLabels}\n scale={scale}\n animated={animated}\n selectedPoint={trainingState.selectedVitalPoint}\n onPointClick={handleVitalPointClick}\n />\n )}\n\n {/* Player model */}\n <Player3DWithTransitions\n {...convertPlayerStateToProps(\n trainingPlayerState,\n finalPlayer3DPosition,\n playerRotation,\n {\n isMobile,\n facing: \"right\",\n enableFacialExpressions: true,\n enableEyeTracking: true,\n opponentPosition: dummyPosition,\n },\n )}\n currentAnimation={animationStateToPlayerAnimation(\n playerAnimation.currentState,\n )}\n attackAnimation={attackAnimation}\n laterality={currentLaterality}\n enableTransitionEffects={!isMobile}\n enableStanceSymbol={true}\n enableStanceAudio={true}\n />\n\n {/* Foot Placement Markers for Footwork Drills */}\n {trainingState.trainingMode === \"footwork\" &&\n trainingState.footworkDrillActive && (\n <FootPlacementMarkers3D\n centerPosition={dummyPosition}\n pattern={\n trainingState.footworkDrillType === \"free_practice\"\n ? \"none\"\n : trainingState.footworkDrillType\n }\n currentStep={trainingState.footworkDrillStep}\n visible={true}\n scale={1.0}\n animated={true}\n />\n )}\n\n {/* Hit effects */}\n {trainingState.hitEffects.map((effect) => (\n <HitFeedbackEffect3D\n key={effect.id}\n position={effect.position}\n type={effect.type}\n damage={effect.damage}\n visible={effect.visible}\n onComplete={() => handleEffectComplete(effect.id)}\n isMobile={isMobile}\n />\n ))}\n\n {/* Stance Change Visual Indicator */}\n <StanceChangeIndicator\n currentStance={trainingState.currentStanceIndex}\n previousStance={previousStanceIndex}\n isMobile={isMobile}\n />\n\n {/* NOTE: Mobile controls moved OUTSIDE Canvas for reliable touch events */}\n {/* See MobileControlsPure component rendered after HUDs */}\n\n {/* Post-processing Effects - desktop high tier only for Android WebGL stability */}\n {performanceSettings.postProcessing && (\n <EffectComposer multisampling={4}>\n <Bloom\n luminanceThreshold={0.9}\n luminanceSmoothing={0.9}\n mipmapBlur\n intensity={0.8}\n radius={0.4}\n />\n <Noise opacity={0.03} />\n <Vignette eskil={false} offset={0.1} darkness={0.3} />\n </EffectComposer>\n )}\n </Canvas>\n\n {/* Html UI Overlays (positioned absolutely over Canvas) - matches CombatScreen pattern */}\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: Z_INDEX.HUD,\n overflow: \"clip\",\n }}\n data-testid=\"training-hud-overlay\"\n >\n {/* Left HUD - Anatomy Controls, Guard Indicator.\n Hidden on mobile because the side HUD occludes the compressed\n arena in both portrait and landscape. Anatomy layer toggles remain\n available on larger viewports where there is room for side panels. */}\n {!isMobile && (\n <TrainingLeftHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n visibleAnatomyLayers={trainingState.visibleAnatomyLayers}\n onAnatomyLayerToggle={handleAnatomyLayerToggle}\n currentStanceIndex={trainingState.currentStanceIndex}\n isInGuard={playerAnimation.isInStanceGuard()}\n />\n )}\n\n {/* Top HUD - Training Controls, Archetype Selector, Return Button. */}\n <TrainingTopHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n isTraining={trainingState.isTraining}\n onStartTraining={handleStartTraining}\n onStopTraining={handleStopTraining}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n overlayVisible={overlayVisible}\n onReturnToMenu={onReturnToMenu}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Right HUD - Mode Selector, Stats, Vital Point Selection.\n Hidden on mobile to keep the training dojang visible and usable.\n The core start/stop, archetype, vital-point toggle, technique bar,\n stance wheel, gestures, and touch controls remain available. */}\n {!isMobile && (\n <TrainingRightHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n trainingMode={trainingState.trainingMode}\n onModeChange={trainingActions.setTrainingMode}\n stats={{\n ...trainingState.stats,\n sessionDuration: trainingState.sessionDuration,\n bestCombo: trainingState.bestCombo,\n perfectStrikes: trainingState.perfectStrikes,\n }}\n distanceToDummy={distanceToDummy}\n effectiveReach={currentTechniqueReach}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n onVitalPointSelect={trainingActions.setSelectedVitalPoint}\n footworkDrillType={trainingState.footworkDrillType}\n footworkDrillStep={trainingState.footworkDrillStep}\n footworkDrillActive={trainingState.footworkDrillActive}\n onStartFootworkDrill={trainingActions.startFootworkDrill}\n onStopFootworkDrill={trainingActions.stopFootworkDrill}\n onAdvanceFootworkStep={trainingActions.advanceFootworkStep}\n />\n )}\n {/* Bottom HUD - Technique Bar, Feedback Messages, Mobile Controls */}\n <TrainingBottomHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n techniques={techniqueSelection.availableTechniques}\n player={trainingPlayerState}\n selectedIndex={techniqueSelection.selectedIndex}\n cooldowns={cooldownsMap}\n onTechniqueSelect={techniqueSelection.selectTechnique}\n showFeedback={trainingState.showFeedback}\n feedbackMessage={trainingState.feedback}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Vital Point Overlay Controls - Pure DOM overlay (outside Canvas) */}\n {overlayVisible && (\n <VitalPointOverlayControlsPure\n visible={overlayVisible}\n onVisibleChange={setOverlayVisible}\n severityFilters={severityFilters}\n onSeverityFiltersChange={setSeverityFilters}\n regionFilter={regionFilter}\n onRegionFilterChange={setRegionFilter}\n searchQuery={searchQuery}\n onSearchQueryChange={setSearchQuery}\n showLabels={showLabels}\n onShowLabelsChange={setShowLabels}\n animated={animated}\n onAnimatedChange={setAnimated}\n scale={scale}\n onScaleChange={setScale}\n screenPosition={{ top: \"180px\", left: \"20px\" }}\n isMobile={isMobile}\n />\n )}\n\n {/* Mobile Controls - Pure DOM overlay (outside Canvas for reliable touch) */}\n {isMobile && (\n <>\n <MobileControlsOverlay\n onMove={handleMobileMove}\n onAttack={handleMobileAttack}\n onBlock={handleMobileBlock}\n disabled={!mobileControlsEnabled}\n bottom={getMobileControlsBottom(height)}\n opacity={0.85}\n viewportWidth={width}\n viewportHeight={height}\n />\n\n <StanceWheelPure\n currentStance={trainingState.currentStanceIndex}\n onStanceChange={handleMobileStanceChange}\n expanded={trainingState.stanceWheelExpanded}\n onToggle={trainingActions.toggleStanceWheel}\n disabled={!mobileControlsEnabled}\n opacity={0.8}\n />\n\n <GestureRecognizerPure\n onGesture={handleMobileGesture}\n enabled={mobileControlsEnabled}\n showFeedback={true}\n minSwipeDistance={50}\n />\n </>\n )}\n </div>\n </div>\n );\n};\n\nexport default TrainingScreen3D;\n"],"mappings":"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"}
1
+ {"version":3,"file":"TrainingScreen3D.js","names":[],"sources":["../../../../src/components/screens/training/TrainingScreen3D.tsx"],"sourcesContent":["/**\n * TrainingScreen3D - Three.js-based training screen\n *\n * Refactored to use consolidated hooks matching CombatScreen architecture.\n * Provides 3D training dummy with vital point targeting and UI overlays.\n *\n * UI Rendering: All HUD elements are rendered in an absolute-positioned div\n * OUTSIDE the Canvas, matching CombatScreen's reliable rendering pattern.\n * This eliminates the need for Html overlays inside Three.js and ensures\n * HUDs appear immediately without waiting for Canvas initialization.\n *\n * Architecture (Consolidated in PR #1394 + Issue #1398):\n * - TrainingLeftHUD: Anatomy controls, guard indicator\n * - TrainingRightHUD: Training stats, mode selector, vital point selection\n * - TrainingTopHUD: Training controls, archetype selector, return button\n * - TrainingBottomHUD: Technique bar, feedback messages, mobile controls\n * - VitalPointOverlayControlsPure: Vital point overlay controls (pure DOM)\n *\n * All UI components render as pure DOM in the HUD overlay div (lines 1230+).\n * NO Html components from @react-three/drei are used inside the Canvas.\n * This ensures clean separation of 3D rendering and UI layers.\n *\n * @korean 훈련화면3D - 훈련 상태 훅을 사용한 리팩토링된 3D 훈련 화면\n */\n\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { AccelerationUpdater } from \"../../../systems/movement/helpers/AccelerationUpdater\";\nimport {\n isRunningSpeed,\n STEP_DISTANCE_THRESHOLDS,\n} from \"../../../systems/movement/helpers/accelerationUtils\";\nimport {\n Bloom,\n EffectComposer,\n Noise,\n Vignette,\n} from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { useCombatAudio } from \"../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../data/archetypePhysicalAttributes\";\nimport { usePlayerAnimation } from \"../../../hooks/usePlayerAnimation\";\nimport { useTechniqueSelection } from \"../../../hooks/useTechniqueSelection\";\nimport { GestureEvent } from \"../../../hooks/useTouchControls\";\nimport { useWebGLContextLossHandler } from \"../../../hooks/useWebGLContextLossHandler\";\nimport { PlayerState } from \"../../../systems\";\nimport {\n AnimationEvents,\n AnimationState,\n AnimationType,\n resolveTechniqueAnimation,\n} from \"../../../systems/animation\";\nimport { getAnimationForTechniqueOrDefault } from \"../../../systems/animation/core/TechniqueAnimationMapping\";\nimport { physicalReachCalculator } from \"../../../systems/physics\";\nimport {\n MovementType,\n SpeedModifierSystem,\n} from \"../../../systems/physics/SpeedModifierSystem\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport {\n CombatState,\n PlayerArchetype,\n Position,\n Technique,\n TrigramStance,\n} from \"../../../types\";\nimport { getPerformanceSettings } from \"../../../types/constants\";\nimport { getMobileControlsBottom } from \"../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { DEFAULT_BODY_RADIUS_METERS } from \"../../../types/physicsConstants\";\nimport { usePlayerMovement } from \"../../../utils/inputSystem\";\nimport { calculateDistance3D } from \"../../../utils/math\";\nimport { getHUDPositionScale } from \"../../../utils/responsiveLayoutHelpers\";\nimport { createCameraConfig } from \"../../../utils/sharedPhysicsConfig\";\nimport {\n animationStateToPlayerAnimation,\n convertPlayerStateToProps,\n} from \"../../../utils/player3DHelpers\";\nimport { useKoreanTheme } from \"../../shared/base/useKoreanTheme\";\nimport {\n GestureRecognizerPure,\n StanceWheelPure,\n} from \"../../shared/mobile\";\nimport {\n MobileControlsOverlay,\n type ButtonEventType,\n type Direction,\n type DPadEventType,\n} from \"../../shared/mobile/MobileControlsPure\";\nimport {\n Player3DWithTransitions,\n VitalPointMarkers3D,\n type BodyRegionFilter,\n} from \"../../shared/three\";\nimport { StanceChangeIndicator } from \"../../shared/three/indicators/StanceChangeIndicator\";\nimport { CombatArena3D } from \"../../shared/three/scene/CombatArena3D\";\nimport { VitalPointOverlayControlsPure } from \"../../shared/ui/VitalPointOverlayControlsPure\";\nimport AnatomyOverlay3D, {\n type AnatomyLayer,\n} from \"./components/AnatomyOverlay3D\";\nimport FootPlacementMarkers3D from \"./components/FootPlacementMarkers3D\";\nimport HitFeedbackEffect3D from \"./components/HitFeedbackEffect3D\";\nimport type { DifficultyMode } from \"./components/TrainingDummy3D\";\nimport TrainingDummy3D from \"./components/TrainingDummy3D\";\nimport {\n TrainingBottomHUD,\n TrainingLeftHUD,\n TrainingRightHUD,\n TrainingTopHUD,\n} from \"./components/hud\";\nimport { useAttackMovement } from \"./hooks/useAttackMovement\";\nimport useTrainingActions from \"./hooks/useTrainingActions\";\nimport { useTrainingLayout } from \"./hooks/useTrainingLayout\";\nimport useTrainingState from \"./hooks/useTrainingState\";\n\n/**\n * AnimationUpdater - Component that updates player animation at 60fps\n *\n * @korean 훈련애니메이션업데이터 - 60fps로 플레이어 애니메이션을 업데이트하는 컴포넌트\n */\ninterface TrainingAnimationUpdaterProps {\n readonly playerAnimation: ReturnType<typeof usePlayerAnimation>;\n}\n\nconst TrainingAnimationUpdater: React.FC<TrainingAnimationUpdaterProps> = ({\n playerAnimation,\n}) => {\n useFrame((_state, delta) => {\n playerAnimation.update(delta);\n });\n\n return null;\n};\n\n/**\n * Props for the TrainingScreen3D component\n */\nexport interface TrainingScreen3DProps {\n /** Callback to update player state */\n readonly onPlayerUpdate: (updates: Partial<PlayerState>) => void;\n /** Callback when returning to menu */\n readonly onReturnToMenu: () => void;\n /** Canvas width in pixels. Defaults to 1200 */\n readonly width?: number;\n /** Canvas height in pixels. Defaults to 800 */\n readonly height?: number;\n /** Initial archetype from IntroScreen selection. Defaults to MUSA */\n readonly initialArchetype?: PlayerArchetype;\n}\n\n/**\n * TrainingScreen3D Component\n * Three.js-based training screen with 3D dummy and Html UI\n *\n * Uses consolidated hooks for state management matching CombatScreen architecture.\n */\nexport const TrainingScreen3D: React.FC<TrainingScreen3DProps> = ({\n onPlayerUpdate,\n onReturnToMenu,\n width = 1200,\n height = 800,\n initialArchetype = PlayerArchetype.MUSA,\n}) => {\n\n\n const { state: trainingState, actions: trainingActions } = useTrainingState();\n\n const audio = useAudio();\n \n const { playBoneImpactSound, playAttackSound, playStanceChangeSound } =\n useCombatAudio();\n\n const { trainingAreaBounds, isMobile, isPortrait, screenSize } =\n useTrainingLayout(width, height);\n\n const theme = useKoreanTheme({\n variant: \"primary\",\n size: \"md\",\n isMobile,\n });\n\n const positionScale = React.useMemo(\n () => getHUDPositionScale(screenSize, isMobile),\n [screenSize, isMobile],\n );\n\n const difficulty: DifficultyMode = \"normal\";\n const vitalPointCount = 70; // Show all 70 vital points\n\n const [selectedArchetype, setSelectedArchetype] =\n React.useState<PlayerArchetype>(initialArchetype);\n\n const [overlayVisible, setOverlayVisible] = React.useState(false);\n const [severityFilters, setSeverityFilters] = React.useState<\n import(\"../../../types/common\").VitalPointSeverity[]\n >([]);\n const [regionFilter, setRegionFilter] =\n React.useState<BodyRegionFilter>(\"all\");\n const [searchQuery, setSearchQuery] = React.useState(\"\");\n const [showLabels, setShowLabels] = React.useState(true);\n const [animated, setAnimated] = React.useState(true);\n const [scale, setScale] = React.useState(1.2);\n\n\n const [attackAnimation, setAttackAnimation] = React.useState<\n string | undefined\n >(undefined);\n\n React.useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"v\" || e.key === \"V\") {\n setOverlayVisible((prev) => !prev);\n audio.playSFX(\"menu_select\");\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, [audio]);\n\n\n const contextLossCountRef = useRef(0);\n\n useWebGLContextLossHandler({\n onContextLost: () => {\n console.warn(\"⚠️ WebGL context lost in TrainingScreen\");\n contextLossCountRef.current += 1;\n },\n onContextRestored: () => {\n console.log(\"✓ WebGL context restored in TrainingScreen\");\n },\n autoRestore: true,\n });\n\n\n const speedModifierSystem = useMemo(() => new SpeedModifierSystem(), []);\n\n const [speedModifiers, setSpeedModifiers] = useState({\n finalSpeed: 6.0, // BASE_WALK_SPEED (6.0 m/s for responsive combat)\n baseSpeed: 6.0,\n finalAcceleration: 12.0, // BASE_ACCELERATION (12.0 m/s² for quick response)\n });\n\n const [walkRunSpeeds, setWalkRunSpeeds] = useState({\n walkSpeed: 6.0,\n runSpeed: 10.0,\n });\n\n\n const initialPositionMeters = useMemo<Position>(\n () => ({\n x: trainingAreaBounds.worldWidthMeters * 0.0, // Centered laterally\n y: 0, // Centered vertically\n }),\n [trainingAreaBounds],\n );\n\n const handlePositionChange = useCallback(\n (newPosition: Position) => {\n onPlayerUpdate({ position: newPosition });\n },\n [onPlayerUpdate],\n );\n\n const movementBounds = useMemo(\n () => ({\n worldWidthMeters: trainingAreaBounds.worldWidthMeters,\n worldDepthMeters: trainingAreaBounds.worldDepthMeters,\n }),\n [trainingAreaBounds.worldWidthMeters, trainingAreaBounds.worldDepthMeters],\n );\n\n \n const movementTimeRef = useRef(0);\n const lastDirectionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n \n const [accelerationBasedSpeed, setAccelerationBasedSpeed] = useState(\n walkRunSpeeds.walkSpeed\n );\n \n const isRunning = isRunningSpeed(accelerationBasedSpeed, walkRunSpeeds.runSpeed);\n\n const { playerPosition, isMoving, velocity } = usePlayerMovement({\n enabled: true, // Always allow movement in training screen\n bounds: movementBounds, // Use memoized bounds object\n onPositionChange: handlePositionChange, // Use memoized callback\n initialPositionMeters,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n legInjuryFactor: 0, // No injury in training mode\n isRunning, // Use computed acceleration-based running state\n maxSpeedOverride: accelerationBasedSpeed,\n accelerationOverride: speedModifiers.finalAcceleration,\n });\n\n const player3DPosition = useMemo<[number, number, number]>(() => {\n return [playerPosition.x, 0, playerPosition.y];\n }, [playerPosition]);\n\n const dummyPosition = useMemo<[number, number, number]>(\n () => [trainingAreaBounds.worldWidthMeters * 0.15, 0, 0],\n [trainingAreaBounds.worldWidthMeters],\n );\n\n const centerToCenterDistance = useMemo(\n () => calculateDistance3D(player3DPosition, dummyPosition),\n [player3DPosition, dummyPosition],\n );\n\n const distanceToDummy = useMemo(\n () => Math.max(0, centerToCenterDistance - DEFAULT_BODY_RADIUS_METERS),\n [centerToCenterDistance],\n );\n\n const lastFacingRotationRef = useRef<number>(0);\n\n const playerRotation = useMemo(() => {\n if (isMoving && velocity && (velocity.x !== 0 || velocity.y !== 0)) {\n return Math.atan2(velocity.x, velocity.y);\n } else {\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return Math.atan2(dx, dz);\n }\n }, [isMoving, velocity, player3DPosition, dummyPosition]);\n\n useEffect(() => {\n lastFacingRotationRef.current = playerRotation;\n }, [playerRotation]);\n\n\n const [currentLaterality, setCurrentLaterality] = useState<\"left\" | \"right\">(\"right\");\n \n const stepCounterRef = useRef(0);\n const lastPositionRef = useRef<Position>(playerPosition);\n \n useEffect(() => {\n if (!isMoving) {\n stepCounterRef.current = 0;\n lastPositionRef.current = playerPosition;\n return;\n }\n\n const dx = playerPosition.x - lastPositionRef.current.x;\n const dy = playerPosition.y - lastPositionRef.current.y;\n const distanceMoved = Math.sqrt(dx * dx + dy * dy);\n \n const stepThreshold = isRunning \n ? STEP_DISTANCE_THRESHOLDS.RUN \n : STEP_DISTANCE_THRESHOLDS.WALK;\n stepCounterRef.current += distanceMoved;\n \n const stepsCrossed = Math.floor(stepCounterRef.current / stepThreshold);\n if (stepsCrossed > 0) {\n if (stepsCrossed % 2 === 1) {\n setCurrentLaterality(prev => prev === \"right\" ? \"left\" : \"right\");\n }\n stepCounterRef.current -= stepsCrossed * stepThreshold;\n }\n \n lastPositionRef.current = playerPosition;\n }, [playerPosition, isMoving, isRunning]);\n\n\n const pendingAttackRef = useRef<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>(null);\n\n const handleDummyHitRef = useRef<\n (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean\n >(() => false);\n\n const playerAnimationRef = useRef<ReturnType<\n typeof usePlayerAnimation\n > | null>(null);\n\n const playerAnimationEvents = useMemo<AnimationEvents>(\n () => ({\n onFrame: (frame, state) => {\n if (state === \"attack\" && frame === 6 && pendingAttackRef.current) {\n const attackData = pendingAttackRef.current;\n handleDummyHitRef.current(attackData.vitalPoint, {\n animationType: attackData.animationType,\n techniqueId: attackData.techniqueId,\n });\n pendingAttackRef.current = null;\n }\n },\n onAnimationComplete: (state) => {\n if (state === \"stance_change\") {\n playStanceChangeSound();\n const currentStance =\n TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex];\n if (currentStance && playerAnimationRef.current) {\n playerAnimationRef.current.transitionToStanceGuard(currentStance);\n }\n }\n },\n }),\n [playStanceChangeSound, trainingState.currentStanceIndex],\n );\n\n const playerAnimation = usePlayerAnimation({\n events: playerAnimationEvents,\n });\n\n useEffect(() => {\n playerAnimationRef.current = playerAnimation;\n }, [playerAnimation]);\n\n\n const currentStance = useMemo(\n () => TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n [trainingState.currentStanceIndex],\n );\n\n const [previousStanceIndex, setPreviousStanceIndex] = useState<number>(0);\n\n const currentTechniqueAnimationTypeRef = useRef<AnimationType>(\n AnimationType.JAB,\n );\n\n\n const trainingPlayerState = useMemo<PlayerState>(() => {\n return {\n id: \"training-player\",\n name: { korean: \"훈련생\", english: \"Trainee\" },\n archetype: selectedArchetype,\n health: 100,\n maxHealth: 100,\n ki: 100,\n maxKi: 100,\n stamina: 100,\n maxStamina: 100,\n energy: 100,\n maxEnergy: 100,\n attackPower: 10,\n defense: 10,\n speed: 10,\n technique: 10,\n pain: 0,\n consciousness: 100,\n balance: 100,\n momentum: 0,\n currentStance: TRIGRAM_STANCES_ORDER[trainingState.currentStanceIndex],\n combatState: CombatState.IDLE,\n position: playerPosition,\n isBlocking: false,\n isStunned: false,\n isCountering: false,\n lastActionTime: 0,\n recoveryTime: 0,\n lastStanceChangeTime: 0,\n statusEffects: [],\n activeEffects: [],\n vitalPoints: [],\n totalDamageReceived: 0,\n totalDamageDealt: 0,\n hitsTaken: 0,\n hitsLanded: trainingState.stats.hits,\n perfectStrikes: trainingState.perfectStrikes,\n vitalPointHits: 0,\n misses: trainingState.stats.misses,\n accuracy: trainingState.stats.accuracy,\n comboCount: trainingState.stats.combo,\n };\n }, [playerPosition, trainingState, selectedArchetype]);\n\n useEffect(() => {\n const updateSpeedModifiers = () => {\n const walkModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.WALKING,\n false, // isCrouching\n );\n\n const runModifiers = speedModifierSystem.calculateSpeedModifiers(\n trainingPlayerState,\n MovementType.RUNNING,\n false, // isCrouching\n );\n\n setSpeedModifiers({\n finalSpeed: walkModifiers.finalSpeed,\n baseSpeed: walkModifiers.baseSpeed,\n finalAcceleration: walkModifiers.finalAcceleration,\n });\n\n setWalkRunSpeeds({\n walkSpeed: walkModifiers.finalSpeed,\n runSpeed: runModifiers.finalSpeed,\n });\n };\n\n updateSpeedModifiers();\n\n const intervalId = setInterval(updateSpeedModifiers, 200);\n\n return () => clearInterval(intervalId);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [trainingPlayerState]); // speedModifierSystem is memoized and never changes\n\n\n const isPlayerAttacking = useMemo(\n () => playerAnimation?.currentState === \"attack\",\n [playerAnimation],\n );\n\n const attackDirection = useMemo(() => {\n if (!isPlayerAttacking) {\n return new THREE.Vector3(0, 0, 1); // Default forward direction\n }\n const dx = dummyPosition[0] - player3DPosition[0];\n const dz = dummyPosition[2] - player3DPosition[2];\n return new THREE.Vector3(dx, 0, dz).normalize();\n }, [dummyPosition, player3DPosition, isPlayerAttacking]);\n\n const {\n currentPosition: player3DPositionWithAttackMovement,\n } = useAttackMovement({\n isAttacking: isPlayerAttacking,\n // eslint-disable-next-line react-hooks/refs -- ref value is set synchronously before isAttacking becomes true; hook only reads this at attack start\n animationType: currentTechniqueAnimationTypeRef.current,\n currentStance: trainingPlayerState.currentStance,\n basePosition: player3DPosition,\n attackDirection,\n animationDuration: 0.4,\n });\n\n const finalPlayer3DPosition = isPlayerAttacking\n ? player3DPositionWithAttackMovement\n : player3DPosition;\n\n\n const handleAttackRef = useRef<(() => void) | null>(null);\n\n const techniqueSelection = useTechniqueSelection({\n player: trainingPlayerState,\n enabled: trainingState.isTraining,\n onTechniqueExecute: useCallback(\n (technique: Technique) => {\n trainingActions.setFeedback(\n `${technique.name.korean} 사용! | Used ${technique.name.english}!`,\n );\n\n const animationName = resolveTechniqueAnimation(technique);\n setAttackAnimation(animationName);\n\n\n handleAttackRef.current?.();\n },\n [trainingActions],\n ),\n });\n\n const selectedTechniqueId = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0 || selectedIdx < 0 || selectedIdx >= techniques.length) {\n return undefined;\n }\n return techniques[selectedIdx]?.id;\n }, [techniqueSelection.availableTechniques, techniqueSelection.selectedIndex]);\n\n const {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n } = useTrainingActions({\n state: trainingState,\n actions: trainingActions,\n playerPosition,\n player3DPosition,\n dummyPosition,\n playerArchetype: selectedArchetype,\n playerStance: currentStance,\n currentTechniqueAnimationTypeRef, // Ref for technique's animation type\n audio,\n playBoneImpactSound, // Pass bone impact audio function from useCombatAudio\n playAttackSound, // Pass attack sound function from useCombatAudio\n selectedTechniqueId, // Pass selected technique ID for intensity-based attack sounds\n onPlayerUpdate: (updates) => {\n onPlayerUpdate(updates);\n },\n playerAnimation: {\n transitionTo: playerAnimation.transitionTo,\n transitionToAttack: playerAnimation.transitionToAttack,\n transitionToStanceGuard: playerAnimation.transitionToStanceGuard,\n currentState: playerAnimation.currentState,\n },\n pendingAttackRef, // Share the ref with animation events\n });\n\n useEffect(() => {\n handleAttackRef.current = handleAttack;\n }, [handleAttack]);\n\n useEffect(() => {\n handleDummyHitRef.current = handleDummyHit;\n }, [handleDummyHit]);\n\n const handleStanceChangeWithVisualFeedback = useCallback(\n (stanceIndex: number) => {\n setPreviousStanceIndex(trainingState.currentStanceIndex);\n handleStanceChange(stanceIndex);\n },\n [handleStanceChange, trainingState.currentStanceIndex],\n );\n\n\n const prevIsMovingRef = useRef<boolean>(isMoving);\n const prevIsRunningRef = useRef<boolean>(isRunning);\n const prevStanceRef = useRef<TrigramStance>(currentStance);\n \n useEffect(() => {\n const isMovingChanged = prevIsMovingRef.current !== isMoving;\n const isRunningChanged = prevIsRunningRef.current !== isRunning;\n const stanceChanged = prevStanceRef.current !== currentStance;\n \n if (isMovingChanged || isRunningChanged) {\n if (isMoving) {\n if (isRunning) {\n playerAnimation.transitionTo(AnimationState.RUN);\n } else {\n playerAnimation.transitionTo(AnimationState.WALK);\n }\n } else if (playerAnimation.currentState === AnimationState.WALK || \n playerAnimation.currentState === AnimationState.RUN) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevIsMovingRef.current = isMoving;\n prevIsRunningRef.current = isRunning;\n }\n \n if (stanceChanged && !isMoving) {\n if (playerAnimation.currentState === AnimationState.IDLE || \n playerAnimation.isInStanceGuard()) {\n playerAnimation.transitionToStanceGuard(currentStance);\n }\n prevStanceRef.current = currentStance;\n }\n }, [isMoving, isRunning, currentStance, playerAnimation]);\n\n\n const cooldownsMap = useMemo(() => {\n const map = new Map<string, number>();\n techniqueSelection.activeCooldowns.forEach((cd) => {\n map.set(cd.techniqueId, cd.remaining);\n });\n return map;\n }, [techniqueSelection.activeCooldowns]);\n\n const { currentTechniqueReach, currentAnimationType } = useMemo(() => {\n const techniques = techniqueSelection.availableTechniques;\n const selectedIdx = techniqueSelection.selectedIndex;\n if (techniques.length === 0) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const currentTechnique =\n techniques[Math.min(selectedIdx, techniques.length - 1)];\n if (!currentTechnique) {\n return {\n currentTechniqueReach: 0.7,\n currentAnimationType: AnimationType.JAB,\n };\n }\n const animConfig = getAnimationForTechniqueOrDefault(currentTechnique.id);\n const physicalAttributes =\n getArchetypePhysicalAttributes(selectedArchetype);\n const reach = physicalReachCalculator.calculateMaxReach(\n physicalAttributes,\n animConfig.type,\n currentStance,\n );\n return {\n currentTechniqueReach: reach,\n currentAnimationType: animConfig.type,\n };\n }, [\n techniqueSelection.availableTechniques,\n techniqueSelection.selectedIndex,\n selectedArchetype,\n currentStance,\n ]);\n\n useEffect(() => {\n currentTechniqueAnimationTypeRef.current = currentAnimationType;\n }, [currentAnimationType]);\n\n\n const activeMobileKeyRef = useRef<string | null>(null);\n\n const mobileControlsEnabled = isMobile;\n\n const handleMobileMove = useCallback(\n (direction: Direction | null, eventType: DPadEventType) => {\n const directionMap: Record<Direction, string> = {\n up: \"w\",\n \"up-right\": \"w\",\n right: \"d\",\n \"down-right\": \"s\",\n down: \"s\",\n \"down-left\": \"s\",\n left: \"a\",\n \"up-left\": \"w\",\n };\n\n if (eventType === \"start\" && direction) {\n if (\n activeMobileKeyRef.current &&\n activeMobileKeyRef.current !== directionMap[direction]\n ) {\n const prevKey = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key: prevKey,\n code: `Key${prevKey.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n }\n\n const key = directionMap[direction];\n activeMobileKeyRef.current = key;\n window.dispatchEvent(\n new KeyboardEvent(\"keydown\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n } else if (eventType === \"end\") {\n if (activeMobileKeyRef.current) {\n const key = activeMobileKeyRef.current;\n window.dispatchEvent(\n new KeyboardEvent(\"keyup\", {\n key,\n code: `Key${key.toUpperCase()}`,\n bubbles: true,\n cancelable: true,\n }),\n );\n activeMobileKeyRef.current = null;\n }\n }\n },\n [],\n );\n\n const handleMobileAttack = useCallback(() => {\n handleAttack();\n }, [handleAttack]);\n\n const handleMobileBlock = useCallback(\n (eventType: ButtonEventType) => {\n if (eventType === \"start\") {\n audio.playSFX(\"block\");\n }\n },\n [audio],\n );\n\n const handleMobileGesture = useCallback(\n (gesture: GestureEvent) => {\n switch (gesture.type) {\n case \"swipe-right\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"d\" }));\n break;\n case \"swipe-left\":\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \"a\" }));\n break;\n case \"swipe-up\":\n if (trainingState.isTraining) {\n window.dispatchEvent(new KeyboardEvent(\"keydown\", { key: \" \" }));\n }\n break;\n case \"swipe-down\":\n trainingActions.resetDummy();\n break;\n case \"two-finger-tap\":\n trainingActions.setTrainingMode(\n trainingState.trainingMode === \"vital_point\"\n ? \"basics\"\n : \"vital_point\",\n );\n audio.playSFX(\"menu_select\");\n break;\n }\n },\n [trainingState, trainingActions, audio],\n );\n\n const handleMobileStanceChange = useCallback(\n (stanceIndex: number) => {\n handleStanceChangeWithVisualFeedback(stanceIndex);\n },\n [handleStanceChangeWithVisualFeedback],\n );\n\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n const key = event.key.toLowerCase();\n\n if (key === \"escape\") {\n onReturnToMenu();\n return;\n }\n\n if (key >= \"1\" && key <= \"8\") {\n const stanceIndex = parseInt(key) - 1;\n handleStanceChangeWithVisualFeedback(stanceIndex);\n event.preventDefault();\n return;\n }\n\n if (key === \" \") {\n handleAttack();\n event.preventDefault();\n return;\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onReturnToMenu, handleStanceChangeWithVisualFeedback, handleAttack]);\n\n\n const hasMountedRef = useRef(false);\n\n useEffect(() => {\n let audioStarted = false;\n\n const startMusic = async () => {\n try {\n await audio.fadeIn(\"cyberpunk_fusion\", 2000);\n audioStarted = true;\n } catch (err) {\n console.warn(\"Failed to start training music:\", err);\n trainingActions.setFeedback(\n \"오디오 초기화 실패 | Audio initialization failed\",\n );\n }\n };\n\n void startMusic();\n\n return () => {\n if (audioStarted) {\n void audio\n .fadeOut(2000)\n .then(() => audio.stopMusic())\n .catch((err) => console.warn(\"Failed to stop training music:\", err));\n }\n };\n }, [audio, trainingActions]);\n\n useEffect(() => {\n if (!hasMountedRef.current) {\n hasMountedRef.current = true;\n handleStartTraining();\n }\n }, [handleStartTraining]);\n\n\n useEffect(() => {\n if (trainingState.showFeedback) {\n const timer = setTimeout(() => trainingActions.hideFeedback(), 1500);\n return () => clearTimeout(timer);\n }\n }, [trainingState.showFeedback, trainingState.feedback, trainingActions]);\n\n useEffect(() => {\n if (!trainingState.isTraining || !trainingState.sessionStartTime) return;\n\n const interval = setInterval(() => {\n trainingActions.updateSessionDuration(\n Math.floor((Date.now() - (trainingState.sessionStartTime ?? 0)) / 1000),\n );\n }, 1000);\n\n return () => clearInterval(interval);\n }, [\n trainingState.isTraining,\n trainingState.sessionStartTime,\n trainingActions,\n ]);\n\n const prevTrainingModeRef = useRef<typeof trainingState.trainingMode>(\n trainingState.trainingMode,\n );\n const isFirstModeEffectRef = useRef<boolean>(true);\n const isTrainingRef = useRef<boolean>(trainingState.isTraining);\n const modeChangeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n isTrainingRef.current = trainingState.isTraining;\n }, [trainingState.isTraining]);\n\n const handleStartTrainingRef = useRef(handleStartTraining);\n const handleStopTrainingRef = useRef(handleStopTraining);\n\n useEffect(() => {\n handleStartTrainingRef.current = handleStartTraining;\n handleStopTrainingRef.current = handleStopTraining;\n }, [handleStartTraining, handleStopTraining]);\n\n useEffect(() => {\n if (isFirstModeEffectRef.current) {\n isFirstModeEffectRef.current = false;\n prevTrainingModeRef.current = trainingState.trainingMode;\n return;\n }\n\n const previousMode = prevTrainingModeRef.current;\n const modeChanged = previousMode !== trainingState.trainingMode;\n\n if (!modeChanged) {\n return;\n }\n\n prevTrainingModeRef.current = trainingState.trainingMode;\n\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n\n if (isTrainingRef.current) {\n handleStopTrainingRef.current();\n }\n\n modeChangeTimerRef.current = setTimeout(() => {\n handleStartTrainingRef.current();\n modeChangeTimerRef.current = null;\n }, 100);\n\n return () => {\n if (modeChangeTimerRef.current) {\n clearTimeout(modeChangeTimerRef.current);\n modeChangeTimerRef.current = null;\n }\n };\n }, [trainingState.trainingMode]); // Only depend on training mode to avoid unnecessary re-runs\n\n\n const handleEffectComplete = useCallback(\n (effectId: number) => {\n trainingActions.removeHitEffect(effectId);\n },\n [trainingActions],\n );\n\n\n const handleAnatomyLayerToggle = useCallback(\n (layer: AnatomyLayer) => {\n trainingActions.toggleAnatomyLayer(layer);\n audio.playSFX(\"menu_click\");\n },\n [trainingActions, audio],\n );\n\n const handleVitalPointClick = useCallback(\n (pointId: string) => {\n trainingActions.setSelectedVitalPoint(pointId);\n audio.playSFX(\"menu_select\");\n },\n [trainingActions, audio],\n );\n\n\n const cameraConfig = useMemo(() => {\n const base = createCameraConfig(isMobile);\n if (!isPortrait) return base;\n return {\n ...base,\n fov: Math.min(80, base.fov + 15),\n position: [base.position[0], base.position[1], base.position[2] + 4] as [\n number,\n number,\n number,\n ],\n };\n }, [isMobile, isPortrait]);\n\n\n const performanceSettings = useMemo(() => {\n return getPerformanceSettings(width, isMobile);\n }, [width, isMobile]);\n\n\n return (\n <div\n style={{\n width: `${width}px`,\n height: `${height}px`,\n position: \"relative\",\n overflow: \"hidden\", // Prevent content from extending beyond container\n }}\n data-testid=\"training-screen-3d\"\n >\n <Canvas\n style={{ width: `${width}px`, height: `${height}px` }}\n gl={{\n antialias: performanceSettings.antialias,\n alpha: false,\n powerPreference: \"high-performance\",\n failIfMajorPerformanceCaveat: false, // Don't fail in software renderer\n preserveDrawingBuffer: true, // Help with context stability\n }}\n dpr={performanceSettings.dpr}\n shadows={false} // Temporarily disable shadows\n onCreated={({ gl }) => {\n gl.setClearColor(theme.colors.UI_BACKGROUND_DARK, 1);\n }}\n camera={cameraConfig}\n >\n {/* Lighting - base lighting, arena provides additional */}\n <ambientLight intensity={0.6} />\n <directionalLight position={[10, 10, 5]} intensity={1.2} />\n\n {/* Combat Arena 3D Environment - uses physics-based world dimensions */}\n <CombatArena3D\n lighting=\"cyberpunk\"\n scale={trainingAreaBounds.scale}\n worldWidthMeters={trainingAreaBounds.worldWidthMeters}\n worldDepthMeters={trainingAreaBounds.worldDepthMeters}\n />\n\n {/* Animation updater - 60fps updates */}\n <TrainingAnimationUpdater playerAnimation={playerAnimation} />\n\n {/* Acceleration updater - tracks movement time and updates speed */}\n <AccelerationUpdater\n isMoving={isMoving}\n velocity={velocity}\n movementTimeRef={movementTimeRef}\n lastDirectionRef={lastDirectionRef}\n onSpeedUpdate={setAccelerationBasedSpeed}\n walkSpeed={walkRunSpeeds.walkSpeed}\n runSpeed={walkRunSpeeds.runSpeed}\n />\n\n {/* Training dummy at fixed position */}\n <TrainingDummy3D\n position={dummyPosition}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n isTraining={trainingState.isTraining}\n health={trainingState.dummyHealth}\n onVitalPointHit={handleDummyHit}\n onDefeated={handleDummyDefeated}\n difficulty={difficulty}\n vitalPointCount={vitalPointCount}\n isMobile={isMobile}\n />\n\n {/* Anatomy overlay for educational visualization */}\n {trainingState.visibleAnatomyLayers.length > 0 && (\n <AnatomyOverlay3D\n position={dummyPosition}\n visibleLayers={trainingState.visibleAnatomyLayers}\n opacity={0.6}\n isMobile={isMobile}\n />\n )}\n\n {/* Vital Point Overlay - Show all 70 points on dummy */}\n {overlayVisible && (\n <VitalPointMarkers3D\n position={dummyPosition}\n visible={overlayVisible}\n severityFilter={severityFilters}\n regionFilter={regionFilter}\n searchQuery={searchQuery}\n showLabels={showLabels}\n scale={scale}\n animated={animated}\n selectedPoint={trainingState.selectedVitalPoint}\n onPointClick={handleVitalPointClick}\n />\n )}\n\n {/* Player model */}\n <Player3DWithTransitions\n {...convertPlayerStateToProps(\n trainingPlayerState,\n finalPlayer3DPosition,\n playerRotation,\n {\n isMobile,\n facing: \"right\",\n enableFacialExpressions: true,\n enableEyeTracking: true,\n opponentPosition: dummyPosition,\n },\n )}\n currentAnimation={animationStateToPlayerAnimation(\n playerAnimation.currentState,\n )}\n attackAnimation={attackAnimation}\n laterality={currentLaterality}\n enableTransitionEffects={!isMobile}\n enableStanceSymbol={true}\n enableStanceAudio={true}\n />\n\n {/* Foot Placement Markers for Footwork Drills */}\n {trainingState.trainingMode === \"footwork\" &&\n trainingState.footworkDrillActive && (\n <FootPlacementMarkers3D\n centerPosition={dummyPosition}\n pattern={\n trainingState.footworkDrillType === \"free_practice\"\n ? \"none\"\n : trainingState.footworkDrillType\n }\n currentStep={trainingState.footworkDrillStep}\n visible={true}\n scale={1.0}\n animated={true}\n />\n )}\n\n {/* Hit effects */}\n {trainingState.hitEffects.map((effect) => (\n <HitFeedbackEffect3D\n key={effect.id}\n position={effect.position}\n type={effect.type}\n damage={effect.damage}\n visible={effect.visible}\n onComplete={() => handleEffectComplete(effect.id)}\n isMobile={isMobile}\n />\n ))}\n\n {/* Stance Change Visual Indicator */}\n <StanceChangeIndicator\n currentStance={trainingState.currentStanceIndex}\n previousStance={previousStanceIndex}\n isMobile={isMobile}\n />\n\n {/* NOTE: Mobile controls moved OUTSIDE Canvas for reliable touch events */}\n {/* See MobileControlsPure component rendered after HUDs */}\n\n {/* Post-processing Effects - desktop high tier only for Android WebGL stability */}\n {performanceSettings.postProcessing && (\n <EffectComposer multisampling={4}>\n <Bloom\n luminanceThreshold={0.9}\n luminanceSmoothing={0.9}\n mipmapBlur\n intensity={0.8}\n radius={0.4}\n />\n <Noise opacity={0.03} />\n <Vignette eskil={false} offset={0.1} darkness={0.3} />\n </EffectComposer>\n )}\n </Canvas>\n\n {/* Html UI Overlays (positioned absolutely over Canvas) - matches CombatScreen pattern */}\n <div\n style={{\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n zIndex: Z_INDEX.HUD,\n overflow: \"clip\",\n }}\n data-testid=\"training-hud-overlay\"\n >\n {/* Left HUD - Anatomy Controls, Guard Indicator.\n Hidden on mobile because the side HUD occludes the compressed\n arena in both portrait and landscape. Anatomy layer toggles remain\n available on larger viewports where there is room for side panels. */}\n {!isMobile && (\n <TrainingLeftHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n visibleAnatomyLayers={trainingState.visibleAnatomyLayers}\n onAnatomyLayerToggle={handleAnatomyLayerToggle}\n currentStanceIndex={trainingState.currentStanceIndex}\n isInGuard={playerAnimation.isInStanceGuard()}\n />\n )}\n\n {/* Top HUD - Training Controls, Archetype Selector, Return Button. */}\n <TrainingTopHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n isTraining={trainingState.isTraining}\n onStartTraining={handleStartTraining}\n onStopTraining={handleStopTraining}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n overlayVisible={overlayVisible}\n onReturnToMenu={onReturnToMenu}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Right HUD - Mode Selector, Stats, Vital Point Selection.\n Hidden on mobile to keep the training dojang visible and usable.\n The core start/stop, archetype, vital-point toggle, technique bar,\n stance wheel, gestures, and touch controls remain available. */}\n {!isMobile && (\n <TrainingRightHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n trainingMode={trainingState.trainingMode}\n onModeChange={trainingActions.setTrainingMode}\n stats={{\n ...trainingState.stats,\n sessionDuration: trainingState.sessionDuration,\n bestCombo: trainingState.bestCombo,\n perfectStrikes: trainingState.perfectStrikes,\n }}\n distanceToDummy={distanceToDummy}\n effectiveReach={currentTechniqueReach}\n selectedVitalPoint={trainingState.selectedVitalPoint}\n onVitalPointSelect={trainingActions.setSelectedVitalPoint}\n footworkDrillType={trainingState.footworkDrillType}\n footworkDrillStep={trainingState.footworkDrillStep}\n footworkDrillActive={trainingState.footworkDrillActive}\n onStartFootworkDrill={trainingActions.startFootworkDrill}\n onStopFootworkDrill={trainingActions.stopFootworkDrill}\n onAdvanceFootworkStep={trainingActions.advanceFootworkStep}\n />\n )}\n {/* Bottom HUD - Technique Bar, Feedback Messages, Mobile Controls */}\n <TrainingBottomHUD\n width={width}\n height={height}\n isMobile={isMobile}\n positionScale={positionScale}\n techniques={techniqueSelection.availableTechniques}\n player={trainingPlayerState}\n selectedIndex={techniqueSelection.selectedIndex}\n cooldowns={cooldownsMap}\n onTechniqueSelect={techniqueSelection.selectTechnique}\n showFeedback={trainingState.showFeedback}\n feedbackMessage={trainingState.feedback}\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={setSelectedArchetype}\n onPlaySFX={(sound) => audio.playSFX(sound)}\n />\n\n {/* Vital Point Overlay Controls - Pure DOM overlay (outside Canvas) */}\n {overlayVisible && (\n <VitalPointOverlayControlsPure\n visible={overlayVisible}\n onVisibleChange={setOverlayVisible}\n severityFilters={severityFilters}\n onSeverityFiltersChange={setSeverityFilters}\n regionFilter={regionFilter}\n onRegionFilterChange={setRegionFilter}\n searchQuery={searchQuery}\n onSearchQueryChange={setSearchQuery}\n showLabels={showLabels}\n onShowLabelsChange={setShowLabels}\n animated={animated}\n onAnimatedChange={setAnimated}\n scale={scale}\n onScaleChange={setScale}\n screenPosition={{ top: \"180px\", left: \"20px\" }}\n isMobile={isMobile}\n />\n )}\n\n {/* Mobile Controls - Pure DOM overlay (outside Canvas for reliable touch) */}\n {isMobile && (\n <>\n <MobileControlsOverlay\n onMove={handleMobileMove}\n onAttack={handleMobileAttack}\n onBlock={handleMobileBlock}\n disabled={!mobileControlsEnabled}\n bottom={getMobileControlsBottom(height)}\n opacity={0.85}\n viewportWidth={width}\n viewportHeight={height}\n />\n\n <StanceWheelPure\n currentStance={trainingState.currentStanceIndex}\n onStanceChange={handleMobileStanceChange}\n expanded={trainingState.stanceWheelExpanded}\n onToggle={trainingActions.toggleStanceWheel}\n disabled={!mobileControlsEnabled}\n opacity={0.8}\n />\n\n <GestureRecognizerPure\n onGesture={handleMobileGesture}\n enabled={mobileControlsEnabled}\n showFeedback={true}\n minSwipeDistance={50}\n />\n </>\n )}\n </div>\n </div>\n );\n};\n\nexport default TrainingScreen3D;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TrainingBottomHUD.d.ts","sourceRoot":"","sources":["../../../../../../src/components/screens/training/components/hud/TrainingBottomHUD.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAgB9D,MAAM,WAAW,sBAAsB;IACrC,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1C,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,iCAAiC;IACjC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,sCAAsC;IACtC,QAAQ,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,uCAAuC;IACvC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,kCAAkC;IAClC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,gDAAgD;IAChD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,eAAe,CAAC;IAC7C,mDAAmD;IACnD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,KAAK,IAAI,CAAC;IAClE,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAsK9D,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"TrainingBottomHUD.d.ts","sourceRoot":"","sources":["../../../../../../src/components/screens/training/components/hud/TrainingBottomHUD.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAmB9D,MAAM,WAAW,sBAAsB;IACrC,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1C,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,yCAAyC;IACzC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,iCAAiC;IACjC,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,sCAAsC;IACtC,QAAQ,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,uCAAuC;IACvC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,kCAAkC;IAClC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,gDAAgD;IAChD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,eAAe,CAAC;IAC7C,mDAAmD;IACnD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,KAAK,IAAI,CAAC;IAClE,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAsK9D,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { BORDERS, GRADIENTS, HUD_STYLE, SPACING } from "../../../../../types/constants/designSystem.js";
2
2
  import { Z_INDEX } from "../../../../../types/LayoutTypes.js";
3
- import { HUD_SIDE_CONTROL_RESERVES } from "../../../../../types/constants/layout.js";
3
+ import { HUD_SIDE_CONTROL_RESERVES, TRAINING_BOTTOM_HUD_HEIGHT_PERCENT } from "../../../../../types/constants/layout.js";
4
4
  import { getHUDHeight, getResponsivePadding, shouldShowMobileControls } from "../../../../../utils/responsiveLayout.js";
5
5
  import TechniqueBar from "../../../../shared/three/ui/TechniqueBar.js";
6
6
  import { VolumeControl } from "../../../../shared/ui/VolumeControl.js";
@@ -35,7 +35,7 @@ var TrainingBottomHUD = ({ width, height, isMobile = false, positionScale, techn
35
35
  const showMobileControls = shouldShowMobileControls(width, isMobile);
36
36
  const showArchetypeSelector = showMobileControls && onArchetypeSelect !== void 0 && selectedArchetype !== void 0;
37
37
  const layout = React.useMemo(() => {
38
- const hudHeight = getHUDHeight(height, .11) * positionScale;
38
+ const hudHeight = getHUDHeight(height, TRAINING_BOTTOM_HUD_HEIGHT_PERCENT) * positionScale;
39
39
  const padding = getResponsivePadding(width) * positionScale;
40
40
  const volumeReserve = showMobileControls ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE : HUD_SIDE_CONTROL_RESERVES.VOLUME_CONTROL;
41
41
  const archetypeReserve = showArchetypeSelector ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE : 0;
@@ -1 +1 @@
1
- {"version":3,"file":"TrainingBottomHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingBottomHUD.tsx"],"sourcesContent":["/**\n * TrainingBottomHUD - Bottom bar for training screen\n *\n * Contains:\n * - Technique Bar (centered)\n * - Volume Control (bottom-right, compact)\n * - Feedback Message (centered overlay)\n * - Archetype Selector (mobile only - bottom-left)\n *\n * Gaming Layout Best Practice:\n * - Width: 100% of screen\n * - Height: Resolution-based ~11% of screen height (40-120px range)\n * - On mobile, consolidates controls from TopHUD\n *\n * @korean 훈련화면 하단 바 - 기술 바, 음량, 피드백, 모바일 원형선택\n */\n\nimport React from \"react\";\nimport { PlayerState } from \"../../../../../systems\";\nimport { Technique } from \"../../../../../types\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport { HUD_SIDE_CONTROL_RESERVES } from \"../../../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../../../types/LayoutTypes\";\nimport { SPACING, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport {\n getHUDHeight,\n getResponsivePadding,\n shouldShowMobileControls,\n} from \"../../../../../utils/responsiveLayout\";\nimport { TechniqueBar } from \"../../../../shared/three/ui/TechniqueBar\";\nimport { VolumeControl } from \"../../../../shared/ui/VolumeControl\";\nimport { ArchetypeSelectionButtons } from \"../TrainingButtonsOverlayHtml\";\nimport TrainingFeedbackOverlayHtml from \"../TrainingFeedbackOverlayHtml\";\n\n\n\nexport interface TrainingBottomHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Available techniques for the technique bar */\n readonly techniques: readonly Technique[];\n /** Player state for technique availability checks */\n readonly player: PlayerState;\n /** Currently selected technique index */\n readonly selectedIndex: number;\n /** Active technique cooldowns */\n readonly cooldowns: Map<string, number>;\n /** Handler for technique selection */\n readonly onTechniqueSelect: (index: number) => void;\n /** Whether to show feedback message */\n readonly showFeedback: boolean;\n /** Feedback message to display */\n readonly feedbackMessage: string;\n /** Currently selected archetype (for mobile) */\n readonly selectedArchetype?: PlayerArchetype;\n /** Handler for archetype selection (for mobile) */\n readonly onArchetypeSelect?: (archetype: PlayerArchetype) => void;\n /** Handler for playing sound effects (for mobile) */\n readonly onPlaySFX?: (sound: string) => void;\n}\n\n/**\n * TrainingBottomHUD Component\n *\n * Compact bottom bar with centered technique bar, volume control,\n * and archetype selector on mobile. Uses resolution-based sizing.\n */\nexport const TrainingBottomHUD: React.FC<TrainingBottomHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n showFeedback,\n feedbackMessage,\n selectedArchetype,\n onArchetypeSelect,\n onPlaySFX,\n}) => {\n const showMobileControls = shouldShowMobileControls(width, isMobile);\n const showArchetypeSelector =\n showMobileControls &&\n onArchetypeSelect !== undefined &&\n selectedArchetype !== undefined;\n\n const layout = React.useMemo(() => {\n const hudHeight = getHUDHeight(height, 0.11) * positionScale;\n \n const padding = getResponsivePadding(width) * positionScale;\n\n const volumeReserve = showMobileControls\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.VOLUME_CONTROL;\n const archetypeReserve = showArchetypeSelector\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : 0;\n\n const techniqueBarContainerWidth = Math.max(\n 0,\n width -\n padding * 2 -\n volumeReserve -\n archetypeReserve,\n );\n\n return {\n hudHeight,\n padding,\n volumeReserve,\n archetypeReserve,\n techniqueBarContainerWidth,\n };\n }, [width, height, positionScale, showArchetypeSelector, showMobileControls]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n bottom: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"center\",\n alignItems: \"center\",\n pointerEvents: \"none\",\n padding: `${layout.padding}px`,\n boxSizing: \"border-box\",\n borderTop: BORDERS.default,\n background: GRADIENTS.verticalReverse(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-bottom-hud\"\n >\n {/* Feedback Message (centered in screen, above technique bar) */}\n {showFeedback && (\n <div\n style={{\n position: \"fixed\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n zIndex: Z_INDEX.MODAL,\n pointerEvents: \"none\",\n }}\n >\n <TrainingFeedbackOverlayHtml\n message={feedbackMessage}\n isMobile={showMobileControls}\n />\n </div>\n )}\n\n {/* Technique Bar - centered, embedded mode for proper containment.\n Reserve right space for the absolute Volume Control and (on mobile)\n left space for the Archetype Selector so technique cards never sit\n under the side controls. */}\n <div\n style={{\n pointerEvents: \"all\",\n flex: 1,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n overflow: \"visible\",\n height: \"100%\",\n marginRight: layout.volumeReserve,\n marginLeft: showArchetypeSelector ? layout.archetypeReserve : 0,\n }}\n data-testid=\"training-bottom-hud-technique-section\"\n >\n <TechniqueBar\n techniques={techniques as Technique[]}\n player={player}\n selectedIndex={selectedIndex}\n cooldowns={cooldowns}\n onTechniqueSelect={onTechniqueSelect}\n onTechniqueHover={(_tech) => {}}\n isMobile={showMobileControls}\n screenWidth={width}\n screenHeight={height}\n embedded={true}\n containerWidth={layout.techniqueBarContainerWidth}\n />\n </div>\n\n {/* Volume Control - bottom right corner */}\n <div\n style={{\n position: \"absolute\",\n right: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n }}\n data-testid=\"training-bottom-hud-volume-section\"\n >\n <VolumeControl position=\"custom\" compact={true} />\n </div>\n\n {/* Mobile Archetype Selector - bottom left corner */}\n {showArchetypeSelector && (\n <div\n style={{\n position: \"absolute\",\n left: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n display: \"flex\",\n alignItems: \"center\",\n gap: SPACING.xxs,\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: GRADIENTS.vertical(0.9),\n border: BORDERS.accent,\n borderRadius: SPACING.xxs,\n }}\n data-testid=\"training-bottom-hud-archetype-section\"\n >\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX ?? (() => {})}\n isMobile={showMobileControls}\n />\n </div>\n )}\n </div>\n );\n};\n\nexport default TrainingBottomHUD;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA,IAAa,qBAAuD,EAClE,OACA,QACA,WAAW,OACX,eACA,YACA,QACA,eACA,WACA,mBACA,cACA,iBACA,mBACA,mBACA,gBACI;CACJ,MAAM,qBAAqB,yBAAyB,OAAO,QAAQ;CACnE,MAAM,wBACJ,sBACA,sBAAsB,KAAA,KACtB,sBAAsB,KAAA;CAExB,MAAM,SAAS,MAAM,cAAc;EACjC,MAAM,YAAY,aAAa,QAAQ,GAAI,IAAI;EAE/C,MAAM,UAAU,qBAAqB,KAAK,IAAI;EAE9C,MAAM,gBAAgB,qBAClB,0BAA0B,uBAC1B,0BAA0B;EAC9B,MAAM,mBAAmB,wBACrB,0BAA0B,uBAC1B;EAUJ,OAAO;GACL;GACA;GACA;GACA;GACA,4BAbiC,KAAK,IACtC,GACA,QACE,UAAU,IACV,gBACA,gBAQF;EACF;CACF,GAAG;EAAC;EAAO;EAAQ;EAAe;EAAuB;CAAkB,CAAC;CAE5E,OACE,qBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,QAAQ;GACR,MAAM;GACN,OAAO;GACP,QAAQ,GAAG,OAAO,UAAU;GAC5B,SAAS;GACT,eAAe;GACf,gBAAgB;GAChB,YAAY;GACZ,eAAe;GACf,SAAS,GAAG,OAAO,QAAQ;GAC3B,WAAW;GACX,WAAW,QAAQ;GACnB,YAAY,UAAU,gBAAgB,EAAG;GACzC,gBAAgB,UAAU;EAC5B;EACA,eAAY;YAlBd;GAqBG,gBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,KAAK;KACL,MAAM;KACN,WAAW;KACX,QAAQ,QAAQ;KAChB,eAAe;IACjB;cAEA,oBAAC,6BAAD;KACE,SAAS;KACT,UAAU;IACX,CAAA;GACE,CAAA;GAOP,oBAAC,OAAD;IACE,OAAO;KACL,eAAe;KACf,MAAM;KACN,SAAS;KACT,eAAe;KACf,gBAAgB;KAChB,YAAY;KACZ,UAAU;KACV,QAAQ;KACR,aAAa,OAAO;KACpB,YAAY,wBAAwB,OAAO,mBAAmB;IAChE;IACA,eAAY;cAEZ,oBAAC,cAAD;KACc;KACJ;KACO;KACJ;KACQ;KACnB,mBAAmB,UAAU,CAAC;KAC9B,UAAU;KACV,aAAa;KACb,cAAc;KACd,UAAU;KACV,gBAAgB,OAAO;IACxB,CAAA;GACE,CAAA;GAGL,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,OAAO,GAAG,OAAO,UAAU,IAAI;KAC/B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;IACjB;IACA,eAAY;cAEZ,oBAAC,eAAD;KAAe,UAAS;KAAS,SAAS;IAAO,CAAA;GAC9C,CAAA;GAGJ,yBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,MAAM,GAAG,OAAO,UAAU,IAAI;KAC9B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;KACf,SAAS;KACT,YAAY;KACZ,KAAK,QAAQ;KACb,SAAS,GAAG,QAAQ,IAAI,GAAG,QAAQ;KACnC,YAAY,UAAU,SAAS,EAAG;KAClC,QAAQ,QAAQ;KAChB,cAAc,QAAQ;IACxB;IACA,eAAY;cAEZ,oBAAC,2BAAD;KACqB;KACA;KACnB,WAAW,oBAAoB,CAAC;KAChC,UAAU;IACX,CAAA;GACE,CAAA;EAEJ;;AAET"}
1
+ {"version":3,"file":"TrainingBottomHUD.js","names":[],"sources":["../../../../../../src/components/screens/training/components/hud/TrainingBottomHUD.tsx"],"sourcesContent":["/**\n * TrainingBottomHUD - Bottom bar for training screen\n *\n * Contains:\n * - Technique Bar (centered)\n * - Volume Control (bottom-right, compact)\n * - Feedback Message (centered overlay)\n * - Archetype Selector (mobile only - bottom-left)\n *\n * Gaming Layout Best Practice:\n * - Width: 100% of screen\n * - Height: Resolution-based ~11% of screen height (40-120px range)\n * - On mobile, consolidates controls from TopHUD\n *\n * @korean 훈련화면 하단 바 - 기술 바, 음량, 피드백, 모바일 원형선택\n */\n\nimport React from \"react\";\nimport { PlayerState } from \"../../../../../systems\";\nimport { Technique } from \"../../../../../types\";\nimport { PlayerArchetype } from \"../../../../../types/common\";\nimport {\n HUD_SIDE_CONTROL_RESERVES,\n TRAINING_BOTTOM_HUD_HEIGHT_PERCENT,\n} from \"../../../../../types/constants/layout\";\nimport { Z_INDEX } from \"../../../../../types/LayoutTypes\";\nimport { SPACING, BORDERS, GRADIENTS, HUD_STYLE } from \"../../../../../types/constants/designSystem\";\nimport {\n getHUDHeight,\n getResponsivePadding,\n shouldShowMobileControls,\n} from \"../../../../../utils/responsiveLayout\";\nimport { TechniqueBar } from \"../../../../shared/three/ui/TechniqueBar\";\nimport { VolumeControl } from \"../../../../shared/ui/VolumeControl\";\nimport { ArchetypeSelectionButtons } from \"../TrainingButtonsOverlayHtml\";\nimport TrainingFeedbackOverlayHtml from \"../TrainingFeedbackOverlayHtml\";\n\n\n\nexport interface TrainingBottomHUDProps {\n /** Screen width for layout calculations */\n readonly width: number;\n /** Screen height for layout calculations */\n readonly height: number;\n /** Whether mobile controls should be shown (NOT for sizing) */\n readonly isMobile?: boolean;\n /** Position scale multiplier for large displays */\n readonly positionScale: number;\n /** Available techniques for the technique bar */\n readonly techniques: readonly Technique[];\n /** Player state for technique availability checks */\n readonly player: PlayerState;\n /** Currently selected technique index */\n readonly selectedIndex: number;\n /** Active technique cooldowns */\n readonly cooldowns: Map<string, number>;\n /** Handler for technique selection */\n readonly onTechniqueSelect: (index: number) => void;\n /** Whether to show feedback message */\n readonly showFeedback: boolean;\n /** Feedback message to display */\n readonly feedbackMessage: string;\n /** Currently selected archetype (for mobile) */\n readonly selectedArchetype?: PlayerArchetype;\n /** Handler for archetype selection (for mobile) */\n readonly onArchetypeSelect?: (archetype: PlayerArchetype) => void;\n /** Handler for playing sound effects (for mobile) */\n readonly onPlaySFX?: (sound: string) => void;\n}\n\n/**\n * TrainingBottomHUD Component\n *\n * Compact bottom bar with centered technique bar, volume control,\n * and archetype selector on mobile. Uses resolution-based sizing.\n */\nexport const TrainingBottomHUD: React.FC<TrainingBottomHUDProps> = ({\n width,\n height,\n isMobile = false,\n positionScale,\n techniques,\n player,\n selectedIndex,\n cooldowns,\n onTechniqueSelect,\n showFeedback,\n feedbackMessage,\n selectedArchetype,\n onArchetypeSelect,\n onPlaySFX,\n}) => {\n const showMobileControls = shouldShowMobileControls(width, isMobile);\n const showArchetypeSelector =\n showMobileControls &&\n onArchetypeSelect !== undefined &&\n selectedArchetype !== undefined;\n\n const layout = React.useMemo(() => {\n const hudHeight = getHUDHeight(height, TRAINING_BOTTOM_HUD_HEIGHT_PERCENT) * positionScale;\n \n const padding = getResponsivePadding(width) * positionScale;\n\n const volumeReserve = showMobileControls\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : HUD_SIDE_CONTROL_RESERVES.VOLUME_CONTROL;\n const archetypeReserve = showArchetypeSelector\n ? HUD_SIDE_CONTROL_RESERVES.TECHNIQUE_BAR_MOBILE\n : 0;\n\n const techniqueBarContainerWidth = Math.max(\n 0,\n width -\n padding * 2 -\n volumeReserve -\n archetypeReserve,\n );\n\n return {\n hudHeight,\n padding,\n volumeReserve,\n archetypeReserve,\n techniqueBarContainerWidth,\n };\n }, [width, height, positionScale, showArchetypeSelector, showMobileControls]);\n\n return (\n <div\n style={{\n position: \"absolute\",\n bottom: 0,\n left: 0,\n width: \"100%\",\n height: `${layout.hudHeight}px`,\n display: \"flex\",\n flexDirection: \"row\",\n justifyContent: \"center\",\n alignItems: \"center\",\n pointerEvents: \"none\",\n padding: `${layout.padding}px`,\n boxSizing: \"border-box\",\n borderTop: BORDERS.default,\n background: GRADIENTS.verticalReverse(0.9),\n backdropFilter: HUD_STYLE.backdropFilter,\n }}\n data-testid=\"training-bottom-hud\"\n >\n {/* Feedback Message (centered in screen, above technique bar) */}\n {showFeedback && (\n <div\n style={{\n position: \"fixed\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n zIndex: Z_INDEX.MODAL,\n pointerEvents: \"none\",\n }}\n >\n <TrainingFeedbackOverlayHtml\n message={feedbackMessage}\n isMobile={showMobileControls}\n />\n </div>\n )}\n\n {/* Technique Bar - centered, embedded mode for proper containment.\n Reserve right space for the absolute Volume Control and (on mobile)\n left space for the Archetype Selector so technique cards never sit\n under the side controls. */}\n <div\n style={{\n pointerEvents: \"all\",\n flex: 1,\n display: \"flex\",\n flexDirection: \"column\",\n justifyContent: \"center\",\n alignItems: \"center\",\n overflow: \"visible\",\n height: \"100%\",\n marginRight: layout.volumeReserve,\n marginLeft: showArchetypeSelector ? layout.archetypeReserve : 0,\n }}\n data-testid=\"training-bottom-hud-technique-section\"\n >\n <TechniqueBar\n techniques={techniques as Technique[]}\n player={player}\n selectedIndex={selectedIndex}\n cooldowns={cooldowns}\n onTechniqueSelect={onTechniqueSelect}\n onTechniqueHover={(_tech) => {}}\n isMobile={showMobileControls}\n screenWidth={width}\n screenHeight={height}\n embedded={true}\n containerWidth={layout.techniqueBarContainerWidth}\n />\n </div>\n\n {/* Volume Control - bottom right corner */}\n <div\n style={{\n position: \"absolute\",\n right: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n }}\n data-testid=\"training-bottom-hud-volume-section\"\n >\n <VolumeControl position=\"custom\" compact={true} />\n </div>\n\n {/* Mobile Archetype Selector - bottom left corner */}\n {showArchetypeSelector && (\n <div\n style={{\n position: \"absolute\",\n left: `${layout.padding * 1.5}px`,\n bottom: `${layout.padding}px`,\n pointerEvents: \"all\",\n display: \"flex\",\n alignItems: \"center\",\n gap: SPACING.xxs,\n padding: `${SPACING.xxs} ${SPACING.xs}`,\n background: GRADIENTS.vertical(0.9),\n border: BORDERS.accent,\n borderRadius: SPACING.xxs,\n }}\n data-testid=\"training-bottom-hud-archetype-section\"\n >\n <ArchetypeSelectionButtons\n selectedArchetype={selectedArchetype}\n onArchetypeSelect={onArchetypeSelect}\n onPlaySFX={onPlaySFX ?? (() => {})}\n isMobile={showMobileControls}\n />\n </div>\n )}\n </div>\n );\n};\n\nexport default TrainingBottomHUD;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,IAAa,qBAAuD,EAClE,OACA,QACA,WAAW,OACX,eACA,YACA,QACA,eACA,WACA,mBACA,cACA,iBACA,mBACA,mBACA,gBACI;CACJ,MAAM,qBAAqB,yBAAyB,OAAO,QAAQ;CACnE,MAAM,wBACJ,sBACA,sBAAsB,KAAA,KACtB,sBAAsB,KAAA;CAExB,MAAM,SAAS,MAAM,cAAc;EACjC,MAAM,YAAY,aAAa,QAAQ,kCAAkC,IAAI;EAE7E,MAAM,UAAU,qBAAqB,KAAK,IAAI;EAE9C,MAAM,gBAAgB,qBAClB,0BAA0B,uBAC1B,0BAA0B;EAC9B,MAAM,mBAAmB,wBACrB,0BAA0B,uBAC1B;EAUJ,OAAO;GACL;GACA;GACA;GACA;GACA,4BAbiC,KAAK,IACtC,GACA,QACE,UAAU,IACV,gBACA,gBAQF;EACF;CACF,GAAG;EAAC;EAAO;EAAQ;EAAe;EAAuB;CAAkB,CAAC;CAE5E,OACE,qBAAC,OAAD;EACE,OAAO;GACL,UAAU;GACV,QAAQ;GACR,MAAM;GACN,OAAO;GACP,QAAQ,GAAG,OAAO,UAAU;GAC5B,SAAS;GACT,eAAe;GACf,gBAAgB;GAChB,YAAY;GACZ,eAAe;GACf,SAAS,GAAG,OAAO,QAAQ;GAC3B,WAAW;GACX,WAAW,QAAQ;GACnB,YAAY,UAAU,gBAAgB,EAAG;GACzC,gBAAgB,UAAU;EAC5B;EACA,eAAY;YAlBd;GAqBG,gBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,KAAK;KACL,MAAM;KACN,WAAW;KACX,QAAQ,QAAQ;KAChB,eAAe;IACjB;cAEA,oBAAC,6BAAD;KACE,SAAS;KACT,UAAU;IACX,CAAA;GACE,CAAA;GAOP,oBAAC,OAAD;IACE,OAAO;KACL,eAAe;KACf,MAAM;KACN,SAAS;KACT,eAAe;KACf,gBAAgB;KAChB,YAAY;KACZ,UAAU;KACV,QAAQ;KACR,aAAa,OAAO;KACpB,YAAY,wBAAwB,OAAO,mBAAmB;IAChE;IACA,eAAY;cAEZ,oBAAC,cAAD;KACc;KACJ;KACO;KACJ;KACQ;KACnB,mBAAmB,UAAU,CAAC;KAC9B,UAAU;KACV,aAAa;KACb,cAAc;KACd,UAAU;KACV,gBAAgB,OAAO;IACxB,CAAA;GACE,CAAA;GAGL,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,OAAO,GAAG,OAAO,UAAU,IAAI;KAC/B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;IACjB;IACA,eAAY;cAEZ,oBAAC,eAAD;KAAe,UAAS;KAAS,SAAS;IAAO,CAAA;GAC9C,CAAA;GAGJ,yBACC,oBAAC,OAAD;IACE,OAAO;KACL,UAAU;KACV,MAAM,GAAG,OAAO,UAAU,IAAI;KAC9B,QAAQ,GAAG,OAAO,QAAQ;KAC1B,eAAe;KACf,SAAS;KACT,YAAY;KACZ,KAAK,QAAQ;KACb,SAAS,GAAG,QAAQ,IAAI,GAAG,QAAQ;KACnC,YAAY,UAAU,SAAS,EAAG;KAClC,QAAQ,QAAQ;KAChB,cAAc,QAAQ;IACxB;IACA,eAAY;cAEZ,oBAAC,2BAAD;KACqB;KACA;KACnB,WAAW,oBAAoB,CAAC;KAChC,UAAU;IACX,CAAA;GACE,CAAA;EAEJ;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTrainingLayout.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/training/hooks/useTrainingLayout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAcH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,eAAe,EAAE,uBAAuB,CAAC;IAClD,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,cAAc,CA6FhB"}
1
+ {"version":3,"file":"useTrainingLayout.d.ts","sourceRoot":"","sources":["../../../../../src/components/screens/training/hooks/useTrainingLayout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAsBH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,eAAe,EAAE,uBAAuB,CAAC;IAClD,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAChD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,cAAc,CAmGhB"}
@@ -1,9 +1,11 @@
1
1
  import { getScreenSize } from "../../../../systems/ResponsiveScaling.js";
2
+ import { TRAINING_BOTTOM_HUD_HEIGHT_PERCENT, TRAINING_TOP_HUD_HEIGHT_PERCENT } from "../../../../types/constants/layout.js";
3
+ import { getHUDHeight } from "../../../../utils/responsiveLayout.js";
4
+ import { getDesktopArenaWidthBudget, getHUDPositionScale } from "../../../../utils/responsiveLayoutHelpers.js";
2
5
  import { calculateArenaWorldDimensions } from "../../../../utils/arenaWorldDimensions.js";
3
6
  import { shouldUseMobileControls } from "../../../../utils/deviceDetection.js";
4
7
  import { calculateMobileAreaBounds } from "../../../../utils/mobileLayoutHelpers.js";
5
8
  import { PORTRAIT_HYSTERESIS_FACTOR, mobileControlsBottomClearance } from "../../../../utils/responsiveOrientationConstants.js";
6
- import { getDesktopArenaWidthBudget } from "../../../../utils/responsiveLayoutHelpers.js";
7
9
  import { useMemo } from "react";
8
10
  //#region src/components/screens/training/hooks/useTrainingLayout.ts
9
11
  /**
@@ -58,16 +60,19 @@ function useTrainingLayout(width, height) {
58
60
  footerHeight: isMobile ? 60 : isTablet ? 70 : isLargeDesktop ? 90 : 80
59
61
  };
60
62
  }, [isMobile, screenSize]);
63
+ const positionScale = useMemo(() => getHUDPositionScale(screenSize, isMobile), [screenSize, isMobile]);
61
64
  return {
62
65
  layoutConstants,
63
66
  trainingAreaBounds: useMemo(() => {
64
- const areaY = layoutConstants.headerHeight + layoutConstants.padding;
67
+ const topHudHeight = getHUDHeight(height, TRAINING_TOP_HUD_HEIGHT_PERCENT) * positionScale;
68
+ const bottomHudHeight = getHUDHeight(height, TRAINING_BOTTOM_HUD_HEIGHT_PERCENT) * positionScale;
69
+ const areaY = topHudHeight + layoutConstants.padding;
65
70
  const worldDimensions = calculateArenaWorldDimensions(width);
66
71
  if (isMobile) {
67
72
  const isExtraSmall = width < 380;
68
- return calculateMobileAreaBounds(width, height, isExtraSmall ? 75 : 80, mobileControlsBottomClearance(layoutConstants.controlsHeight, layoutConstants.footerHeight, isExtraSmall, isPortrait, "training"), areaY, isPortrait ? "portrait" : "landscape");
73
+ return calculateMobileAreaBounds(width, height, isExtraSmall ? 75 : 80, mobileControlsBottomClearance(layoutConstants.controlsHeight, Math.max(layoutConstants.footerHeight, bottomHudHeight), isExtraSmall, isPortrait, "training"), areaY, isPortrait ? "portrait" : "landscape");
69
74
  }
70
- const totalReservedHeight = layoutConstants.headerHeight + layoutConstants.controlsHeight + layoutConstants.footerHeight;
75
+ const totalReservedHeight = topHudHeight + bottomHudHeight;
71
76
  const totalPadding = layoutConstants.padding * 3;
72
77
  const availableHeight = height - totalReservedHeight - totalPadding;
73
78
  let arenaWidth = getDesktopArenaWidthBudget(width);
@@ -91,7 +96,8 @@ function useTrainingLayout(width, height) {
91
96
  height,
92
97
  layoutConstants,
93
98
  isMobile,
94
- isPortrait
99
+ isPortrait,
100
+ positionScale
95
101
  ]),
96
102
  isMobile,
97
103
  isPortrait,
@@ -1 +1 @@
1
- {"version":3,"file":"useTrainingLayout.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingLayout.ts"],"sourcesContent":["/**\n * useTrainingLayout Hook - Enhanced Responsive Training Layout\n *\n * Custom hook for managing responsive training screen layout calculations with\n * comprehensive support for all screen sizes from mobile to ultra-wide displays.\n *\n * Enhanced Features:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional scaling for consistent sizing across devices\n * - Optimized layout sizing for each device category\n * - Smooth transitions for resize operations\n * - 60fps performance maintained\n *\n * Uses robust device detection combining user-agent and screen size to ensure\n * mobile controls are shown on all mobile devices, including high-resolution phones.\n *\n * Performance:\n * - Reduces recalculations by checking only breakpoint changes, not exact dimensions\n * - Memoizes layout constants to prevent cascading re-renders\n * - Targets <1ms execution time for layout calculations\n *\n * @param width - Screen width\n * @param height - Screen height\n *\n * @returns Layout constants and training area bounds\n *\n * @example\n * ```typescript\n * const { layoutConstants, trainingAreaBounds, isMobile, screenSize } = useTrainingLayout(1200, 800);\n * ```\n */\n\nimport { useMemo } from \"react\";\nimport { getScreenSize } from \"../../../../systems/ResponsiveScaling\";\nimport { calculateArenaWorldDimensions } from \"../../../../utils/arenaWorldDimensions\";\nimport { shouldUseMobileControls } from \"../../../../utils/deviceDetection\";\nimport { calculateMobileAreaBounds } from \"../../../../utils/mobileLayoutHelpers\";\nimport {\n mobileControlsBottomClearance,\n PORTRAIT_FORCE_MAX_WIDTH_PX,\n PORTRAIT_HYSTERESIS_FACTOR,\n} from \"../../../../utils/responsiveOrientationConstants\";\nimport { getDesktopArenaWidthBudget } from \"../../../../utils/responsiveLayoutHelpers\";\n\nimport type { ScreenSize } from \"../../../../systems/ResponsiveScaling\";\n\nexport interface TrainingLayoutConstants {\n readonly padding: number;\n readonly headerHeight: number;\n readonly buttonHeight: number;\n readonly sectionSpacing: number;\n readonly controlsHeight: number;\n readonly footerHeight: number;\n}\n\nexport interface TrainingAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number; // 3D scale factor for training area (1.0 = desktop, <1.0 = mobile)\n readonly worldWidthMeters: number; // Physical training area width in meters\n readonly worldDepthMeters: number; // Physical training area depth in meters\n}\n\nexport interface TrainingLayout {\n readonly layoutConstants: TrainingLayoutConstants;\n readonly trainingAreaBounds: TrainingAreaBounds;\n readonly isMobile: boolean;\n readonly isPortrait: boolean;\n readonly screenSize: ScreenSize;\n}\n\n/**\n * Custom hook for training screen layout calculations\n * Enhanced with centralized responsive scaling system\n * Optimized to reduce recalculations and improve 60fps performance\n */\nexport function useTrainingLayout(\n width: number,\n height: number,\n): TrainingLayout {\n const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n const layoutConstants = useMemo<TrainingLayoutConstants>(() => {\n const isLargeDesktop = screenSize === \"xlarge\";\n const isTablet = screenSize === \"tablet\";\n\n return {\n padding: isMobile ? 20 : isTablet ? 25 : isLargeDesktop ? 35 : 30,\n headerHeight: isMobile ? 80 : isTablet ? 90 : isLargeDesktop ? 110 : 100,\n buttonHeight: isMobile ? 45 : isTablet ? 50 : isLargeDesktop ? 60 : 55,\n sectionSpacing: isMobile ? 15 : isTablet ? 18 : isLargeDesktop ? 25 : 20,\n controlsHeight: isMobile\n ? 120\n : isTablet\n ? 110\n : isLargeDesktop\n ? 150\n : 130,\n footerHeight: isMobile ? 60 : isTablet ? 70 : isLargeDesktop ? 90 : 80,\n };\n }, [isMobile, screenSize]);\n\n const trainingAreaBounds = useMemo<TrainingAreaBounds>(() => {\n const areaY = layoutConstants.headerHeight + layoutConstants.padding;\n\n const worldDimensions = calculateArenaWorldDimensions(width);\n\n if (isMobile) {\n const isExtraSmall = width < 380;\n const topClearance = isExtraSmall ? 75 : 80;\n const bottomClearance = mobileControlsBottomClearance(\n layoutConstants.controlsHeight,\n layoutConstants.footerHeight,\n isExtraSmall,\n isPortrait,\n \"training\",\n );\n\n return calculateMobileAreaBounds(\n width,\n height,\n topClearance,\n bottomClearance,\n areaY,\n isPortrait ? \"portrait\" : \"landscape\",\n );\n }\n\n const totalReservedHeight =\n layoutConstants.headerHeight +\n layoutConstants.controlsHeight +\n layoutConstants.footerHeight;\n const totalPadding = layoutConstants.padding * 3;\n const availableHeight = height - totalReservedHeight - totalPadding;\n const availableWidth = getDesktopArenaWidthBudget(width);\n\n let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\n const pixelsPerMeter = arenaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - arenaWidth) / 2, // Center horizontally\n y: areaY,\n width: arenaWidth,\n height: arenaHeight, // 4:3 aspect ratio\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n }, [width, height, layoutConstants, isMobile, isPortrait]);\n\n return {\n layoutConstants,\n trainingAreaBounds,\n isMobile,\n isPortrait,\n screenSize,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgB,kBACd,OACA,QACgB;CAChB,MAAM,aAAa,cAAc,cAAc,KAAK,GAAG,CAAC,KAAK,CAAC;CAE9D,MAAM,aAAa,SAAS,QAAQ;CAEpC,MAAM,WACJ,wBAAwB,KACvB,cAAc,QAAA;CAEjB,MAAM,kBAAkB,cAAuC;EAC7D,MAAM,iBAAiB,eAAe;EACtC,MAAM,WAAW,eAAe;EAEhC,OAAO;GACL,SAAS,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GAC/D,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,MAAM;GACrE,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACpE,gBAAgB,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACtE,gBAAgB,WACZ,MACA,WACE,MACA,iBACE,MACA;GACR,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;EACtE;CACF,GAAG,CAAC,UAAU,UAAU,CAAC;CA2DzB,OAAO;EACL;EACA,oBA3DyB,cAAkC;GAC3D,MAAM,QAAQ,gBAAgB,eAAe,gBAAgB;GAE7D,MAAM,kBAAkB,8BAA8B,KAAK;GAE3D,IAAI,UAAU;IACZ,MAAM,eAAe,QAAQ;IAU7B,OAAO,0BACL,OACA,QAXmB,eAAe,KAAK,IACjB,8BACtB,gBAAgB,gBAChB,gBAAgB,cAChB,cACA,YACA,UAOA,GACA,OACA,aAAa,aAAa,WAC5B;GACF;GAEA,MAAM,sBACJ,gBAAgB,eAChB,gBAAgB,iBAChB,gBAAgB;GAClB,MAAM,eAAe,gBAAgB,UAAU;GAC/C,MAAM,kBAAkB,SAAS,sBAAsB;GAGvD,IAAI,aAFmB,2BAA2B,KAEjC;GACjB,IAAI,cAAc,cAAc,IAAI;GAEpC,IAAI,cAAc,iBAAiB;IACjC,cAAc;IACd,aAAa,eAAe,IAAI;GAClC;GAIA,MAAM,QAFiB,aAAa,gBAAgB,cAErB;GAE/B,OAAO;IACL,IAAI,QAAQ,cAAc;IAC1B,GAAG;IACH,OAAO;IACP,QAAQ;IACR;IACA,kBAAkB,gBAAgB;IAClC,kBAAkB,gBAAgB;GACpC;EACF,GAAG;GAAC;GAAO;GAAQ;GAAiB;GAAU;EAAU,CAItD;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"useTrainingLayout.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingLayout.ts"],"sourcesContent":["/**\n * useTrainingLayout Hook - Enhanced Responsive Training Layout\n *\n * Custom hook for managing responsive training screen layout calculations with\n * comprehensive support for all screen sizes from mobile to ultra-wide displays.\n *\n * Enhanced Features:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional scaling for consistent sizing across devices\n * - Optimized layout sizing for each device category\n * - Smooth transitions for resize operations\n * - 60fps performance maintained\n *\n * Uses robust device detection combining user-agent and screen size to ensure\n * mobile controls are shown on all mobile devices, including high-resolution phones.\n *\n * Performance:\n * - Reduces recalculations by checking only breakpoint changes, not exact dimensions\n * - Memoizes layout constants to prevent cascading re-renders\n * - Targets <1ms execution time for layout calculations\n *\n * @param width - Screen width\n * @param height - Screen height\n *\n * @returns Layout constants and training area bounds\n *\n * @example\n * ```typescript\n * const { layoutConstants, trainingAreaBounds, isMobile, screenSize } = useTrainingLayout(1200, 800);\n * ```\n */\n\nimport { useMemo } from \"react\";\nimport { getScreenSize } from \"../../../../systems/ResponsiveScaling\";\nimport { calculateArenaWorldDimensions } from \"../../../../utils/arenaWorldDimensions\";\nimport { shouldUseMobileControls } from \"../../../../utils/deviceDetection\";\nimport { calculateMobileAreaBounds } from \"../../../../utils/mobileLayoutHelpers\";\nimport {\n mobileControlsBottomClearance,\n PORTRAIT_FORCE_MAX_WIDTH_PX,\n PORTRAIT_HYSTERESIS_FACTOR,\n} from \"../../../../utils/responsiveOrientationConstants\";\nimport {\n getDesktopArenaWidthBudget,\n getHUDPositionScale,\n} from \"../../../../utils/responsiveLayoutHelpers\";\nimport {\n TRAINING_BOTTOM_HUD_HEIGHT_PERCENT,\n TRAINING_TOP_HUD_HEIGHT_PERCENT,\n} from \"../../../../types/constants/layout\";\nimport { getHUDHeight } from \"../../../../utils/responsiveLayout\";\n\nimport type { ScreenSize } from \"../../../../systems/ResponsiveScaling\";\n\nexport interface TrainingLayoutConstants {\n readonly padding: number;\n readonly headerHeight: number;\n readonly buttonHeight: number;\n readonly sectionSpacing: number;\n readonly controlsHeight: number;\n readonly footerHeight: number;\n}\n\nexport interface TrainingAreaBounds {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n readonly scale: number; // 3D scale factor for training area (1.0 = desktop, <1.0 = mobile)\n readonly worldWidthMeters: number; // Physical training area width in meters\n readonly worldDepthMeters: number; // Physical training area depth in meters\n}\n\nexport interface TrainingLayout {\n readonly layoutConstants: TrainingLayoutConstants;\n readonly trainingAreaBounds: TrainingAreaBounds;\n readonly isMobile: boolean;\n readonly isPortrait: boolean;\n readonly screenSize: ScreenSize;\n}\n\n/**\n * Custom hook for training screen layout calculations\n * Enhanced with centralized responsive scaling system\n * Optimized to reduce recalculations and improve 60fps performance\n */\nexport function useTrainingLayout(\n width: number,\n height: number,\n): TrainingLayout {\n const screenSize = useMemo(() => getScreenSize(width), [width]);\n\n const isPortrait = height > width * PORTRAIT_HYSTERESIS_FACTOR;\n\n const isMobile =\n shouldUseMobileControls() ||\n (isPortrait && width < PORTRAIT_FORCE_MAX_WIDTH_PX);\n\n const layoutConstants = useMemo<TrainingLayoutConstants>(() => {\n const isLargeDesktop = screenSize === \"xlarge\";\n const isTablet = screenSize === \"tablet\";\n\n return {\n padding: isMobile ? 20 : isTablet ? 25 : isLargeDesktop ? 35 : 30,\n headerHeight: isMobile ? 80 : isTablet ? 90 : isLargeDesktop ? 110 : 100,\n buttonHeight: isMobile ? 45 : isTablet ? 50 : isLargeDesktop ? 60 : 55,\n sectionSpacing: isMobile ? 15 : isTablet ? 18 : isLargeDesktop ? 25 : 20,\n controlsHeight: isMobile\n ? 120\n : isTablet\n ? 110\n : isLargeDesktop\n ? 150\n : 130,\n footerHeight: isMobile ? 60 : isTablet ? 70 : isLargeDesktop ? 90 : 80,\n };\n }, [isMobile, screenSize]);\n\n const positionScale = useMemo(\n () => getHUDPositionScale(screenSize, isMobile),\n [screenSize, isMobile],\n );\n\n const trainingAreaBounds = useMemo<TrainingAreaBounds>(() => {\n const topHudHeight =\n getHUDHeight(height, TRAINING_TOP_HUD_HEIGHT_PERCENT) * positionScale;\n const bottomHudHeight =\n getHUDHeight(height, TRAINING_BOTTOM_HUD_HEIGHT_PERCENT) * positionScale;\n const areaY = topHudHeight + layoutConstants.padding;\n\n const worldDimensions = calculateArenaWorldDimensions(width);\n\n if (isMobile) {\n const isExtraSmall = width < 380;\n const topClearance = isExtraSmall ? 75 : 80;\n const bottomClearance = mobileControlsBottomClearance(\n layoutConstants.controlsHeight,\n Math.max(layoutConstants.footerHeight, bottomHudHeight),\n isExtraSmall,\n isPortrait,\n \"training\",\n );\n\n return calculateMobileAreaBounds(\n width,\n height,\n topClearance,\n bottomClearance,\n areaY,\n isPortrait ? \"portrait\" : \"landscape\",\n );\n }\n\n const totalReservedHeight = topHudHeight + bottomHudHeight;\n const totalPadding = layoutConstants.padding * 3;\n const availableHeight = height - totalReservedHeight - totalPadding;\n const availableWidth = getDesktopArenaWidthBudget(width);\n\n let arenaWidth = availableWidth;\n let arenaHeight = arenaWidth * (3 / 4); // 4:3 aspect ratio\n\n if (arenaHeight > availableHeight) {\n arenaHeight = availableHeight;\n arenaWidth = arenaHeight * (4 / 3);\n }\n\n const pixelsPerMeter = arenaWidth / worldDimensions.widthMeters;\n const referencePixelsPerMeter = 100;\n const scale = pixelsPerMeter / referencePixelsPerMeter;\n\n return {\n x: (width - arenaWidth) / 2, // Center horizontally\n y: areaY,\n width: arenaWidth,\n height: arenaHeight, // 4:3 aspect ratio\n scale,\n worldWidthMeters: worldDimensions.widthMeters,\n worldDepthMeters: worldDimensions.depthMeters,\n };\n }, [width, height, layoutConstants, isMobile, isPortrait, positionScale]);\n\n return {\n layoutConstants,\n trainingAreaBounds,\n isMobile,\n isPortrait,\n screenSize,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,SAAgB,kBACd,OACA,QACgB;CAChB,MAAM,aAAa,cAAc,cAAc,KAAK,GAAG,CAAC,KAAK,CAAC;CAE9D,MAAM,aAAa,SAAS,QAAQ;CAEpC,MAAM,WACJ,wBAAwB,KACvB,cAAc,QAAA;CAEjB,MAAM,kBAAkB,cAAuC;EAC7D,MAAM,iBAAiB,eAAe;EACtC,MAAM,WAAW,eAAe;EAEhC,OAAO;GACL,SAAS,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GAC/D,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,MAAM;GACrE,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACpE,gBAAgB,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;GACtE,gBAAgB,WACZ,MACA,WACE,MACA,iBACE,MACA;GACR,cAAc,WAAW,KAAK,WAAW,KAAK,iBAAiB,KAAK;EACtE;CACF,GAAG,CAAC,UAAU,UAAU,CAAC;CAEzB,MAAM,gBAAgB,cACd,oBAAoB,YAAY,QAAQ,GAC9C,CAAC,YAAY,QAAQ,CACvB;CA4DA,OAAO;EACL;EACA,oBA5DyB,cAAkC;GAC3D,MAAM,eACJ,aAAa,QAAQ,+BAA+B,IAAI;GAC1D,MAAM,kBACJ,aAAa,QAAQ,kCAAkC,IAAI;GAC7D,MAAM,QAAQ,eAAe,gBAAgB;GAE7C,MAAM,kBAAkB,8BAA8B,KAAK;GAE3D,IAAI,UAAU;IACZ,MAAM,eAAe,QAAQ;IAU7B,OAAO,0BACL,OACA,QAXmB,eAAe,KAAK,IACjB,8BACtB,gBAAgB,gBAChB,KAAK,IAAI,gBAAgB,cAAc,eAAe,GACtD,cACA,YACA,UAOA,GACA,OACA,aAAa,aAAa,WAC5B;GACF;GAEA,MAAM,sBAAsB,eAAe;GAC3C,MAAM,eAAe,gBAAgB,UAAU;GAC/C,MAAM,kBAAkB,SAAS,sBAAsB;GAGvD,IAAI,aAFmB,2BAA2B,KAEjC;GACjB,IAAI,cAAc,cAAc,IAAI;GAEpC,IAAI,cAAc,iBAAiB;IACjC,cAAc;IACd,aAAa,eAAe,IAAI;GAClC;GAIA,MAAM,QAFiB,aAAa,gBAAgB,cAErB;GAE/B,OAAO;IACL,IAAI,QAAQ,cAAc;IAC1B,GAAG;IACH,OAAO;IACP,QAAQ;IACR;IACA,kBAAkB,gBAAgB;IAClC,kBAAkB,gBAAgB;GACpC;EACF,GAAG;GAAC;GAAO;GAAQ;GAAiB;GAAU;GAAY;EAAa,CAIrE;EACA;EACA;EACA;CACF;AACF"}
@@ -183,7 +183,7 @@ var SplashScreen = ({ onStart, width, height }) => {
183
183
  }),
184
184
  /* @__PURE__ */ jsxs("div", {
185
185
  role: "contentinfo",
186
- "aria-label": `Application version 0.7.48`,
186
+ "aria-label": `Application version 0.7.49`,
187
187
  style: {
188
188
  position: "absolute",
189
189
  bottom: "20px",
@@ -192,7 +192,7 @@ var SplashScreen = ({ onStart, width, height }) => {
192
192
  fontSize: "10px",
193
193
  zIndex: 1
194
194
  },
195
- children: ["v", "0.7.48"]
195
+ children: ["v", "0.7.49"]
196
196
  })
197
197
  ]
198
198
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useHUDLayout.d.ts","sourceRoot":"","sources":["../../src/hooks/useHUDLayout.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAG5E;;;GAGG;AACH,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,0BAA0B;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2BAA2B;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,UAAU,GAAG,QAAqB,EAC3C,eAAe,CAAC,EAAE,MAAM,EACxB,WAAW,CAAC,EAAE,MAAM,GACnB,eAAe,CAwEjB"}
1
+ {"version":3,"file":"useHUDLayout.d.ts","sourceRoot":"","sources":["../../src/hooks/useHUDLayout.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0CAA0C,CAAC;AAS5E;;;GAGG;AACH,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,0BAA0B;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2BAA2B;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,UAAU,GAAG,QAAqB,EAC3C,eAAe,CAAC,EAAE,MAAM,EACxB,WAAW,CAAC,EAAE,MAAM,GACnB,eAAe,CAyEjB"}
@@ -1,3 +1,4 @@
1
+ import { COMBAT_BOTTOM_HUD_HEIGHT_PERCENT, COMBAT_TOP_HUD_HEIGHT_PERCENT, TRAINING_BOTTOM_HUD_HEIGHT_PERCENT, TRAINING_TOP_HUD_HEIGHT_PERCENT } from "../types/constants/layout.js";
1
2
  import { getHUDHeight, getResponsivePadding, getResponsiveSize } from "../utils/responsiveLayout.js";
2
3
  import { useMemo } from "react";
3
4
  //#region src/hooks/useHUDLayout.ts
@@ -44,8 +45,8 @@ function useHUDLayout(width, height, positionScale, position, context = "trainin
44
45
  tablet: 16,
45
46
  desktop: 14
46
47
  }) / 100 : 1;
47
- const topHeightPercent = context === "training" ? .06 : .08;
48
- const bottomHeightPercent = context === "training" ? .11 : .12;
48
+ const topHeightPercent = context === "training" ? TRAINING_TOP_HUD_HEIGHT_PERCENT : COMBAT_TOP_HUD_HEIGHT_PERCENT;
49
+ const bottomHeightPercent = context === "training" ? TRAINING_BOTTOM_HUD_HEIGHT_PERCENT : COMBAT_BOTTOM_HUD_HEIGHT_PERCENT;
49
50
  const scaledTopHeight = getHUDHeight(height, topHeightPercent) * positionScale;
50
51
  const scaledBottomHeight = getHUDHeight(height, bottomHeightPercent) * positionScale;
51
52
  const hudWidth = Math.round(width * hudWidthPercent);
@@ -1 +1 @@
1
- {"version":3,"file":"useHUDLayout.js","names":[],"sources":["../../src/hooks/useHUDLayout.ts"],"sourcesContent":["/**\n * useHUDLayout - Centralized HUD layout calculations\n * \n * Extracts common HUD layout patterns used across Training and Combat screens.\n * Provides resolution-based responsive sizing, positioning, and spacing calculations.\n * \n * @module hooks\n * @korean HUD레이아웃훅 - 중앙화된 HUD 레이아웃 계산\n */\n\nimport { useMemo } from 'react';\nimport type { HUDPosition } from '../components/shared/ui/BaseHUDContainer';\nimport { getResponsiveSize, getHUDHeight, getResponsivePadding } from '../utils/responsiveLayout';\n\n/**\n * HUD position type - left, right, top, or bottom\n * Re-exported from BaseHUDContainer to maintain single source of truth\n */\nexport type { HUDPosition };\n\n/**\n * Result of HUD layout calculations\n */\nexport interface HUDLayoutResult {\n /** HUD width as percentage of screen (0.0-1.0) */\n readonly hudWidthPercent: number;\n /** HUD height as percentage of screen (0.0-1.0) */\n readonly hudHeightPercent: number;\n /** HUD width in pixels */\n readonly hudWidth: number;\n /** HUD height in pixels */\n readonly hudHeight: number;\n /** Top offset in pixels (for left/right HUDs) */\n readonly topOffset: number;\n /** Bottom offset in pixels (for left/right HUDs) */\n readonly bottomOffset: number;\n /** Available height between top and bottom bars */\n readonly availableHeight: number;\n /** Internal padding in pixels */\n readonly padding: number;\n /** Gap between sections in pixels */\n readonly gap: number;\n}\n\n/**\n * Custom hook for calculating HUD layout dimensions\n * \n * Provides consistent layout calculations across Training and Combat screens.\n * Uses resolution-based responsive sizing for smooth scaling across all screen sizes.\n * \n * @param width - Screen width in pixels\n * @param height - Screen height in pixels\n * @param positionScale - Position scale multiplier for large displays (1.0-1.5)\n * @param position - HUD position (left, right, top, bottom)\n * @param context - Context ('training' or 'combat') for context-specific dimensions\n * @param paddingOverride - Optional padding override (final pixel value, already scaled). When provided, bypasses default padding calculation.\n * @param gapOverride - Optional gap override (final pixel value, already scaled). When provided, bypasses default gap calculation.\n * @returns Calculated layout dimensions and offsets\n * \n * @example\n * ```tsx\n * const layout = useHUDLayout(\n * 1920, 1080, 1.0, 'left', 'training'\n * );\n * // Resolution-based sizing interpolates smoothly between breakpoints\n * // layout.hudWidth = 269 (14% of 1920 for desktop)\n * // layout.topOffset = 64.8 (getHUDHeight(1080, 0.06) = 64.8)\n * // layout.bottomOffset = 118.8 (getHUDHeight(1080, 0.11) = 118.8)\n * // layout.availableHeight = 896.4 (1080 - 64.8 - 118.8)\n * ```\n */\nexport function useHUDLayout(\n width: number,\n height: number,\n positionScale: number,\n position: HUDPosition,\n context: 'training' | 'combat' = 'training',\n paddingOverride?: number,\n gapOverride?: number\n): HUDLayoutResult {\n return useMemo(() => {\n\n // Resolution-based width calculation: interpolates smoothly between breakpoints\n // Left/Right HUDs: 14-18% of screen width (mobile: 18%, tablet: 16%, desktop: 14%)\n // Top/Bottom HUDs: 100% width\n const hudWidthPercent = (position === 'left' || position === 'right')\n ? getResponsiveSize(width, {\n mobile: 18, // 18% for small screens\n tablet: 16, // 16% for medium screens\n desktop: 14, // 14% for large screens\n }) / 100 // Convert to decimal for percentage calculation\n : 1.0; // Top and bottom HUDs use full width\n\n // Resolution-based height calculation for top/bottom bars\n // Training: ~6% for top, ~11% for bottom\n // Combat: ~8% for top, ~12% for bottom\n const topHeightPercent = context === 'training' ? 0.06 : 0.08;\n const bottomHeightPercent = context === 'training' ? 0.11 : 0.12;\n \n const scaledTopHeight = getHUDHeight(height, topHeightPercent) * positionScale;\n const scaledBottomHeight = getHUDHeight(height, bottomHeightPercent) * positionScale;\n\n // Calculate HUD dimensions in pixels\n const hudWidth = Math.round(width * hudWidthPercent);\n const hudHeight = position === 'top' || position === 'bottom'\n ? (position === 'top' ? scaledTopHeight : scaledBottomHeight)\n : Math.max(0, height - scaledTopHeight - scaledBottomHeight);\n\n // Calculate offsets for left/right HUDs\n const topOffset = scaledTopHeight;\n const bottomOffset = scaledBottomHeight;\n const availableHeight = Math.max(0, height - topOffset - bottomOffset);\n\n // Resolution-based padding using responsive utility\n const defaultPadding = getResponsivePadding(width) * positionScale;\n \n // Resolution-based gap: context-specific values\n // Training uses slightly larger gaps than combat for better readability\n const defaultGap = context === 'training'\n ? getResponsiveSize(width, {\n mobile: 12,\n tablet: 15,\n desktop: 18,\n }) * positionScale\n : getResponsiveSize(width, {\n mobile: 10,\n tablet: 12,\n desktop: 14,\n }) * positionScale;\n \n // Use overrides if provided, otherwise use defaults\n // Overrides allow per-position customization (e.g., TrainingRightHUD uses tighter spacing)\n const padding = paddingOverride ?? defaultPadding;\n const gap = gapOverride ?? defaultGap;\n\n // Guard against division by zero and ensure valid percentage\n const safeHeight = Math.max(height, 1);\n const hudHeightPercent = Math.max(0, Math.min(1, hudHeight / safeHeight));\n\n return {\n hudWidthPercent,\n hudHeightPercent,\n hudWidth,\n hudHeight,\n topOffset,\n bottomOffset,\n availableHeight,\n padding,\n gap,\n };\n }, [width, height, positionScale, position, context, paddingOverride, gapOverride]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEA,SAAgB,aACd,OACA,QACA,eACA,UACA,UAAiC,YACjC,iBACA,aACiB;CACjB,OAAO,cAAc;EAKnB,MAAM,kBAAmB,aAAa,UAAU,aAAa,UACzD,kBAAkB,OAAO;GACvB,QAAQ;GACR,QAAQ;GACR,SAAS;EACX,CAAC,IAAI,MACL;EAKJ,MAAM,mBAAmB,YAAY,aAAa,MAAO;EACzD,MAAM,sBAAsB,YAAY,aAAa,MAAO;EAE5D,MAAM,kBAAkB,aAAa,QAAQ,gBAAgB,IAAI;EACjE,MAAM,qBAAqB,aAAa,QAAQ,mBAAmB,IAAI;EAGvE,MAAM,WAAW,KAAK,MAAM,QAAQ,eAAe;EACnD,MAAM,YAAY,aAAa,SAAS,aAAa,WAChD,aAAa,QAAQ,kBAAkB,qBACxC,KAAK,IAAI,GAAG,SAAS,kBAAkB,kBAAkB;EAG7D,MAAM,YAAY;EAClB,MAAM,eAAe;EACrB,MAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS,YAAY,YAAY;EAGrE,MAAM,iBAAiB,qBAAqB,KAAK,IAAI;EAIrD,MAAM,aAAa,YAAY,aAC3B,kBAAkB,OAAO;GACvB,QAAQ;GACR,QAAQ;GACR,SAAS;EACX,CAAC,IAAI,gBACL,kBAAkB,OAAO;GACvB,QAAQ;GACR,QAAQ;GACR,SAAS;EACX,CAAC,IAAI;EAIT,MAAM,UAAU,mBAAmB;EACnC,MAAM,MAAM,eAAe;EAM3B,OAAO;GACL;GACA,kBAJuB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,YAD9B,KAAK,IAAI,QAAQ,CACyB,CAAU,CAIrE;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;CACF,GAAG;EAAC;EAAO;EAAQ;EAAe;EAAU;EAAS;EAAiB;CAAW,CAAC;AACpF"}
1
+ {"version":3,"file":"useHUDLayout.js","names":[],"sources":["../../src/hooks/useHUDLayout.ts"],"sourcesContent":["/**\n * useHUDLayout - Centralized HUD layout calculations\n * \n * Extracts common HUD layout patterns used across Training and Combat screens.\n * Provides resolution-based responsive sizing, positioning, and spacing calculations.\n * \n * @module hooks\n * @korean HUD레이아웃훅 - 중앙화된 HUD 레이아웃 계산\n */\n\nimport { useMemo } from 'react';\nimport type { HUDPosition } from '../components/shared/ui/BaseHUDContainer';\nimport {\n COMBAT_BOTTOM_HUD_HEIGHT_PERCENT,\n COMBAT_TOP_HUD_HEIGHT_PERCENT,\n TRAINING_BOTTOM_HUD_HEIGHT_PERCENT,\n TRAINING_TOP_HUD_HEIGHT_PERCENT,\n} from '../types/constants/layout';\nimport { getResponsiveSize, getHUDHeight, getResponsivePadding } from '../utils/responsiveLayout';\n\n/**\n * HUD position type - left, right, top, or bottom\n * Re-exported from BaseHUDContainer to maintain single source of truth\n */\nexport type { HUDPosition };\n\n/**\n * Result of HUD layout calculations\n */\nexport interface HUDLayoutResult {\n /** HUD width as percentage of screen (0.0-1.0) */\n readonly hudWidthPercent: number;\n /** HUD height as percentage of screen (0.0-1.0) */\n readonly hudHeightPercent: number;\n /** HUD width in pixels */\n readonly hudWidth: number;\n /** HUD height in pixels */\n readonly hudHeight: number;\n /** Top offset in pixels (for left/right HUDs) */\n readonly topOffset: number;\n /** Bottom offset in pixels (for left/right HUDs) */\n readonly bottomOffset: number;\n /** Available height between top and bottom bars */\n readonly availableHeight: number;\n /** Internal padding in pixels */\n readonly padding: number;\n /** Gap between sections in pixels */\n readonly gap: number;\n}\n\n/**\n * Custom hook for calculating HUD layout dimensions\n * \n * Provides consistent layout calculations across Training and Combat screens.\n * Uses resolution-based responsive sizing for smooth scaling across all screen sizes.\n * \n * @param width - Screen width in pixels\n * @param height - Screen height in pixels\n * @param positionScale - Position scale multiplier for large displays (1.0-1.5)\n * @param position - HUD position (left, right, top, bottom)\n * @param context - Context ('training' or 'combat') for context-specific dimensions\n * @param paddingOverride - Optional padding override (final pixel value, already scaled). When provided, bypasses default padding calculation.\n * @param gapOverride - Optional gap override (final pixel value, already scaled). When provided, bypasses default gap calculation.\n * @returns Calculated layout dimensions and offsets\n * \n * @example\n * ```tsx\n * const layout = useHUDLayout(\n * 1920, 1080, 1.0, 'left', 'training'\n * );\n * // Resolution-based sizing interpolates smoothly between breakpoints\n * // layout.hudWidth = 269 (14% of 1920 for desktop)\n * // layout.topOffset = 64.8 (getHUDHeight(1080, 0.06) = 64.8)\n * // layout.bottomOffset = 118.8 (getHUDHeight(1080, 0.11) = 118.8)\n * // layout.availableHeight = 896.4 (1080 - 64.8 - 118.8)\n * ```\n */\nexport function useHUDLayout(\n width: number,\n height: number,\n positionScale: number,\n position: HUDPosition,\n context: 'training' | 'combat' = 'training',\n paddingOverride?: number,\n gapOverride?: number\n): HUDLayoutResult {\n return useMemo(() => {\n\n // Resolution-based width calculation: interpolates smoothly between breakpoints\n // Left/Right HUDs: 14-18% of screen width (mobile: 18%, tablet: 16%, desktop: 14%)\n // Top/Bottom HUDs: 100% width\n const hudWidthPercent = (position === 'left' || position === 'right')\n ? getResponsiveSize(width, {\n mobile: 18, // 18% for small screens\n tablet: 16, // 16% for medium screens\n desktop: 14, // 14% for large screens\n }) / 100 // Convert to decimal for percentage calculation\n : 1.0; // Top and bottom HUDs use full width\n\n const topHeightPercent = context === 'training'\n ? TRAINING_TOP_HUD_HEIGHT_PERCENT\n : COMBAT_TOP_HUD_HEIGHT_PERCENT;\n const bottomHeightPercent = context === 'training'\n ? TRAINING_BOTTOM_HUD_HEIGHT_PERCENT\n : COMBAT_BOTTOM_HUD_HEIGHT_PERCENT;\n \n const scaledTopHeight = getHUDHeight(height, topHeightPercent) * positionScale;\n const scaledBottomHeight = getHUDHeight(height, bottomHeightPercent) * positionScale;\n\n // Calculate HUD dimensions in pixels\n const hudWidth = Math.round(width * hudWidthPercent);\n const hudHeight = position === 'top' || position === 'bottom'\n ? (position === 'top' ? scaledTopHeight : scaledBottomHeight)\n : Math.max(0, height - scaledTopHeight - scaledBottomHeight);\n\n // Calculate offsets for left/right HUDs\n const topOffset = scaledTopHeight;\n const bottomOffset = scaledBottomHeight;\n const availableHeight = Math.max(0, height - topOffset - bottomOffset);\n\n // Resolution-based padding using responsive utility\n const defaultPadding = getResponsivePadding(width) * positionScale;\n \n // Resolution-based gap: context-specific values\n // Training uses slightly larger gaps than combat for better readability\n const defaultGap = context === 'training'\n ? getResponsiveSize(width, {\n mobile: 12,\n tablet: 15,\n desktop: 18,\n }) * positionScale\n : getResponsiveSize(width, {\n mobile: 10,\n tablet: 12,\n desktop: 14,\n }) * positionScale;\n \n // Use overrides if provided, otherwise use defaults\n // Overrides allow per-position customization (e.g., TrainingRightHUD uses tighter spacing)\n const padding = paddingOverride ?? defaultPadding;\n const gap = gapOverride ?? defaultGap;\n\n // Guard against division by zero and ensure valid percentage\n const safeHeight = Math.max(height, 1);\n const hudHeightPercent = Math.max(0, Math.min(1, hudHeight / safeHeight));\n\n return {\n hudWidthPercent,\n hudHeightPercent,\n hudWidth,\n hudHeight,\n topOffset,\n bottomOffset,\n availableHeight,\n padding,\n gap,\n };\n }, [width, height, positionScale, position, context, paddingOverride, gapOverride]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA,SAAgB,aACd,OACA,QACA,eACA,UACA,UAAiC,YACjC,iBACA,aACiB;CACjB,OAAO,cAAc;EAKnB,MAAM,kBAAmB,aAAa,UAAU,aAAa,UACzD,kBAAkB,OAAO;GACvB,QAAQ;GACR,QAAQ;GACR,SAAS;EACX,CAAC,IAAI,MACL;EAEJ,MAAM,mBAAmB,YAAY,aACjC,kCACA;EACJ,MAAM,sBAAsB,YAAY,aACpC,qCACA;EAEJ,MAAM,kBAAkB,aAAa,QAAQ,gBAAgB,IAAI;EACjE,MAAM,qBAAqB,aAAa,QAAQ,mBAAmB,IAAI;EAGvE,MAAM,WAAW,KAAK,MAAM,QAAQ,eAAe;EACnD,MAAM,YAAY,aAAa,SAAS,aAAa,WAChD,aAAa,QAAQ,kBAAkB,qBACxC,KAAK,IAAI,GAAG,SAAS,kBAAkB,kBAAkB;EAG7D,MAAM,YAAY;EAClB,MAAM,eAAe;EACrB,MAAM,kBAAkB,KAAK,IAAI,GAAG,SAAS,YAAY,YAAY;EAGrE,MAAM,iBAAiB,qBAAqB,KAAK,IAAI;EAIrD,MAAM,aAAa,YAAY,aAC3B,kBAAkB,OAAO;GACvB,QAAQ;GACR,QAAQ;GACR,SAAS;EACX,CAAC,IAAI,gBACL,kBAAkB,OAAO;GACvB,QAAQ;GACR,QAAQ;GACR,SAAS;EACX,CAAC,IAAI;EAIT,MAAM,UAAU,mBAAmB;EACnC,MAAM,MAAM,eAAe;EAM3B,OAAO;GACL;GACA,kBAJuB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,YAD9B,KAAK,IAAI,QAAQ,CACyB,CAAU,CAIrE;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;CACF,GAAG;EAAC;EAAO;EAAQ;EAAe;EAAU;EAAS;EAAiB;CAAW,CAAC;AACpF"}
@@ -77,6 +77,27 @@ export declare const TECHNIQUE_BAR_MIN_READABLE_SCALE = 0.7;
77
77
  * top HUD and side HUDs align without overlap across responsive breakpoints.
78
78
  */
79
79
  export declare const TRAINING_TOP_HUD_HEIGHT_PERCENT = 0.06;
80
+ /**
81
+ * Combat top HUD height ratio.
82
+ *
83
+ * Shared by CombatTopHUD, side HUD offsets, and arena layout reservations so
84
+ * the 3D combat area starts immediately below the visible top bar.
85
+ */
86
+ export declare const COMBAT_TOP_HUD_HEIGHT_PERCENT = 0.06;
87
+ /**
88
+ * Combat bottom HUD height ratio.
89
+ *
90
+ * Shared by CombatBottomHUD, side HUD offsets, and arena layout reservations so
91
+ * technique controls fit without over-reserving vertical arena space.
92
+ */
93
+ export declare const COMBAT_BOTTOM_HUD_HEIGHT_PERCENT = 0.1;
94
+ /**
95
+ * Training bottom HUD height ratio.
96
+ *
97
+ * Shared by TrainingBottomHUD, side HUD offsets, and training area layout
98
+ * reservations so the dojang content is framed consistently.
99
+ */
100
+ export declare const TRAINING_BOTTOM_HUD_HEIGHT_PERCENT = 0.11;
80
101
  /**
81
102
  * Top positioning for UI elements (in pixels)
82
103
  * Used for elements positioned from the top of the screen