blacktrigram 0.7.111 → 0.7.113
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.
- package/lib/assets/index.css +0 -1
- package/lib/audio/AudioManager.js +22 -20
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioPool.js +6 -4
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/VariantSelector.js +12 -8
- package/lib/audio/VariantSelector.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +4 -3
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.js +4 -2
- package/lib/components/shared/mobile/HapticController.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js +7 -5
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js +7 -5
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js +7 -5
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js +4 -2
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +6 -4
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTouchControls.js +18 -17
- package/lib/hooks/useTouchControls.js.map +1 -1
- package/lib/systems/animation/builders/AnimationBuilder.js +6 -2
- package/lib/systems/animation/builders/AnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/PunchPhaseApplicator.js +8 -4
- package/lib/systems/animation/builders/PunchPhaseApplicator.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js +39 -37
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js +25 -24
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/utils/deviceDetection.js +14 -13
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/inputSystem.js +4 -2
- package/lib/utils/inputSystem.js.map +1 -1
- package/package.json +6 -6
|
@@ -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 { 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
|
+
{"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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"}
|
|
@@ -171,8 +171,10 @@ var HapticController = class HapticController {
|
|
|
171
171
|
this.lastTriggerTime = now;
|
|
172
172
|
try {
|
|
173
173
|
let adaptedPattern = pattern;
|
|
174
|
-
if (this.performanceTier === "low")
|
|
175
|
-
|
|
174
|
+
if (this.performanceTier === "low") {
|
|
175
|
+
if (Array.isArray(pattern)) adaptedPattern = pattern.map((duration) => Math.floor(duration * .5));
|
|
176
|
+
else adaptedPattern = Math.floor(pattern * .5);
|
|
177
|
+
}
|
|
176
178
|
return navigator.vibrate(adaptedPattern) !== false;
|
|
177
179
|
} catch (error) {
|
|
178
180
|
console.warn("Custom haptic feedback failed:", error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HapticController.js","names":[],"sources":["../../../../src/components/shared/mobile/HapticController.ts"],"sourcesContent":["/**\n * HapticController\n * \n * Optimized haptic feedback system with device capability detection\n * Provides differentiated haptic patterns without causing frame drops\n * \n * Key Features:\n * - Device capability detection (auto-disable on low-end devices)\n * - Differentiated feedback intensities (light, medium, strong)\n * - Frame-drop prevention (<2ms overhead)\n * - Adaptive patterns based on device performance\n * - Throttling to prevent excessive haptic triggers\n * \n * Browser Compatibility Note:\n * navigator.vibrate() returns boolean in some browsers (Chrome, Edge) and void in others (Firefox).\n * This implementation normalizes return values to boolean for consistency.\n * \n * @module components/mobile/HapticController\n * @category Mobile Controls\n * @korean 햅틱 컨트롤러\n */\n\n/**\n * Haptic intensity levels\n */\nexport type HapticIntensity = 'light' | 'medium' | 'strong' | 'disabled';\n\n/**\n * Device performance tier\n */\nexport type DevicePerformanceTier = 'high' | 'medium' | 'low';\n\n/**\n * Haptic pattern configuration\n */\ninterface HapticPattern {\n readonly vibration: number | number[];\n readonly maxFrameTime: number; // Maximum frame time impact in ms\n}\n\n/**\n * Optimized haptic patterns for different intensities\n * Designed to provide clear feedback without frame drops\n * \n * @korean 햅틱 패턴\n */\nconst HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [20], // Short, crisp tap\n maxFrameTime: 0.5, // Minimal impact\n },\n medium: {\n vibration: [40], // Medium pulse\n maxFrameTime: 1.0, // Small impact\n },\n strong: {\n vibration: [60], // Strong pulse\n maxFrameTime: 2.0, // Noticeable impact\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * Adaptive haptic patterns for low-end devices\n * Reduced intensities to prevent frame drops\n * \n * @korean 적응형 햅틱 패턴\n */\nconst ADAPTIVE_HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [10], // Minimal vibration\n maxFrameTime: 0.2,\n },\n medium: {\n vibration: [20], // Reduced medium\n maxFrameTime: 0.5,\n },\n strong: {\n vibration: [30], // Reduced strong\n maxFrameTime: 1.0,\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * HapticController class\n * Manages haptic feedback with device-aware optimization\n * \n * @korean 햅틱컨트롤러\n */\nexport class HapticController {\n private static instance: HapticController | null = null;\n private isSupported: boolean = false;\n private isEnabled: boolean = true;\n private performanceTier: DevicePerformanceTier = 'high';\n /**\n * Last trigger timestamp for throttling.\n * Initialized to -Infinity to ensure the first trigger always succeeds.\n * Using -Infinity instead of 0 prevents throttling when performance.now() returns 0,\n * which can occur in test environments or immediately after page load.\n */\n private lastTriggerTime: number = -Infinity;\n private minTriggerInterval: number = 50; // Minimum 50ms between haptics\n\n /**\n * Private constructor for singleton pattern\n */\n private constructor() {\n this.isSupported = this.detectHapticSupport();\n this.performanceTier = this.detectPerformanceTier();\n \n if (this.performanceTier === 'low') {\n this.isEnabled = false;\n }\n }\n\n /**\n * Get singleton instance\n * \n * @returns HapticController instance\n * @korean 인스턴스가져오기\n */\n public static getInstance(): HapticController {\n this.instance ??= new HapticController();\n return this.instance;\n }\n\n /**\n * Detect if haptic feedback is supported\n * \n * @returns True if Vibration API is available\n * @korean 햅틱지원감지\n */\n private detectHapticSupport(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n return 'vibrate' in navigator;\n }\n\n /**\n * Detect device performance tier\n * Uses multiple heuristics to determine device capability\n * \n * @returns Performance tier (high, medium, low)\n * @korean 성능등급감지\n */\n private detectPerformanceTier(): DevicePerformanceTier {\n if (typeof navigator === 'undefined') {\n return 'medium';\n }\n\n const cores = navigator.hardwareConcurrency ?? 4;\n \n const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 4;\n \n const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);\n \n let score = 0;\n \n if (cores >= 8) score += 2;\n else if (cores >= 4) score += 1;\n \n if (memory >= 8) score += 2;\n else if (memory >= 4) score += 1;\n \n if (!isMobile) score += 1;\n \n if (score >= 4) return 'high';\n if (score >= 2) return 'medium';\n return 'low';\n }\n\n /**\n * Trigger haptic feedback with optimized patterns\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Light haptic for button tap\n * haptic.trigger('light');\n * \n * // Strong haptic for critical hit\n * haptic.trigger('strong');\n * ```\n * \n */\n public trigger(intensity: HapticIntensity): boolean {\n if (!this.isSupported || !this.isEnabled || intensity === 'disabled') {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n const patterns = this.performanceTier === 'low' \n ? ADAPTIVE_HAPTIC_PATTERNS \n : HAPTIC_PATTERNS;\n \n const pattern = patterns[intensity];\n\n try {\n const result = navigator.vibrate(pattern.vibration);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Custom combo pattern\n * haptic.triggerCustom([30, 20, 30, 20, 50]);\n * ```\n * \n */\n public triggerCustom(pattern: number | number[]): boolean {\n if (!this.isSupported || !this.isEnabled) {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n try {\n let adaptedPattern = pattern;\n if (this.performanceTier === 'low') {\n if (Array.isArray(pattern)) {\n adaptedPattern = pattern.map(duration => Math.floor(duration * 0.5));\n } else {\n adaptedPattern = Math.floor(pattern * 0.5);\n }\n }\n\n const result = navigator.vibrate(adaptedPattern);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Custom haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Stop any ongoing haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * \n */\n public stop(): boolean {\n if (!this.isSupported) {\n return false;\n }\n\n try {\n const result = navigator.vibrate(0);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Failed to stop haptic feedback:', error);\n return false;\n }\n }\n\n /**\n * Enable haptic feedback\n * \n * @korean 햅틱활성화\n */\n public enable(): void {\n this.isEnabled = true;\n }\n\n /**\n * Disable haptic feedback\n * \n * @korean 햅틱비활성화\n */\n public disable(): void {\n this.isEnabled = false;\n this.stop();\n }\n\n /**\n * Check if haptic is currently enabled\n * \n * @returns True if haptic is enabled\n * @korean 햅틱활성화상태\n */\n public isHapticEnabled(): boolean {\n return this.isSupported && this.isEnabled;\n }\n\n /**\n * Get current device performance tier\n * \n * @returns Performance tier\n * @korean 성능등급가져오기\n */\n public getPerformanceTier(): DevicePerformanceTier {\n return this.performanceTier;\n }\n\n /**\n * Set minimum interval between haptic triggers\n * \n * @param intervalMs - Minimum interval in milliseconds\n * @korean 최소간격설정\n */\n public setMinTriggerInterval(intervalMs: number): void {\n this.minTriggerInterval = Math.max(0, intervalMs);\n }\n}\n\n/**\n * Convenience function to trigger haptic feedback\n * Uses singleton HapticController instance\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * // Light haptic for UI interaction\n * triggerOptimizedHaptic('light');\n * \n * // Medium haptic for combat action\n * triggerOptimizedHaptic('medium');\n * \n * // Strong haptic for critical hit\n * triggerOptimizedHaptic('strong');\n * ```\n * \n */\nexport function triggerOptimizedHaptic(intensity: HapticIntensity): boolean {\n return HapticController.getInstance().trigger(intensity);\n}\n\n/**\n * Convenience function to trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n */\nexport function triggerCustomOptimizedHaptic(pattern: number | number[]): boolean {\n return HapticController.getInstance().triggerCustom(pattern);\n}\n\n/**\n * Convenience function to stop haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n */\nexport function stopOptimizedHaptic(): boolean {\n return HapticController.getInstance().stop();\n}\n\n/**\n * Combat-specific optimized haptic patterns\n * Pre-configured for common combat scenarios\n * \n * @korean 전투 햅틱 패턴\n */\nexport const OptimizedCombatHaptics = {\n /**\n * Standard attack hit feedback\n * @korean 일반 공격\n */\n attack: () => triggerOptimizedHaptic('medium'),\n\n /**\n * Block successful feedback\n * @korean 방어 성공\n */\n block: () => triggerOptimizedHaptic('light'),\n\n /**\n * Critical hit feedback with double pulse\n * @korean 크리티컬 히트\n */\n criticalHit: () => triggerCustomOptimizedHaptic([40, 20, 60]),\n\n /**\n * Vital point strike feedback\n * @korean 급소 타격\n */\n vitalPointStrike: () => triggerOptimizedHaptic('strong'),\n\n /**\n * Stance change feedback\n * @korean 자세 변경\n */\n stanceChange: () => triggerOptimizedHaptic('light'),\n\n /**\n * Combo counter increment\n * @korean 콤보 카운터\n */\n comboIncrement: () => triggerOptimizedHaptic('light'),\n\n /**\n * Player KO feedback with extended pattern\n * @korean 플레이어 KO\n */\n knockout: () => triggerCustomOptimizedHaptic([60, 30, 60, 30, 100]),\n\n /**\n * Error or invalid action feedback\n * @korean 오류 피드백\n */\n error: () => triggerCustomOptimizedHaptic([15, 10, 15]),\n} as const;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HapticController.js","names":[],"sources":["../../../../src/components/shared/mobile/HapticController.ts"],"sourcesContent":["/**\n * HapticController\n * \n * Optimized haptic feedback system with device capability detection\n * Provides differentiated haptic patterns without causing frame drops\n * \n * Key Features:\n * - Device capability detection (auto-disable on low-end devices)\n * - Differentiated feedback intensities (light, medium, strong)\n * - Frame-drop prevention (<2ms overhead)\n * - Adaptive patterns based on device performance\n * - Throttling to prevent excessive haptic triggers\n * \n * Browser Compatibility Note:\n * navigator.vibrate() returns boolean in some browsers (Chrome, Edge) and void in others (Firefox).\n * This implementation normalizes return values to boolean for consistency.\n * \n * @module components/mobile/HapticController\n * @category Mobile Controls\n * @korean 햅틱 컨트롤러\n */\n\n/**\n * Haptic intensity levels\n */\nexport type HapticIntensity = 'light' | 'medium' | 'strong' | 'disabled';\n\n/**\n * Device performance tier\n */\nexport type DevicePerformanceTier = 'high' | 'medium' | 'low';\n\n/**\n * Haptic pattern configuration\n */\ninterface HapticPattern {\n readonly vibration: number | number[];\n readonly maxFrameTime: number; // Maximum frame time impact in ms\n}\n\n/**\n * Optimized haptic patterns for different intensities\n * Designed to provide clear feedback without frame drops\n * \n * @korean 햅틱 패턴\n */\nconst HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [20], // Short, crisp tap\n maxFrameTime: 0.5, // Minimal impact\n },\n medium: {\n vibration: [40], // Medium pulse\n maxFrameTime: 1.0, // Small impact\n },\n strong: {\n vibration: [60], // Strong pulse\n maxFrameTime: 2.0, // Noticeable impact\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * Adaptive haptic patterns for low-end devices\n * Reduced intensities to prevent frame drops\n * \n * @korean 적응형 햅틱 패턴\n */\nconst ADAPTIVE_HAPTIC_PATTERNS: Record<HapticIntensity, HapticPattern> = {\n light: {\n vibration: [10], // Minimal vibration\n maxFrameTime: 0.2,\n },\n medium: {\n vibration: [20], // Reduced medium\n maxFrameTime: 0.5,\n },\n strong: {\n vibration: [30], // Reduced strong\n maxFrameTime: 1.0,\n },\n disabled: {\n vibration: [],\n maxFrameTime: 0,\n },\n} as const;\n\n/**\n * HapticController class\n * Manages haptic feedback with device-aware optimization\n * \n * @korean 햅틱컨트롤러\n */\nexport class HapticController {\n private static instance: HapticController | null = null;\n private isSupported: boolean = false;\n private isEnabled: boolean = true;\n private performanceTier: DevicePerformanceTier = 'high';\n /**\n * Last trigger timestamp for throttling.\n * Initialized to -Infinity to ensure the first trigger always succeeds.\n * Using -Infinity instead of 0 prevents throttling when performance.now() returns 0,\n * which can occur in test environments or immediately after page load.\n */\n private lastTriggerTime: number = -Infinity;\n private minTriggerInterval: number = 50; // Minimum 50ms between haptics\n\n /**\n * Private constructor for singleton pattern\n */\n private constructor() {\n this.isSupported = this.detectHapticSupport();\n this.performanceTier = this.detectPerformanceTier();\n \n if (this.performanceTier === 'low') {\n this.isEnabled = false;\n }\n }\n\n /**\n * Get singleton instance\n * \n * @returns HapticController instance\n * @korean 인스턴스가져오기\n */\n public static getInstance(): HapticController {\n this.instance ??= new HapticController();\n return this.instance;\n }\n\n /**\n * Detect if haptic feedback is supported\n * \n * @returns True if Vibration API is available\n * @korean 햅틱지원감지\n */\n private detectHapticSupport(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n return 'vibrate' in navigator;\n }\n\n /**\n * Detect device performance tier\n * Uses multiple heuristics to determine device capability\n * \n * @returns Performance tier (high, medium, low)\n * @korean 성능등급감지\n */\n private detectPerformanceTier(): DevicePerformanceTier {\n if (typeof navigator === 'undefined') {\n return 'medium';\n }\n\n const cores = navigator.hardwareConcurrency ?? 4;\n \n const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory ?? 4;\n \n const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);\n \n let score = 0;\n \n if (cores >= 8) score += 2;\n else if (cores >= 4) score += 1;\n \n if (memory >= 8) score += 2;\n else if (memory >= 4) score += 1;\n \n if (!isMobile) score += 1;\n \n if (score >= 4) return 'high';\n if (score >= 2) return 'medium';\n return 'low';\n }\n\n /**\n * Trigger haptic feedback with optimized patterns\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Light haptic for button tap\n * haptic.trigger('light');\n * \n * // Strong haptic for critical hit\n * haptic.trigger('strong');\n * ```\n * \n */\n public trigger(intensity: HapticIntensity): boolean {\n if (!this.isSupported || !this.isEnabled || intensity === 'disabled') {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n const patterns = this.performanceTier === 'low' \n ? ADAPTIVE_HAPTIC_PATTERNS \n : HAPTIC_PATTERNS;\n \n const pattern = patterns[intensity];\n\n try {\n const result = navigator.vibrate(pattern.vibration);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n * @example\n * ```typescript\n * const haptic = HapticController.getInstance();\n * \n * // Custom combo pattern\n * haptic.triggerCustom([30, 20, 30, 20, 50]);\n * ```\n * \n */\n public triggerCustom(pattern: number | number[]): boolean {\n if (!this.isSupported || !this.isEnabled) {\n return false;\n }\n\n const now = performance.now();\n if (now - this.lastTriggerTime < this.minTriggerInterval) {\n return false;\n }\n this.lastTriggerTime = now;\n\n try {\n let adaptedPattern = pattern;\n if (this.performanceTier === 'low') {\n if (Array.isArray(pattern)) {\n adaptedPattern = pattern.map(duration => Math.floor(duration * 0.5));\n } else {\n adaptedPattern = Math.floor(pattern * 0.5);\n }\n }\n\n const result = navigator.vibrate(adaptedPattern);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Custom haptic feedback failed:', error);\n return false;\n }\n }\n\n /**\n * Stop any ongoing haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n * \n */\n public stop(): boolean {\n if (!this.isSupported) {\n return false;\n }\n\n try {\n const result = navigator.vibrate(0);\n return result !== false; // Return true if not explicitly false\n } catch (error) {\n console.warn('Failed to stop haptic feedback:', error);\n return false;\n }\n }\n\n /**\n * Enable haptic feedback\n * \n * @korean 햅틱활성화\n */\n public enable(): void {\n this.isEnabled = true;\n }\n\n /**\n * Disable haptic feedback\n * \n * @korean 햅틱비활성화\n */\n public disable(): void {\n this.isEnabled = false;\n this.stop();\n }\n\n /**\n * Check if haptic is currently enabled\n * \n * @returns True if haptic is enabled\n * @korean 햅틱활성화상태\n */\n public isHapticEnabled(): boolean {\n return this.isSupported && this.isEnabled;\n }\n\n /**\n * Get current device performance tier\n * \n * @returns Performance tier\n * @korean 성능등급가져오기\n */\n public getPerformanceTier(): DevicePerformanceTier {\n return this.performanceTier;\n }\n\n /**\n * Set minimum interval between haptic triggers\n * \n * @param intervalMs - Minimum interval in milliseconds\n * @korean 최소간격설정\n */\n public setMinTriggerInterval(intervalMs: number): void {\n this.minTriggerInterval = Math.max(0, intervalMs);\n }\n}\n\n/**\n * Convenience function to trigger haptic feedback\n * Uses singleton HapticController instance\n * \n * @param intensity - Haptic intensity level\n * @returns True if haptic was triggered\n * @korean 햅틱실행\n * \n * @example\n * ```typescript\n * // Light haptic for UI interaction\n * triggerOptimizedHaptic('light');\n * \n * // Medium haptic for combat action\n * triggerOptimizedHaptic('medium');\n * \n * // Strong haptic for critical hit\n * triggerOptimizedHaptic('strong');\n * ```\n * \n */\nexport function triggerOptimizedHaptic(intensity: HapticIntensity): boolean {\n return HapticController.getInstance().trigger(intensity);\n}\n\n/**\n * Convenience function to trigger custom haptic pattern\n * \n * @param pattern - Custom vibration pattern\n * @returns True if haptic was triggered\n * @korean 커스텀햅틱실행\n * \n */\nexport function triggerCustomOptimizedHaptic(pattern: number | number[]): boolean {\n return HapticController.getInstance().triggerCustom(pattern);\n}\n\n/**\n * Convenience function to stop haptic feedback\n * \n * @returns True if haptic was stopped\n * @korean 햅틱중지\n */\nexport function stopOptimizedHaptic(): boolean {\n return HapticController.getInstance().stop();\n}\n\n/**\n * Combat-specific optimized haptic patterns\n * Pre-configured for common combat scenarios\n * \n * @korean 전투 햅틱 패턴\n */\nexport const OptimizedCombatHaptics = {\n /**\n * Standard attack hit feedback\n * @korean 일반 공격\n */\n attack: () => triggerOptimizedHaptic('medium'),\n\n /**\n * Block successful feedback\n * @korean 방어 성공\n */\n block: () => triggerOptimizedHaptic('light'),\n\n /**\n * Critical hit feedback with double pulse\n * @korean 크리티컬 히트\n */\n criticalHit: () => triggerCustomOptimizedHaptic([40, 20, 60]),\n\n /**\n * Vital point strike feedback\n * @korean 급소 타격\n */\n vitalPointStrike: () => triggerOptimizedHaptic('strong'),\n\n /**\n * Stance change feedback\n * @korean 자세 변경\n */\n stanceChange: () => triggerOptimizedHaptic('light'),\n\n /**\n * Combo counter increment\n * @korean 콤보 카운터\n */\n comboIncrement: () => triggerOptimizedHaptic('light'),\n\n /**\n * Player KO feedback with extended pattern\n * @korean 플레이어 KO\n */\n knockout: () => triggerCustomOptimizedHaptic([60, 30, 60, 30, 100]),\n\n /**\n * Error or invalid action feedback\n * @korean 오류 피드백\n */\n error: () => triggerCustomOptimizedHaptic([15, 10, 15]),\n} as const;\n"],"mappings":"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"}
|
|
@@ -197,7 +197,7 @@ var StanceWheelPure = ({ currentStance, onStanceChange, expanded, onToggle, disa
|
|
|
197
197
|
const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);
|
|
198
198
|
if (expanded) {
|
|
199
199
|
const wheelSize = 200;
|
|
200
|
-
const segmentAngle =
|
|
200
|
+
const segmentAngle = 45;
|
|
201
201
|
return /* @__PURE__ */ jsx("div", {
|
|
202
202
|
style: {
|
|
203
203
|
position: "absolute",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StanceWheelPure.js","names":[],"sources":["../../../../src/components/shared/mobile/StanceWheelPure.tsx"],"sourcesContent":["/**\n * StanceWheelPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Circular 8-segment stance selector for mobile touch controls\n * Provides visual and tactile stance switching interface\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 stance buttons\n * - Keyboard navigation (Tab, Enter, Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - aria-expanded state for wheel toggle\n * - role=\"radiogroup\" for stance selection\n * - 50x50px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/StanceWheelPure\n * @category Mobile Controls\n * @korean 자세 휠 (순수 DOM)\n */\n\nimport React, { useCallback, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../types/common\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport {\n handleKeyboardNav,\n getFocusStyle,\n announceToScreenReader,\n} from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\n\n/**\n * Props for StanceWheelPure component\n */\nexport interface StanceWheelPureProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Callback when stance changes */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Whether wheel is expanded */\n readonly expanded: boolean;\n /** Callback to toggle expansion */\n readonly onToggle: () => void;\n /** Whether wheel is disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 when collapsed, 100 when expanded for safe area) */\n readonly bottom?: number;\n /** Opacity of wheel (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Trigram symbols for each stance\n */\nconst TRIGRAM_SYMBOLS = [\n \"☰\",\n \"☱\",\n \"☲\",\n \"☳\",\n \"☴\",\n \"☵\",\n \"☶\",\n \"☷\",\n] as const;\n\n/**\n * Korean names for each stance\n */\nconst STANCE_KOREAN_NAMES = [\n \"건\", // Geon - Heaven\n \"태\", // Tae - Lake\n \"리\", // Li - Fire\n \"진\", // Jin - Thunder\n \"손\", // Son - Wind\n \"감\", // Gam - Water\n \"간\", // Gan - Mountain\n \"곤\", // Gon - Earth\n] as const;\n\n/**\n * Get color for a specific stance\n */\nconst getStanceColor = (stance: TrigramStance): number => {\n const stanceColors: Record<TrigramStance, number> = {\n geon: 0xffd700, // Gold - Heaven\n tae: 0x00ffff, // Cyan - Lake\n li: 0xff4444, // Red - Fire\n jin: 0xffaa00, // Orange - Thunder\n son: 0x88ff88, // Light Green - Wind\n gam: 0x0088ff, // Blue - Water\n gan: 0x8844ff, // Purple - Mountain\n gon: 0xaa6644, // Brown - Earth\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * StanceWheelPure Component\n *\n * Pure DOM circular stance selector with 8 segments for trigram stances\n * Features:\n * - Expandable/collapsible interface\n * - Visual stance indicator when collapsed (60x60px)\n * - 8 touch-optimized stance buttons when expanded (50x50px each)\n * - 200px wheel diameter with safe positioning\n * - Korean trigram symbols and names\n * - Color-coded by stance element\n * - Haptic feedback on selection\n * - 50x50px minimum touch targets\n *\n * Usage in Combat:\n * - Tap collapsed indicator to expand wheel\n * - Select from 8 trigram stances\n * - Current stance highlighted with gold accent\n * - Tap current stance to collapse wheel\n *\n * @example\n * ```tsx\n * <StanceWheelPure\n * currentStance={player.stance}\n * onStanceChange={(index) => handleStanceChange(index)}\n * expanded={wheelExpanded}\n * onToggle={() => setWheelExpanded(!wheelExpanded)}\n * disabled={isPaused}\n * />\n * ```\n *\n * @korean 자세휠순수\n */\nexport const StanceWheelPure: React.FC<StanceWheelPureProps> = ({\n currentStance,\n onStanceChange,\n expanded,\n onToggle,\n disabled = false,\n bottom,\n opacity = 0.8,\n}) => {\n const [hoveredStance, setHoveredStance] = useState<number | null>(null);\n const [focusedStance, setFocusedStance] = useState<number | null>(null);\n\n /**\n * Handle stance selection (touch or mouse)\n */\n const handleStanceSelect = useCallback(\n (e: React.TouchEvent | React.MouseEvent, stanceIndex: number) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n if (stanceIndex === currentStance) {\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\n const stanceName = STANCE_KOREAN_NAMES[stanceIndex];\n const stanceSymbol = TRIGRAM_SYMBOLS[stanceIndex];\n announceToScreenReader({\n message: createBilingualLabel(\n `자세 변경: ${stanceName} ${stanceSymbol}`,\n `Stance changed to ${TRIGRAM_STANCES_ORDER[stanceIndex]}`,\n ).label,\n politeness: \"polite\",\n });\n\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle wheel toggle (touch or mouse)\n */\n const handleToggle = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n onToggle();\n triggerHaptic(\"light\");\n\n const nextExpanded = !expanded;\n announceToScreenReader({\n message: createBilingualLabel(\n nextExpanded ? \"자세 휠 열림\" : \"자세 휠 닫힘\",\n nextExpanded ? \"Stance wheel opened\" : \"Stance wheel closed\",\n ).label,\n politeness: \"polite\",\n });\n },\n [disabled, onToggle, expanded],\n );\n\n /**\n * Handle keyboard navigation for stance buttons\n */\n const handleStanceKeyDown = useCallback(\n (stanceIndex: number) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n if (stanceIndex === currentStance) {\n onToggle();\n } else {\n onStanceChange(stanceIndex);\n }\n triggerHaptic(\"medium\");\n },\n onCancel: () => {\n onToggle();\n },\n onNavigate: (direction) => {\n let newIndex = stanceIndex;\n if (direction === \"left\" || direction === \"up\") {\n newIndex = (stanceIndex - 1 + 8) % 8;\n } else if (direction === \"right\" || direction === \"down\") {\n newIndex = (stanceIndex + 1) % 8;\n }\n setFocusedStance(newIndex);\n requestAnimationFrame(() => {\n const button = document.querySelector(\n `[data-testid=\"stance-button-pure-${newIndex}\"]`,\n ) as HTMLElement | null;\n button?.focus();\n });\n },\n });\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle keyboard navigation for toggle button\n */\n const handleToggleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n onToggle();\n triggerHaptic(\"light\");\n },\n });\n },\n [disabled, onToggle],\n );\n\n const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\n const currentStanceColor = getColorRGB(\n getStanceColor(TRIGRAM_STANCES_ORDER[currentStance]),\n );\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n if (expanded) {\n const wheelSize = 200;\n const segmentAngle = 360 / 8;\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-expanded\"\n role=\"radiogroup\"\n aria-label={\n createBilingualLabel(\n \"팔괘 자세 선택\",\n \"Eight Trigram Stance Selection\",\n ).label\n }\n aria-activedescendant={`stance-button-pure-${currentStance}`}\n >\n <div\n style={{\n width: `${wheelSize}px`,\n height: `${wheelSize}px`,\n position: \"relative\",\n }}\n >\n {/* Stance buttons arranged in circle */}\n {TRIGRAM_STANCES_ORDER.map((stance, index) => {\n const angle = index * segmentAngle;\n const radian = (angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * 80 + 100;\n const y = Math.sin(radian) * 80 + 100;\n const isActive = index === currentStance;\n const isHovered = index === hoveredStance;\n const stanceColor = getColorRGB(getStanceColor(stance));\n\n return (\n <button\n key={stance}\n onTouchStart={(e) => handleStanceSelect(e, index)}\n onTouchEnd={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setHoveredStance(null);\n }}\n onMouseDown={(e) => handleStanceSelect(e, index)}\n onMouseEnter={() => setHoveredStance(index)}\n onMouseLeave={() => setHoveredStance(null)}\n onKeyDown={handleStanceKeyDown(index)}\n onFocus={() => setFocusedStance(index)}\n onBlur={() => setFocusedStance(null)}\n style={{\n position: \"absolute\",\n left: `${x - 25}px`,\n top: `${y - 25}px`,\n width: \"50px\",\n height: \"50px\",\n borderRadius: \"50%\",\n background: isActive\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.95)`\n : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`,\n border: `3px solid ${isActive ? \"#fff\" : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 1)`}`,\n fontSize: \"24px\",\n color: isActive ? \"#000\" : \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n transform: isActive || isHovered ? \"scale(1.15)\" : \"scale(1)\",\n boxShadow: isActive\n ? `0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : isHovered\n ? `0 0 15px rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`\n : `0 4px 10px rgba(0, 0, 0, 0.5)`,\n ...getFocusStyle(focusedStance === index, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n aria-label={\n createBilingualLabel(\n `${STANCE_KOREAN_NAMES[index]} ${TRIGRAM_SYMBOLS[index]}`,\n `${stance} stance`,\n ).label\n }\n aria-checked={isActive}\n role=\"radio\"\n tabIndex={0}\n disabled={disabled}\n id={`stance-button-pure-${index}`}\n data-testid={`stance-button-pure-${index}`}\n >\n <div style={{ fontSize: \"20px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[index]}\n </div>\n <div style={{ fontSize: \"8px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[index]}\n </div>\n </button>\n );\n })}\n\n {/* Center label */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontSize: \"12px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n textAlign: \"center\",\n pointerEvents: \"none\",\n }}\n >\n 자세 선택\n <br />\n <span style={{ fontSize: \"10px\" }}>Stance</span>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-collapsed\"\n >\n <button\n onTouchStart={handleToggle}\n onMouseDown={handleToggle}\n onKeyDown={handleToggleKeyDown}\n onFocus={() => setFocusedStance(currentStance)}\n onBlur={() => setFocusedStance(null)}\n style={{\n width: \"60px\",\n height: \"60px\",\n borderRadius: \"50%\",\n background: `rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.9)`,\n border: `3px solid rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n fontSize: \"28px\",\n color: \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n ...getFocusStyle(focusedStance === currentStance, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n disabled={disabled}\n aria-label={\n createBilingualLabel(\n `현재 자세: ${STANCE_KOREAN_NAMES[currentStance]} ${TRIGRAM_SYMBOLS[currentStance]}. 자세 휠 열기`,\n `Current stance: ${TRIGRAM_STANCES_ORDER[currentStance]}. Open stance wheel`,\n ).label\n }\n aria-expanded={expanded}\n aria-haspopup=\"menu\"\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"stance-wheel-pure-toggle\"\n >\n <div style={{ fontSize: \"24px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[currentStance]}\n </div>\n <div style={{ fontSize: \"10px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[currentStance]}\n </div>\n </button>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StanceWheelPure.js","names":[],"sources":["../../../../src/components/shared/mobile/StanceWheelPure.tsx"],"sourcesContent":["/**\n * StanceWheelPure Component - Pure DOM version (no Three.js/drei dependency)\n *\n * Circular 8-segment stance selector for mobile touch controls\n * Provides visual and tactile stance switching interface\n *\n * This is a pure DOM version that renders OUTSIDE the Three.js Canvas.\n * It does NOT use Html from @react-three/drei, making it compatible with\n * rendering outside Canvas contexts.\n *\n * WCAG 2.1 Level AA Compliance:\n * - ARIA labels for all 8 stance buttons\n * - Keyboard navigation (Tab, Enter, Arrow keys)\n * - Visible focus indicators (2px cyan border)\n * - aria-expanded state for wheel toggle\n * - role=\"radiogroup\" for stance selection\n * - 50x50px touch targets (exceeds 44x44px minimum)\n *\n * @module components/mobile/StanceWheelPure\n * @category Mobile Controls\n * @korean 자세 휠 (순수 DOM)\n */\n\nimport React, { useCallback, useState } from \"react\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../types/common\";\nimport { triggerHaptic } from \"../../../utils/haptics\";\nimport { getColorRGB } from \"../../../utils/colorHelpers\";\nimport {\n handleKeyboardNav,\n getFocusStyle,\n announceToScreenReader,\n} from \"../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../types/AccessibilityTypes\";\n\n/**\n * Props for StanceWheelPure component\n */\nexport interface StanceWheelPureProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Callback when stance changes */\n readonly onStanceChange: (stanceIndex: number) => void;\n /** Whether wheel is expanded */\n readonly expanded: boolean;\n /** Callback to toggle expansion */\n readonly onToggle: () => void;\n /** Whether wheel is disabled */\n readonly disabled?: boolean;\n /** Position from bottom in pixels (default: 34 when collapsed, 100 when expanded for safe area) */\n readonly bottom?: number;\n /** Opacity of wheel (default: 0.8) */\n readonly opacity?: number;\n}\n\n/**\n * Trigram symbols for each stance\n */\nconst TRIGRAM_SYMBOLS = [\n \"☰\",\n \"☱\",\n \"☲\",\n \"☳\",\n \"☴\",\n \"☵\",\n \"☶\",\n \"☷\",\n] as const;\n\n/**\n * Korean names for each stance\n */\nconst STANCE_KOREAN_NAMES = [\n \"건\", // Geon - Heaven\n \"태\", // Tae - Lake\n \"리\", // Li - Fire\n \"진\", // Jin - Thunder\n \"손\", // Son - Wind\n \"감\", // Gam - Water\n \"간\", // Gan - Mountain\n \"곤\", // Gon - Earth\n] as const;\n\n/**\n * Get color for a specific stance\n */\nconst getStanceColor = (stance: TrigramStance): number => {\n const stanceColors: Record<TrigramStance, number> = {\n geon: 0xffd700, // Gold - Heaven\n tae: 0x00ffff, // Cyan - Lake\n li: 0xff4444, // Red - Fire\n jin: 0xffaa00, // Orange - Thunder\n son: 0x88ff88, // Light Green - Wind\n gam: 0x0088ff, // Blue - Water\n gan: 0x8844ff, // Purple - Mountain\n gon: 0xaa6644, // Brown - Earth\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * StanceWheelPure Component\n *\n * Pure DOM circular stance selector with 8 segments for trigram stances\n * Features:\n * - Expandable/collapsible interface\n * - Visual stance indicator when collapsed (60x60px)\n * - 8 touch-optimized stance buttons when expanded (50x50px each)\n * - 200px wheel diameter with safe positioning\n * - Korean trigram symbols and names\n * - Color-coded by stance element\n * - Haptic feedback on selection\n * - 50x50px minimum touch targets\n *\n * Usage in Combat:\n * - Tap collapsed indicator to expand wheel\n * - Select from 8 trigram stances\n * - Current stance highlighted with gold accent\n * - Tap current stance to collapse wheel\n *\n * @example\n * ```tsx\n * <StanceWheelPure\n * currentStance={player.stance}\n * onStanceChange={(index) => handleStanceChange(index)}\n * expanded={wheelExpanded}\n * onToggle={() => setWheelExpanded(!wheelExpanded)}\n * disabled={isPaused}\n * />\n * ```\n *\n * @korean 자세휠순수\n */\nexport const StanceWheelPure: React.FC<StanceWheelPureProps> = ({\n currentStance,\n onStanceChange,\n expanded,\n onToggle,\n disabled = false,\n bottom,\n opacity = 0.8,\n}) => {\n const [hoveredStance, setHoveredStance] = useState<number | null>(null);\n const [focusedStance, setFocusedStance] = useState<number | null>(null);\n\n /**\n * Handle stance selection (touch or mouse)\n */\n const handleStanceSelect = useCallback(\n (e: React.TouchEvent | React.MouseEvent, stanceIndex: number) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n if (stanceIndex === currentStance) {\n onToggle();\n triggerHaptic(\"light\");\n return;\n }\n\n onStanceChange(stanceIndex);\n triggerHaptic(\"medium\");\n\n const stanceName = STANCE_KOREAN_NAMES[stanceIndex];\n const stanceSymbol = TRIGRAM_SYMBOLS[stanceIndex];\n announceToScreenReader({\n message: createBilingualLabel(\n `자세 변경: ${stanceName} ${stanceSymbol}`,\n `Stance changed to ${TRIGRAM_STANCES_ORDER[stanceIndex]}`,\n ).label,\n politeness: \"polite\",\n });\n\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle wheel toggle (touch or mouse)\n */\n const handleToggle = useCallback(\n (e: React.TouchEvent | React.MouseEvent) => {\n if (disabled) return;\n e.preventDefault();\n e.stopPropagation();\n\n onToggle();\n triggerHaptic(\"light\");\n\n const nextExpanded = !expanded;\n announceToScreenReader({\n message: createBilingualLabel(\n nextExpanded ? \"자세 휠 열림\" : \"자세 휠 닫힘\",\n nextExpanded ? \"Stance wheel opened\" : \"Stance wheel closed\",\n ).label,\n politeness: \"polite\",\n });\n },\n [disabled, onToggle, expanded],\n );\n\n /**\n * Handle keyboard navigation for stance buttons\n */\n const handleStanceKeyDown = useCallback(\n (stanceIndex: number) => (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n if (stanceIndex === currentStance) {\n onToggle();\n } else {\n onStanceChange(stanceIndex);\n }\n triggerHaptic(\"medium\");\n },\n onCancel: () => {\n onToggle();\n },\n onNavigate: (direction) => {\n let newIndex = stanceIndex;\n if (direction === \"left\" || direction === \"up\") {\n newIndex = (stanceIndex - 1 + 8) % 8;\n } else if (direction === \"right\" || direction === \"down\") {\n newIndex = (stanceIndex + 1) % 8;\n }\n setFocusedStance(newIndex);\n requestAnimationFrame(() => {\n const button = document.querySelector(\n `[data-testid=\"stance-button-pure-${newIndex}\"]`,\n ) as HTMLElement | null;\n button?.focus();\n });\n },\n });\n },\n [disabled, currentStance, onStanceChange, onToggle],\n );\n\n /**\n * Handle keyboard navigation for toggle button\n */\n const handleToggleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n onToggle();\n triggerHaptic(\"light\");\n },\n });\n },\n [disabled, onToggle],\n );\n\n const dynamicBottom = bottom ?? (expanded ? 100 : 34);\n\n const currentStanceColor = getColorRGB(\n getStanceColor(TRIGRAM_STANCES_ORDER[currentStance]),\n );\n const goldColor = getColorRGB(KOREAN_COLORS.ACCENT_GOLD);\n const primaryColor = getColorRGB(KOREAN_COLORS.PRIMARY_CYAN);\n\n if (expanded) {\n const wheelSize = 200;\n const segmentAngle = 360 / 8;\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-expanded\"\n role=\"radiogroup\"\n aria-label={\n createBilingualLabel(\n \"팔괘 자세 선택\",\n \"Eight Trigram Stance Selection\",\n ).label\n }\n aria-activedescendant={`stance-button-pure-${currentStance}`}\n >\n <div\n style={{\n width: `${wheelSize}px`,\n height: `${wheelSize}px`,\n position: \"relative\",\n }}\n >\n {/* Stance buttons arranged in circle */}\n {TRIGRAM_STANCES_ORDER.map((stance, index) => {\n const angle = index * segmentAngle;\n const radian = (angle - 90) * (Math.PI / 180); // -90 to start from top\n const x = Math.cos(radian) * 80 + 100;\n const y = Math.sin(radian) * 80 + 100;\n const isActive = index === currentStance;\n const isHovered = index === hoveredStance;\n const stanceColor = getColorRGB(getStanceColor(stance));\n\n return (\n <button\n key={stance}\n onTouchStart={(e) => handleStanceSelect(e, index)}\n onTouchEnd={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setHoveredStance(null);\n }}\n onMouseDown={(e) => handleStanceSelect(e, index)}\n onMouseEnter={() => setHoveredStance(index)}\n onMouseLeave={() => setHoveredStance(null)}\n onKeyDown={handleStanceKeyDown(index)}\n onFocus={() => setFocusedStance(index)}\n onBlur={() => setFocusedStance(null)}\n style={{\n position: \"absolute\",\n left: `${x - 25}px`,\n top: `${y - 25}px`,\n width: \"50px\",\n height: \"50px\",\n borderRadius: \"50%\",\n background: isActive\n ? `rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.95)`\n : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`,\n border: `3px solid ${isActive ? \"#fff\" : `rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 1)`}`,\n fontSize: \"24px\",\n color: isActive ? \"#000\" : \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n transform: isActive || isHovered ? \"scale(1.15)\" : \"scale(1)\",\n boxShadow: isActive\n ? `0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`\n : isHovered\n ? `0 0 15px rgba(${stanceColor.r}, ${stanceColor.g}, ${stanceColor.b}, 0.8)`\n : `0 4px 10px rgba(0, 0, 0, 0.5)`,\n ...getFocusStyle(focusedStance === index, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 0 25px rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n aria-label={\n createBilingualLabel(\n `${STANCE_KOREAN_NAMES[index]} ${TRIGRAM_SYMBOLS[index]}`,\n `${stance} stance`,\n ).label\n }\n aria-checked={isActive}\n role=\"radio\"\n tabIndex={0}\n disabled={disabled}\n id={`stance-button-pure-${index}`}\n data-testid={`stance-button-pure-${index}`}\n >\n <div style={{ fontSize: \"20px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[index]}\n </div>\n <div style={{ fontSize: \"8px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[index]}\n </div>\n </button>\n );\n })}\n\n {/* Center label */}\n <div\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%, -50%)\",\n fontSize: \"12px\",\n color: `rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.9)`,\n textShadow: \"0 1px 3px rgba(0, 0, 0, 0.8)\",\n fontWeight: \"bold\",\n textAlign: \"center\",\n pointerEvents: \"none\",\n }}\n >\n 자세 선택\n <br />\n <span style={{ fontSize: \"10px\" }}>Stance</span>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div\n style={{\n position: \"absolute\", // Changed from fixed to position relative to container\n bottom: `${dynamicBottom}px`,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n opacity: disabled ? 0.3 : opacity,\n pointerEvents: disabled ? \"none\" : \"auto\",\n transition: \"all 0.3s ease\",\n zIndex: 1000,\n }}\n data-testid=\"stance-wheel-pure-collapsed\"\n >\n <button\n onTouchStart={handleToggle}\n onMouseDown={handleToggle}\n onKeyDown={handleToggleKeyDown}\n onFocus={() => setFocusedStance(currentStance)}\n onBlur={() => setFocusedStance(null)}\n style={{\n width: \"60px\",\n height: \"60px\",\n borderRadius: \"50%\",\n background: `rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.9)`,\n border: `3px solid rgba(${goldColor.r}, ${goldColor.g}, ${goldColor.b}, 0.9)`,\n fontSize: \"28px\",\n color: \"#fff\",\n fontWeight: \"bold\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n userSelect: \"none\",\n touchAction: \"none\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n ...getFocusStyle(focusedStance === currentStance, {\n boxShadow: `0 0 0 4px rgba(${primaryColor.r}, ${primaryColor.g}, ${primaryColor.b}, 0.5), 0 4px 15px rgba(0, 0, 0, 0.6), 0 0 20px rgba(${currentStanceColor.r}, ${currentStanceColor.g}, ${currentStanceColor.b}, 0.6)`,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineWidth: 3,\n }),\n }}\n disabled={disabled}\n aria-label={\n createBilingualLabel(\n `현재 자세: ${STANCE_KOREAN_NAMES[currentStance]} ${TRIGRAM_SYMBOLS[currentStance]}. 자세 휠 열기`,\n `Current stance: ${TRIGRAM_STANCES_ORDER[currentStance]}. Open stance wheel`,\n ).label\n }\n aria-expanded={expanded}\n aria-haspopup=\"menu\"\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n data-testid=\"stance-wheel-pure-toggle\"\n >\n <div style={{ fontSize: \"24px\", lineHeight: 1 }}>\n {TRIGRAM_SYMBOLS[currentStance]}\n </div>\n <div style={{ fontSize: \"10px\", marginTop: \"2px\" }}>\n {STANCE_KOREAN_NAMES[currentStance]}\n </div>\n </button>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -196,11 +196,13 @@ var VitalPointMarkers3D = ({ position = [
|
|
|
196
196
|
const visiblePoints = useMemo(() => {
|
|
197
197
|
let points = [...KOREAN_VITAL_POINTS];
|
|
198
198
|
if (severityFilter && severityFilter.length > 0) points = points.filter((vp) => severityFilter.includes(vp.severity));
|
|
199
|
-
if (regionFilter !== "all")
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
if (regionFilter !== "all") {
|
|
200
|
+
if (regionFilter === "arms") points = points.filter((vp) => vp.id.startsWith("arm_left_") || vp.id.startsWith("arm_right_"));
|
|
201
|
+
else if (regionFilter === "legs") points = points.filter((vp) => vp.id.startsWith("leg_left_") || vp.id.startsWith("leg_right_"));
|
|
202
|
+
else {
|
|
203
|
+
const prefix = `${regionFilter}_`;
|
|
204
|
+
points = points.filter((vp) => vp.id.startsWith(prefix));
|
|
205
|
+
}
|
|
204
206
|
}
|
|
205
207
|
if (searchQuery) {
|
|
206
208
|
const query = searchQuery.toLowerCase();
|