blacktrigram 0.7.98 → 0.7.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioManager.js +2 -4
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +8 -9
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
- package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
- package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js +2 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js +3 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +16 -15
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js +0 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js +7 -6
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js +3 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js +7 -6
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +34 -30
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/Key3D.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js +2 -2
- package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js +2 -2
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js +2 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +2 -4
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +10 -9
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js +14 -13
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
- package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointMarker3D.js +8 -7
- package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js +15 -13
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js +3 -2
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +1 -2
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js +3 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js +59 -53
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js +19 -18
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js +22 -18
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js +4 -5
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js +29 -24
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js +3 -2
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js +3 -2
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js +6 -5
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js +2 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js +25 -24
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js +1 -3
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js +2 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js +69 -68
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js +0 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +12 -3
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/systems/CombatSystem.js +6 -2
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js +0 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js +10 -21
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js +1 -3
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js +3 -4
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js +2 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js +3 -9
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js +8 -5
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/types/facial.js.map +1 -1
- package/lib/utils/accessibility.js +0 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/effectUtils.js +1 -3
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/inputSystem.js +0 -2
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js +6 -3
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/playerUtils.js +4 -2
- package/lib/utils/playerUtils.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StanceChangeIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/StanceChangeIndicator.tsx"],"sourcesContent":["/**\n * StanceChangeIndicator - Visual feedback for stance changes\n * Displays Korean and English stance names with trigram symbols\n * \n * @module components/shared/three/indicators/StanceChangeIndicator\n * @category Combat UI\n * @korean 자세변경표시기\n */\n\n \nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useState, useMemo, useRef } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { \n getTrigramElementColor, \n getTrigramKoreanName, \n getTrigramEnglishName,\n getTrigramSymbol\n} from \"./ElementalColorSystem\";\nimport { triggerStanceChangeHaptic } from \"./HapticFeedback\";\n\n/**\n * Props for StanceChangeIndicator component\n */\nexport interface StanceChangeIndicatorProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Previous stance index (0-7) for change detection */\n readonly previousStance: number;\n /** Mobile layout flag */\n readonly isMobile?: boolean;\n /** Display duration in milliseconds (default: 1000ms) */\n readonly duration?: number;\n /** Show transition progress bar (default: true) */\n readonly showProgress?: boolean;\n /** Transition duration in milliseconds for progress bar (default: 600ms) */\n readonly transitionDuration?: number;\n}\n\n/**\n * StanceChangeIndicator Component\n * \n * Displays a temporary overlay showing the current trigram stance\n * with Korean name, English name, and trigram symbol.\n * \n * Features:\n * - Fade in/out animation\n * - Korean cyberpunk styling\n * - Responsive mobile layout\n * - Trigram symbol display\n * - Glow effect for visual emphasis\n * \n * @example\n * ```tsx\n * <StanceChangeIndicator\n * currentStance={player.stance}\n * previousStance={previousStance}\n * isMobile={isMobile}\n * />\n * ```\n * \n * @korean 자세변경표시기\n */\nexport const StanceChangeIndicator: React.FC<StanceChangeIndicatorProps> = ({\n currentStance,\n previousStance,\n isMobile = false,\n duration = 1000,\n showProgress = true,\n transitionDuration = 600,\n}) => {\n const [showIndicator, setShowIndicator] = useState(false);\n const [progress, setProgress] = useState(0);\n const isMountedRef = useRef(true);\n const startTimeRef = useRef<number>(0);\n const animationFrameRef = useRef<number | undefined>(undefined);\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (animationFrameRef.current !== undefined) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = undefined;\n }\n\n if (currentStance !== previousStance) {\n setShowIndicator(true);\n setProgress(0);\n startTimeRef.current = 0;\n\n triggerStanceChangeHaptic();\n\n if (showProgress) {\n const animate = (timestamp: number) => {\n if (!isMountedRef.current) return;\n \n if (startTimeRef.current === 0) {\n startTimeRef.current = timestamp;\n }\n \n const elapsed = timestamp - startTimeRef.current;\n const newProgress = Math.min((elapsed / transitionDuration) * 100, 100);\n \n setProgress(newProgress);\n\n if (newProgress < 100) {\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n };\n\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n\n const timer = setTimeout(() => {\n if (isMountedRef.current) {\n setShowIndicator(false);\n }\n }, duration);\n\n return () => {\n clearTimeout(timer);\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n };\n }\n }, [currentStance, previousStance, duration, showProgress, transitionDuration]);\n\n const stance = useMemo(\n () => TRIGRAM_STANCES_ORDER[currentStance] ?? TrigramStance.GEON,\n [currentStance]\n );\n\n const elementalColor = useMemo(\n () => getTrigramElementColor(stance),\n [stance]\n );\n\n const koreanName = useMemo(\n () => getTrigramKoreanName(stance),\n [stance]\n );\n\n const englishName = useMemo(\n () => getTrigramEnglishName(stance),\n [stance]\n );\n\n const trigramSymbol = useMemo(\n () => getTrigramSymbol(stance),\n [stance]\n );\n\n const animationStyles = useMemo(() => (\n <style>\n {`\n @keyframes fadeInOut {\n 0% {\n opacity: 0;\n transform: translateY(-20px);\n }\n 10% {\n opacity: 1;\n transform: translateY(0);\n }\n 90% {\n opacity: 1;\n transform: translateY(0);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px);\n }\n }\n `}\n </style>\n ), []);\n\n if (!showIndicator) return null;\n\n const fontSize = isMobile ? 24 : 36;\n const subFontSize = isMobile ? 14 : 18;\n const top = isMobile ? \"30%\" : \"20%\";\n\n const primaryColorHex = hexToRgbaString(elementalColor, 1);\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"stance-change-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Stance changed to ${englishName}`}\n style={{\n position: \"absolute\",\n top,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n textAlign: \"center\",\n animation: \"fadeInOut 1s\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n >\n {/* Main stance display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: primaryColorHex,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n textShadow: `0 0 20px ${hexToRgbaString(elementalColor, 0.8)}, \n 0 0 40px ${hexToRgbaString(elementalColor, 0.5)}`,\n marginBottom: \"8px\",\n }}\n >\n {koreanName} {trigramSymbol}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6)}`,\n marginBottom: showProgress ? \"12px\" : \"0\",\n }}\n >\n {englishName}\n </div>\n\n {/* Transition progress bar (600ms) */}\n {showProgress && (\n <div\n data-testid=\"stance-transition-progress\"\n style={{\n width: \"200px\",\n margin: \"0 auto\",\n padding: \"8px 0\",\n }}\n >\n {/* Progress label */}\n <div\n style={{\n fontSize: \"11px\",\n color: \"rgba(255, 255, 255, 0.7)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n letterSpacing: \"1px\",\n }}\n >\n 팔괘전환 | Transition\n </div>\n \n {/* Progress bar container */}\n <div\n style={{\n width: \"100%\",\n height: \"6px\",\n backgroundColor: \"rgba(0, 0, 0, 0.6)\",\n borderRadius: \"3px\",\n overflow: \"hidden\",\n border: `1px solid ${hexToRgbaString(elementalColor, 0.3)}`,\n }}\n >\n {/* Progress bar fill */}\n <div\n style={{\n width: `${progress}%`,\n height: \"100%\",\n backgroundColor: primaryColorHex,\n transition: \"width 0.05s linear\",\n boxShadow: `0 0 8px ${primaryColorHex}`,\n }}\n />\n </div>\n \n {/* Time remaining */}\n <div\n style={{\n fontSize: \"10px\",\n color: \"rgba(255, 255, 255, 0.5)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginTop: \"4px\",\n }}\n >\n {Math.max(0, Math.ceil(transitionDuration * (1 - progress / 100)))}ms\n </div>\n </div>\n )}\n\n {/* CSS Animation - Memoized to prevent redefinition */}\n {animationStyles}\n </div>\n </Html>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StanceChangeIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/StanceChangeIndicator.tsx"],"sourcesContent":["/**\n * StanceChangeIndicator - Visual feedback for stance changes\n * Displays Korean and English stance names with trigram symbols\n * \n * @module components/shared/three/indicators/StanceChangeIndicator\n * @category Combat UI\n * @korean 자세변경표시기\n */\n\n \nimport { Html } from \"@react-three/drei\";\nimport React, { useEffect, useState, useMemo, useRef } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { \n getTrigramElementColor, \n getTrigramKoreanName, \n getTrigramEnglishName,\n getTrigramSymbol\n} from \"./ElementalColorSystem\";\nimport { triggerStanceChangeHaptic } from \"./HapticFeedback\";\n\n/**\n * Props for StanceChangeIndicator component\n */\nexport interface StanceChangeIndicatorProps {\n /** Current stance index (0-7) */\n readonly currentStance: number;\n /** Previous stance index (0-7) for change detection */\n readonly previousStance: number;\n /** Mobile layout flag */\n readonly isMobile?: boolean;\n /** Display duration in milliseconds (default: 1000ms) */\n readonly duration?: number;\n /** Show transition progress bar (default: true) */\n readonly showProgress?: boolean;\n /** Transition duration in milliseconds for progress bar (default: 600ms) */\n readonly transitionDuration?: number;\n}\n\n/**\n * StanceChangeIndicator Component\n * \n * Displays a temporary overlay showing the current trigram stance\n * with Korean name, English name, and trigram symbol.\n * \n * Features:\n * - Fade in/out animation\n * - Korean cyberpunk styling\n * - Responsive mobile layout\n * - Trigram symbol display\n * - Glow effect for visual emphasis\n * \n * @example\n * ```tsx\n * <StanceChangeIndicator\n * currentStance={player.stance}\n * previousStance={previousStance}\n * isMobile={isMobile}\n * />\n * ```\n * \n * @korean 자세변경표시기\n */\nexport const StanceChangeIndicator: React.FC<StanceChangeIndicatorProps> = ({\n currentStance,\n previousStance,\n isMobile = false,\n duration = 1000,\n showProgress = true,\n transitionDuration = 600,\n}) => {\n const [showIndicator, setShowIndicator] = useState(false);\n const [progress, setProgress] = useState(0);\n const isMountedRef = useRef(true);\n const startTimeRef = useRef<number>(0);\n const animationFrameRef = useRef<number | undefined>(undefined);\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (animationFrameRef.current !== undefined) {\n cancelAnimationFrame(animationFrameRef.current);\n animationFrameRef.current = undefined;\n }\n\n if (currentStance !== previousStance) {\n setShowIndicator(true);\n setProgress(0);\n startTimeRef.current = 0;\n\n triggerStanceChangeHaptic();\n\n if (showProgress) {\n const animate = (timestamp: number) => {\n if (!isMountedRef.current) return;\n \n if (startTimeRef.current === 0) {\n startTimeRef.current = timestamp;\n }\n \n const elapsed = timestamp - startTimeRef.current;\n const newProgress = Math.min((elapsed / transitionDuration) * 100, 100);\n \n setProgress(newProgress);\n\n if (newProgress < 100) {\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n };\n\n animationFrameRef.current = requestAnimationFrame(animate);\n }\n\n const timer = setTimeout(() => {\n if (isMountedRef.current) {\n setShowIndicator(false);\n }\n }, duration);\n\n return () => {\n clearTimeout(timer);\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n };\n }\n }, [currentStance, previousStance, duration, showProgress, transitionDuration]);\n\n const stance = useMemo(\n () => TRIGRAM_STANCES_ORDER[currentStance] ?? TrigramStance.GEON,\n [currentStance]\n );\n\n const elementalColor = useMemo(\n () => getTrigramElementColor(stance),\n [stance]\n );\n\n const koreanName = useMemo(\n () => getTrigramKoreanName(stance),\n [stance]\n );\n\n const englishName = useMemo(\n () => getTrigramEnglishName(stance),\n [stance]\n );\n\n const trigramSymbol = useMemo(\n () => getTrigramSymbol(stance),\n [stance]\n );\n\n const animationStyles = useMemo(() => (\n <style>\n {`\n @keyframes fadeInOut {\n 0% {\n opacity: 0;\n transform: translateY(-20px);\n }\n 10% {\n opacity: 1;\n transform: translateY(0);\n }\n 90% {\n opacity: 1;\n transform: translateY(0);\n }\n 100% {\n opacity: 0;\n transform: translateY(-10px);\n }\n }\n `}\n </style>\n ), []);\n\n if (!showIndicator) return null;\n\n const fontSize = isMobile ? 24 : 36;\n const subFontSize = isMobile ? 14 : 18;\n const top = isMobile ? \"30%\" : \"20%\";\n\n const primaryColorHex = hexToRgbaString(elementalColor, 1);\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"stance-change-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Stance changed to ${englishName}`}\n style={{\n position: \"absolute\",\n top,\n left: \"50%\",\n transform: \"translateX(-50%)\",\n textAlign: \"center\",\n animation: \"fadeInOut 1s\",\n pointerEvents: \"none\",\n zIndex: 1000,\n }}\n >\n {/* Main stance display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: primaryColorHex,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n textShadow: `0 0 20px ${hexToRgbaString(elementalColor, 0.8)}, \n 0 0 40px ${hexToRgbaString(elementalColor, 0.5)}`,\n marginBottom: \"8px\",\n }}\n >\n {koreanName} {trigramSymbol}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n textShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6)}`,\n marginBottom: showProgress ? \"12px\" : \"0\",\n }}\n >\n {englishName}\n </div>\n\n {/* Transition progress bar (600ms) */}\n {showProgress && (\n <div\n data-testid=\"stance-transition-progress\"\n style={{\n width: \"200px\",\n margin: \"0 auto\",\n padding: \"8px 0\",\n }}\n >\n {/* Progress label */}\n <div\n style={{\n fontSize: \"11px\",\n color: \"rgba(255, 255, 255, 0.7)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n letterSpacing: \"1px\",\n }}\n >\n 팔괘전환 | Transition\n </div>\n \n {/* Progress bar container */}\n <div\n style={{\n width: \"100%\",\n height: \"6px\",\n backgroundColor: \"rgba(0, 0, 0, 0.6)\",\n borderRadius: \"3px\",\n overflow: \"hidden\",\n border: `1px solid ${hexToRgbaString(elementalColor, 0.3)}`,\n }}\n >\n {/* Progress bar fill */}\n <div\n style={{\n width: `${progress}%`,\n height: \"100%\",\n backgroundColor: primaryColorHex,\n transition: \"width 0.05s linear\",\n boxShadow: `0 0 8px ${primaryColorHex}`,\n }}\n />\n </div>\n \n {/* Time remaining */}\n <div\n style={{\n fontSize: \"10px\",\n color: \"rgba(255, 255, 255, 0.5)\",\n fontFamily: FONT_FAMILY.KOREAN,\n marginTop: \"4px\",\n }}\n >\n {Math.max(0, Math.ceil(transitionDuration * (1 - progress / 100)))}ms\n </div>\n </div>\n )}\n\n {/* CSS Animation - Memoized to prevent redefinition */}\n {animationStyles}\n </div>\n </Html>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Player3DWithTransitions.js","names":[],"sources":["../../../../../src/components/shared/three/models/Player3DWithTransitions.tsx"],"sourcesContent":["/**\n * Enhanced Player3D component with stance transition animations\n *\n * Demonstrates integration of stance change visual effects:\n * - StanceSymbol3D for floating trigram symbol\n * - StanceTransitionEffect for smooth transitions\n *\n * This wrapper can be used to enhance SkeletalPlayer3D with automatic\n * stance change detection and visual effects.\n *\n * @module components/three/Player3DWithTransitions\n * @category 3D Components\n * @korean 자세전환플레이어3D\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport StanceSymbol3D from \"../effects/StanceSymbol3D\";\nimport StanceTransitionEffect from \"../effects/StanceTransitionEffect\";\nimport { SkeletalPlayer3D } from \"./SkeletalPlayer3D\";\n\n/**\n * Props for Player3DWithTransitions component\n */\nexport interface Player3DWithTransitionsProps extends Player3DUnifiedProps {\n /** Specific attack animation name (for attack state) */\n readonly attackAnimation?: string;\n /** Enable stance transition effects (default: true) */\n readonly enableTransitionEffects?: boolean;\n /** Enable floating stance symbol (default: true) */\n readonly enableStanceSymbol?: boolean;\n /** Enable stance change audio (default: true) */\n readonly enableStanceAudio?: boolean;\n /** Transition duration in seconds (default: 0.5) */\n readonly transitionDuration?: number;\n /** Callback when stance transition starts */\n readonly onStanceTransitionStart?: (\n fromStance: TrigramStance,\n toStance: TrigramStance\n ) => void;\n /** Callback when stance transition completes */\n readonly onStanceTransitionComplete?: (stance: TrigramStance) => void;\n}\n\n/**\n * Audio asset IDs for stance transitions\n */\nconst AUDIO_ASSETS = {\n STANCE_CHANGE: \"stance_change\",\n} as const;\n\n/**\n * Player3DWithTransitions Component\n *\n * Enhanced player component with automatic stance change detection and visual effects.\n * Wraps SkeletalPlayer3D and adds:\n * - Floating trigram symbol\n * - Smooth transition effects\n * - Audio synchronization\n *\n * Performance optimized:\n * - Effects can be individually disabled for mobile\n * - Uses stance change detection to minimize updates\n * - Reuses components efficiently\n *\n * @example\n * ```tsx\n * <Player3DWithTransitions\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={currentStance}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * pain={20}\n * balance=\"READY\"\n * consciousness={100}\n * bloodLoss={0}\n * currentAnimation=\"idle\"\n * isMobile={false}\n * enableTransitionEffects={true}\n * enableStanceSymbol={true}\n * onStanceTransitionComplete={(stance) => console.log('Transitioned to:', stance)}\n * />\n * ```\n */\nexport const Player3DWithTransitions: React.FC<\n Player3DWithTransitionsProps\n> = ({\n stance,\n ki,\n isMobile = false,\n attackAnimation,\n enableTransitionEffects = true,\n enableStanceSymbol = true,\n enableStanceAudio = true,\n transitionDuration = 0.5,\n onStanceTransitionStart,\n onStanceTransitionComplete,\n ...playerProps\n}) => {\n const audio = useAudio();\n const prevStanceRef = useRef<TrigramStance>(stance);\n const [isTransitioning, setIsTransitioning] = useState(false);\n const [fromStance, setFromStance] = useState<TrigramStance>(stance);\n\n\n useEffect(() => {\n const previousStance = prevStanceRef.current;\n\n if (previousStance !== stance) {\n prevStanceRef.current = stance;\n\n setIsTransitioning(true);\n setFromStance(previousStance);\n onStanceTransitionStart?.(previousStance, stance);\n\n if (enableStanceAudio) {\n audio.playSFX(AUDIO_ASSETS.STANCE_CHANGE);\n }\n }\n }, [stance, audio, enableStanceAudio, onStanceTransitionStart]);\n\n const handleTransitionComplete = useCallback(() => {\n setIsTransitioning(false);\n onStanceTransitionComplete?.(stance);\n }, [stance, onStanceTransitionComplete]);\n\n return (\n <group name=\"player3d-with-transitions\">\n {/* Base player model */}\n <SkeletalPlayer3D\n stance={stance}\n ki={ki}\n isMobile={isMobile}\n attackAnimation={attackAnimation}\n {...playerProps}\n />\n\n {/* Floating stance symbol */}\n {enableStanceSymbol && (\n <StanceSymbol3D\n stance={stance}\n heightOffset={2.5}\n animated={true}\n scale={isMobile ? 0.8 : 1.0} // Smaller on mobile\n showName={!isMobile} // Hide Korean name on mobile for clarity\n />\n )}\n\n {/* Stance transition effect */}\n {enableTransitionEffects && isTransitioning && (\n <StanceTransitionEffect\n fromStance={fromStance}\n toStance={stance}\n onTransitionComplete={handleTransitionComplete}\n duration={transitionDuration}\n showNameOverlay={!isMobile} // Hide overlay on mobile\n />\n )}\n </group>\n );\n};\n\nexport default Player3DWithTransitions;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAM,eAAe,EACnB,eAAe,gBACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAa,2BAER,EACH,QACA,IACA,WAAW,OACX,iBACA,0BAA0B,MAC1B,qBAAqB,MACrB,oBAAoB,MACpB,qBAAqB,IACrB,yBACA,4BACA,GAAG,kBACC;CACJ,MAAM,QAAQ,SAAS;CACvB,MAAM,gBAAgB,OAAsB,MAAM;CAClD,MAAM,CAAC,iBAAiB,sBAAsB,SAAS,KAAK;CAC5D,MAAM,CAAC,YAAY,iBAAiB,SAAwB,MAAM;CAGlE,gBAAgB;EACd,MAAM,iBAAiB,cAAc;EAErC,IAAI,mBAAmB,QAAQ;GAC7B,cAAc,UAAU;GAExB,mBAAmB,IAAI;GACvB,cAAc,cAAc;GAC5B,0BAA0B,gBAAgB,MAAM;GAEhD,IAAI,mBACF,MAAM,QAAQ,aAAa,aAAa;EAE5C;CACF,GAAG;EAAC;EAAQ;EAAO;EAAmB;CAAuB,CAAC;CAE9D,MAAM,2BAA2B,kBAAkB;EACjD,mBAAmB,KAAK;EACxB,6BAA6B,MAAM;CACrC,GAAG,CAAC,QAAQ,0BAA0B,CAAC;CAEvC,OACE,qBAAC,SAAD;EAAO,MAAK;
|
|
1
|
+
{"version":3,"file":"Player3DWithTransitions.js","names":[],"sources":["../../../../../src/components/shared/three/models/Player3DWithTransitions.tsx"],"sourcesContent":["/**\n * Enhanced Player3D component with stance transition animations\n *\n * Demonstrates integration of stance change visual effects:\n * - StanceSymbol3D for floating trigram symbol\n * - StanceTransitionEffect for smooth transitions\n *\n * This wrapper can be used to enhance SkeletalPlayer3D with automatic\n * stance change detection and visual effects.\n *\n * @module components/three/Player3DWithTransitions\n * @category 3D Components\n * @korean 자세전환플레이어3D\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useAudio } from \"../../../../audio/AudioProvider\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport StanceSymbol3D from \"../effects/StanceSymbol3D\";\nimport StanceTransitionEffect from \"../effects/StanceTransitionEffect\";\nimport { SkeletalPlayer3D } from \"./SkeletalPlayer3D\";\n\n/**\n * Props for Player3DWithTransitions component\n */\nexport interface Player3DWithTransitionsProps extends Player3DUnifiedProps {\n /** Specific attack animation name (for attack state) */\n readonly attackAnimation?: string;\n /** Enable stance transition effects (default: true) */\n readonly enableTransitionEffects?: boolean;\n /** Enable floating stance symbol (default: true) */\n readonly enableStanceSymbol?: boolean;\n /** Enable stance change audio (default: true) */\n readonly enableStanceAudio?: boolean;\n /** Transition duration in seconds (default: 0.5) */\n readonly transitionDuration?: number;\n /** Callback when stance transition starts */\n readonly onStanceTransitionStart?: (\n fromStance: TrigramStance,\n toStance: TrigramStance\n ) => void;\n /** Callback when stance transition completes */\n readonly onStanceTransitionComplete?: (stance: TrigramStance) => void;\n}\n\n/**\n * Audio asset IDs for stance transitions\n */\nconst AUDIO_ASSETS = {\n STANCE_CHANGE: \"stance_change\",\n} as const;\n\n/**\n * Player3DWithTransitions Component\n *\n * Enhanced player component with automatic stance change detection and visual effects.\n * Wraps SkeletalPlayer3D and adds:\n * - Floating trigram symbol\n * - Smooth transition effects\n * - Audio synchronization\n *\n * Performance optimized:\n * - Effects can be individually disabled for mobile\n * - Uses stance change detection to minimize updates\n * - Reuses components efficiently\n *\n * @example\n * ```tsx\n * <Player3DWithTransitions\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={currentStance}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * pain={20}\n * balance=\"READY\"\n * consciousness={100}\n * bloodLoss={0}\n * currentAnimation=\"idle\"\n * isMobile={false}\n * enableTransitionEffects={true}\n * enableStanceSymbol={true}\n * onStanceTransitionComplete={(stance) => console.log('Transitioned to:', stance)}\n * />\n * ```\n */\nexport const Player3DWithTransitions: React.FC<\n Player3DWithTransitionsProps\n> = ({\n stance,\n ki,\n isMobile = false,\n attackAnimation,\n enableTransitionEffects = true,\n enableStanceSymbol = true,\n enableStanceAudio = true,\n transitionDuration = 0.5,\n onStanceTransitionStart,\n onStanceTransitionComplete,\n ...playerProps\n}) => {\n const audio = useAudio();\n const prevStanceRef = useRef<TrigramStance>(stance);\n const [isTransitioning, setIsTransitioning] = useState(false);\n const [fromStance, setFromStance] = useState<TrigramStance>(stance);\n\n\n useEffect(() => {\n const previousStance = prevStanceRef.current;\n\n if (previousStance !== stance) {\n prevStanceRef.current = stance;\n\n setIsTransitioning(true);\n setFromStance(previousStance);\n onStanceTransitionStart?.(previousStance, stance);\n\n if (enableStanceAudio) {\n audio.playSFX(AUDIO_ASSETS.STANCE_CHANGE);\n }\n }\n }, [stance, audio, enableStanceAudio, onStanceTransitionStart]);\n\n const handleTransitionComplete = useCallback(() => {\n setIsTransitioning(false);\n onStanceTransitionComplete?.(stance);\n }, [stance, onStanceTransitionComplete]);\n\n return (\n <group name=\"player3d-with-transitions\">\n {/* Base player model */}\n <SkeletalPlayer3D\n stance={stance}\n ki={ki}\n isMobile={isMobile}\n attackAnimation={attackAnimation}\n {...playerProps}\n />\n\n {/* Floating stance symbol */}\n {enableStanceSymbol && (\n <StanceSymbol3D\n stance={stance}\n heightOffset={2.5}\n animated={true}\n scale={isMobile ? 0.8 : 1.0} // Smaller on mobile\n showName={!isMobile} // Hide Korean name on mobile for clarity\n />\n )}\n\n {/* Stance transition effect */}\n {enableTransitionEffects && isTransitioning && (\n <StanceTransitionEffect\n fromStance={fromStance}\n toStance={stance}\n onTransitionComplete={handleTransitionComplete}\n duration={transitionDuration}\n showNameOverlay={!isMobile} // Hide overlay on mobile\n />\n )}\n </group>\n );\n};\n\nexport default Player3DWithTransitions;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiDA,IAAM,eAAe,EACnB,eAAe,gBACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,IAAa,2BAER,EACH,QACA,IACA,WAAW,OACX,iBACA,0BAA0B,MAC1B,qBAAqB,MACrB,oBAAoB,MACpB,qBAAqB,IACrB,yBACA,4BACA,GAAG,kBACC;CACJ,MAAM,QAAQ,SAAS;CACvB,MAAM,gBAAgB,OAAsB,MAAM;CAClD,MAAM,CAAC,iBAAiB,sBAAsB,SAAS,KAAK;CAC5D,MAAM,CAAC,YAAY,iBAAiB,SAAwB,MAAM;CAGlE,gBAAgB;EACd,MAAM,iBAAiB,cAAc;EAErC,IAAI,mBAAmB,QAAQ;GAC7B,cAAc,UAAU;GAExB,mBAAmB,IAAI;GACvB,cAAc,cAAc;GAC5B,0BAA0B,gBAAgB,MAAM;GAEhD,IAAI,mBACF,MAAM,QAAQ,aAAa,aAAa;EAE5C;CACF,GAAG;EAAC;EAAQ;EAAO;EAAmB;CAAuB,CAAC;CAE9D,MAAM,2BAA2B,kBAAkB;EACjD,mBAAmB,KAAK;EACxB,6BAA6B,MAAM;CACrC,GAAG,CAAC,QAAQ,0BAA0B,CAAC;CAEvC,OACE,qBAAC,SAAD;EAAO,MAAK;EAAZ,UAAA;GAEE,oBAAC,kBAAD;IACU;IACJ;IACM;IACO;IACjB,GAAI;GACL,CAAA;GAGA,sBACC,oBAAC,gBAAD;IACU;IACR,cAAc;IACd,UAAU;IACV,OAAO,WAAW,KAAM;IACxB,UAAU,CAAC;GACZ,CAAA;GAIF,2BAA2B,mBAC1B,oBAAC,wBAAD;IACc;IACZ,UAAU;IACV,sBAAsB;IACtB,UAAU;IACV,iBAAiB,CAAC;GACnB,CAAA;EAEE;;AAEX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SkeletalPlayer3D.js","names":[],"sources":["../../../../../src/components/shared/three/models/SkeletalPlayer3D.tsx"],"sourcesContent":["/**\n * SkeletalPlayer3D component with articulated body model\n *\n * Implements full skeletal rigging system for realistic fighter animations\n * with independent limb movement, elbow/knee joints, and attack animations.\n *\n * @module components/three/SkeletalPlayer3D\n * @category 3D Components\n * @korean 골격플레이어3D컴포넌트\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport { useBalanceAnimations } from \"../../../../hooks/useBalanceAnimations\";\nimport { useHandPoseTransitions } from \"../../../../hooks/useHandPoseTransitions\";\nimport { useMuscleActivation } from \"../../../../hooks/useMuscleActivation\";\nimport { useSkeletalAnimation } from \"../../../../hooks/useSkeletalAnimation\";\nimport {\n createDefaultFacialDamage,\n createScaledHumanoidRig,\n MAX_VISUAL_FRAME_DELTA_SECONDS,\n getExpressionFromCombatState,\n getHeadAngleRadians,\n lockFacing,\n unlockFacing,\n updateFacingTowardOpponent,\n} from \"../../../../systems/animation\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { FacialExpression } from \"../../../../types/facial\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport type { SkeletalRig } from \"../../../../types/skeletal\";\nimport { toHexColor } from \"../../../../utils/colorHelpers\";\nimport { getArchetypeSkinTone } from \"../../../../utils/colorUtils\";\nimport BoneRenderer from \"../anatomy/BoneRenderer\";\nimport PlayerStateIndicators from \"../effects/PlayerStateIndicators\";\n\n/**\n * Get stance-specific color from Korean theming\n *\n * @param stance - Current trigram stance\n * @returns Hex color number\n * @korean 자세색상가져오기\n */\nconst getStanceColor = (stance: string): number => {\n const stanceColors: Record<string, number> = {\n geon: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY,\n tae: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY,\n li: KOREAN_COLORS.TRIGRAM_LI_PRIMARY,\n jin: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY,\n son: KOREAN_COLORS.TRIGRAM_SON_PRIMARY,\n gam: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY,\n gan: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY,\n gon: KOREAN_COLORS.TRIGRAM_GON_PRIMARY,\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * Get trigram symbol for stance\n *\n * @param stance - Current trigram stance\n * @returns Unicode trigram symbol\n * @korean 팔괘기호가져오기\n */\nconst getTrigramSymbol = (stance: string): string => {\n const symbols: Record<string, string> = {\n geon: \"☰\",\n tae: \"☱\",\n li: \"☲\",\n jin: \"☳\",\n son: \"☴\",\n gam: \"☵\",\n gan: \"☶\",\n gon: \"☷\",\n };\n return symbols[stance] ?? \"☰\";\n};\n\n/**\n * SkeletalPlayer3D Component\n *\n * Complete skeletal player with 28-bone rig and realistic animations.\n * Supports all Korean martial arts attack animations (jab, cross, kicks, block).\n *\n * @example\n * ```tsx\n * <SkeletalPlayer3D\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={TrigramStance.GEON}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * currentAnimation=\"attack\"\n * attackAnimation=\"jab\"\n * showDetails={true}\n * />\n * ```\n *\n * @korean 골격플레이어3D컴포넌트\n */\nexport const SkeletalPlayer3D: React.FC<\n Player3DUnifiedProps & {\n readonly attackAnimation?: string;\n readonly showSkeleton?: boolean;\n }\n> = ({\n playerId,\n archetype,\n stance,\n position,\n rotation,\n health,\n maxHealth,\n stamina,\n ki,\n pain,\n balance,\n consciousness,\n bloodLoss,\n isBlocking,\n isStunned = false,\n isCountering = false,\n currentAnimation,\n isMobile,\n name,\n scale = 1,\n showDetails = true,\n facing = \"right\",\n showStanceIndicator = true,\n onAnimationComplete,\n attackAnimation,\n showSkeleton = false,\n facialExpression,\n facialDamage,\n enableFacialExpressions = false,\n enableEyeTracking = true,\n opponentPosition,\n bodyFacing,\n onBodyFacingUpdate,\n laterality, // Stance laterality (left/right foot forward)\n}) => {\n const effectiveLaterality = laterality ?? \"right\";\n const physicalAttributes = useMemo(\n () => getArchetypePhysicalAttributes(archetype),\n [archetype],\n );\n\n const rig = useMemo<SkeletalRig>(\n () => createScaledHumanoidRig(physicalAttributes),\n [physicalAttributes],\n );\n\n\n const { updateRigAnimation, diagonalRotationY } = useSkeletalAnimation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stance,\n laterality: effectiveLaterality, // Pass laterality for animation mirroring\n onAnimationComplete,\n });\n\n const { leftHandState, rightHandState, updateHandAnimations } =\n useHandPoseTransitions({\n currentAnimation,\n attackAnimation,\n isBlocking,\n });\n\n\n const { swayPosition, helplessRotation, updateBalanceAnimations } =\n useBalanceAnimations({\n balance,\n });\n\n const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stamina,\n });\n\n const skinTone = useMemo(() => getArchetypeSkinTone(archetype), [archetype]);\n\n const bodyColor = useMemo(() => {\n if (isStunned) return KOREAN_COLORS.WARNING_YELLOW;\n if (health / maxHealth < 0.3) return KOREAN_COLORS.ACCENT_RED;\n if (ki / 100 > 0.8) return KOREAN_COLORS.PRIMARY_CYAN;\n return skinTone; // Use archetype skin tone instead of primary color\n }, [isStunned, health, maxHealth, ki, skinTone]);\n\n const stanceColor = useMemo(() => getStanceColor(stance), [stance]);\n const trigramSymbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n\n const [justHit, setJustHit] = useState(false);\n const [justLanded, setJustLanded] = useState(false);\n const lastHealthRef = useRef(health);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (health < lastHealthRef.current) {\n setJustHit(true);\n timeoutId = setTimeout(() => setJustHit(false), 1000); // Clear after 1 second\n }\n\n lastHealthRef.current = health;\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [health]);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (currentAnimation === \"attack\") {\n setJustLanded(true);\n timeoutId = setTimeout(() => setJustLanded(false), 500); // Clear after 0.5 seconds\n }\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [currentAnimation]);\n\n const calculatedExpression = useMemo(() => {\n if (!enableFacialExpressions) {\n return FacialExpression.NEUTRAL;\n }\n\n if (facialExpression) {\n return facialExpression;\n }\n\n return getExpressionFromCombatState(\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n );\n }, [\n enableFacialExpressions,\n facialExpression,\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n ]);\n\n const calculatedFacialDamage = useMemo(() => {\n return facialDamage ?? createDefaultFacialDamage();\n }, [facialDamage]);\n\n const opponentPos = useMemo(() => {\n if (opponentPosition) {\n return new THREE.Vector3(...opponentPosition);\n }\n return new THREE.Vector3(facing === \"right\" ? 5 : -5, 2, 0);\n }, [opponentPosition, facing]);\n\n\n const frameCounter = useRef(0);\n\n useFrame((_state, delta) => {\n const safeDelta = Math.min(delta, MAX_VISUAL_FRAME_DELTA_SECONDS);\n const isWalkingAnimation =\n currentAnimation === \"walk\" ||\n (typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\"));\n\n if (\n bodyFacing &&\n opponentPosition &&\n onBodyFacingUpdate &&\n !isWalkingAnimation\n ) {\n const playerPos = { x: position[0], y: position[2] }; // X and Z for 2D top-down\n const opponentPos = { x: opponentPosition[0], y: opponentPosition[2] };\n\n const isStepAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\");\n const isTurnAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"turn_\");\n\n const shouldLock =\n currentAnimation === \"attack\" ||\n currentAnimation === \"defend\" ||\n isStepAnimation ||\n isTurnAnimation;\n\n let updatedFacing = bodyFacing;\n\n if (shouldLock && !bodyFacing.isLocked) {\n updatedFacing = lockFacing(bodyFacing);\n } else if (!shouldLock && bodyFacing.isLocked) {\n updatedFacing = unlockFacing(bodyFacing);\n }\n\n if (!updatedFacing.isLocked) {\n updatedFacing = updateFacingTowardOpponent(\n updatedFacing,\n playerPos,\n opponentPos,\n safeDelta,\n Date.now(),\n );\n }\n\n if (updatedFacing !== bodyFacing) {\n onBodyFacingUpdate(updatedFacing);\n }\n }\n\n\n frameCounter.current = (frameCounter.current + 1) % 10;\n\n updateRigAnimation(rig, safeDelta);\n\n updateHandAnimations(safeDelta);\n\n updateBalanceAnimations(safeDelta, frameCounter.current);\n\n updateMuscleActivations(safeDelta, frameCounter.current);\n\n if (bodyFacing) {\n const head = rig.bones.get(\"head\");\n if (head) {\n const headRotation = getHeadAngleRadians(bodyFacing);\n head.rotation.y = headRotation;\n }\n }\n });\n\n const effectiveRotation = diagonalRotationY ?? rotation;\n\n return (\n <group\n position={position}\n rotation={[0, effectiveRotation, 0]}\n scale={[facing === \"left\" ? -scale : scale, scale, scale]}\n name={`skeletal-player3d-${playerId}`}\n >\n {/* Inner group for sway animation and helpless lean */}\n <group position={swayPosition} rotation={[helplessRotation, 0, 0]}>\n {/* Skeletal rig rendering with bone-attached muscles */}\n <BoneRenderer\n rig={rig}\n color={bodyColor}\n showBones={true}\n renderMode={showSkeleton ? \"debug\" : \"solid\"}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={10}\n facialExpression={calculatedExpression}\n facialDamage={calculatedFacialDamage}\n opponentPosition={opponentPos}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth,\n torsoLength: physicalAttributes.torsoLength,\n armLength: physicalAttributes.armLength,\n legLength: physicalAttributes.legLength,\n }}\n muscleStates={muscleStates}\n isExhausted={stamina < 20}\n archetype={archetype}\n />\n\n {/* Clothing is now rendered via BoneClothing inside BoneRenderer */}\n {/* This ensures clothing inherits bone transforms automatically */}\n\n {/* Blocking shield effect */}\n {isBlocking && (\n <mesh position={[0, 1.2, 0.3]}>\n <circleGeometry args={[0.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_BLUE}\n transparent\n opacity={0.5}\n side={2}\n />\n </mesh>\n )}\n\n {/* Counter indicator */}\n {isCountering && (\n <mesh position={[0, 1.5, 0]}>\n <torusGeometry args={[0.4, 0.05, 8, 32]} />\n <meshBasicMaterial color={KOREAN_COLORS.ACCENT_PURPLE} />\n </mesh>\n )}\n\n {/* Player name overlay */}\n {showDetails && name && (\n <Html\n position={[0, 2.8, 0]}\n center\n distanceFactor={isMobile ? 15 : 10}\n occlude={false}\n style={{ pointerEvents: \"none\", userSelect: \"none\" }}\n >\n <div\n style={{\n fontFamily: FONT_FAMILY.KOREAN,\n textAlign: \"center\",\n color: \"white\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n >\n {/* Player name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n data-testid=\"player-name\"\n >\n {name.korean}\n </div>\n\n {/* Trigram symbol */}\n {showStanceIndicator && (\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: toHexColor(stanceColor),\n }}\n data-testid=\"trigram-symbol\"\n >\n {trigramSymbol}\n </div>\n )}\n\n {/* Combat state text */}\n {(isBlocking || isStunned || isCountering) && (\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontWeight: \"bold\",\n color: \"#ffff00\",\n marginTop: \"4px\",\n }}\n data-testid=\"combat-state\"\n >\n {isBlocking ? \"방어\" : isStunned ? \"기절\" : \"반격\"}\n </div>\n )}\n </div>\n </Html>\n )}\n\n {/* State indicators (health, stamina, Ki, balance) */}\n {showDetails && (\n <PlayerStateIndicators\n health={health}\n maxHealth={maxHealth}\n stamina={stamina}\n ki={ki}\n balance={balance}\n consciousness={consciousness}\n pain={pain}\n bloodLoss={bloodLoss}\n isMobile={isMobile}\n />\n )}\n </group>{\" \"}\n {/* Close inner sway group */}\n </group>\n );\n};\n\nexport default SkeletalPlayer3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SkeletalPlayer3D.js","names":[],"sources":["../../../../../src/components/shared/three/models/SkeletalPlayer3D.tsx"],"sourcesContent":["/**\n * SkeletalPlayer3D component with articulated body model\n *\n * Implements full skeletal rigging system for realistic fighter animations\n * with independent limb movement, elbow/knee joints, and attack animations.\n *\n * @module components/three/SkeletalPlayer3D\n * @category 3D Components\n * @korean 골격플레이어3D컴포넌트\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport { useBalanceAnimations } from \"../../../../hooks/useBalanceAnimations\";\nimport { useHandPoseTransitions } from \"../../../../hooks/useHandPoseTransitions\";\nimport { useMuscleActivation } from \"../../../../hooks/useMuscleActivation\";\nimport { useSkeletalAnimation } from \"../../../../hooks/useSkeletalAnimation\";\nimport {\n createDefaultFacialDamage,\n createScaledHumanoidRig,\n MAX_VISUAL_FRAME_DELTA_SECONDS,\n getExpressionFromCombatState,\n getHeadAngleRadians,\n lockFacing,\n unlockFacing,\n updateFacingTowardOpponent,\n} from \"../../../../systems/animation\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { FacialExpression } from \"../../../../types/facial\";\nimport type { Player3DUnifiedProps } from \"../../../../types/player-visual\";\nimport type { SkeletalRig } from \"../../../../types/skeletal\";\nimport { toHexColor } from \"../../../../utils/colorHelpers\";\nimport { getArchetypeSkinTone } from \"../../../../utils/colorUtils\";\nimport BoneRenderer from \"../anatomy/BoneRenderer\";\nimport PlayerStateIndicators from \"../effects/PlayerStateIndicators\";\n\n/**\n * Get stance-specific color from Korean theming\n *\n * @param stance - Current trigram stance\n * @returns Hex color number\n * @korean 자세색상가져오기\n */\nconst getStanceColor = (stance: string): number => {\n const stanceColors: Record<string, number> = {\n geon: KOREAN_COLORS.TRIGRAM_GEON_PRIMARY,\n tae: KOREAN_COLORS.TRIGRAM_TAE_PRIMARY,\n li: KOREAN_COLORS.TRIGRAM_LI_PRIMARY,\n jin: KOREAN_COLORS.TRIGRAM_JIN_PRIMARY,\n son: KOREAN_COLORS.TRIGRAM_SON_PRIMARY,\n gam: KOREAN_COLORS.TRIGRAM_GAM_PRIMARY,\n gan: KOREAN_COLORS.TRIGRAM_GAN_PRIMARY,\n gon: KOREAN_COLORS.TRIGRAM_GON_PRIMARY,\n };\n return stanceColors[stance] ?? KOREAN_COLORS.PRIMARY_CYAN;\n};\n\n/**\n * Get trigram symbol for stance\n *\n * @param stance - Current trigram stance\n * @returns Unicode trigram symbol\n * @korean 팔괘기호가져오기\n */\nconst getTrigramSymbol = (stance: string): string => {\n const symbols: Record<string, string> = {\n geon: \"☰\",\n tae: \"☱\",\n li: \"☲\",\n jin: \"☳\",\n son: \"☴\",\n gam: \"☵\",\n gan: \"☶\",\n gon: \"☷\",\n };\n return symbols[stance] ?? \"☰\";\n};\n\n/**\n * SkeletalPlayer3D Component\n *\n * Complete skeletal player with 28-bone rig and realistic animations.\n * Supports all Korean martial arts attack animations (jab, cross, kicks, block).\n *\n * @example\n * ```tsx\n * <SkeletalPlayer3D\n * playerId=\"player1\"\n * archetype={PlayerArchetype.MUSA}\n * stance={TrigramStance.GEON}\n * position={[0, 0, 0]}\n * rotation={0}\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * currentAnimation=\"attack\"\n * attackAnimation=\"jab\"\n * showDetails={true}\n * />\n * ```\n *\n * @korean 골격플레이어3D컴포넌트\n */\nexport const SkeletalPlayer3D: React.FC<\n Player3DUnifiedProps & {\n readonly attackAnimation?: string;\n readonly showSkeleton?: boolean;\n }\n> = ({\n playerId,\n archetype,\n stance,\n position,\n rotation,\n health,\n maxHealth,\n stamina,\n ki,\n pain,\n balance,\n consciousness,\n bloodLoss,\n isBlocking,\n isStunned = false,\n isCountering = false,\n currentAnimation,\n isMobile,\n name,\n scale = 1,\n showDetails = true,\n facing = \"right\",\n showStanceIndicator = true,\n onAnimationComplete,\n attackAnimation,\n showSkeleton = false,\n facialExpression,\n facialDamage,\n enableFacialExpressions = false,\n enableEyeTracking = true,\n opponentPosition,\n bodyFacing,\n onBodyFacingUpdate,\n laterality, // Stance laterality (left/right foot forward)\n}) => {\n const effectiveLaterality = laterality ?? \"right\";\n const physicalAttributes = useMemo(\n () => getArchetypePhysicalAttributes(archetype),\n [archetype],\n );\n\n const rig = useMemo<SkeletalRig>(\n () => createScaledHumanoidRig(physicalAttributes),\n [physicalAttributes],\n );\n\n\n const { updateRigAnimation, diagonalRotationY } = useSkeletalAnimation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stance,\n laterality: effectiveLaterality, // Pass laterality for animation mirroring\n onAnimationComplete,\n });\n\n const { leftHandState, rightHandState, updateHandAnimations } =\n useHandPoseTransitions({\n currentAnimation,\n attackAnimation,\n isBlocking,\n });\n\n\n const { swayPosition, helplessRotation, updateBalanceAnimations } =\n useBalanceAnimations({\n balance,\n });\n\n const { muscleStates, updateMuscleActivations } = useMuscleActivation({\n currentAnimation,\n attackAnimation,\n isBlocking,\n stamina,\n });\n\n const skinTone = useMemo(() => getArchetypeSkinTone(archetype), [archetype]);\n\n const bodyColor = useMemo(() => {\n if (isStunned) return KOREAN_COLORS.WARNING_YELLOW;\n if (health / maxHealth < 0.3) return KOREAN_COLORS.ACCENT_RED;\n if (ki / 100 > 0.8) return KOREAN_COLORS.PRIMARY_CYAN;\n return skinTone; // Use archetype skin tone instead of primary color\n }, [isStunned, health, maxHealth, ki, skinTone]);\n\n const stanceColor = useMemo(() => getStanceColor(stance), [stance]);\n const trigramSymbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n\n const [justHit, setJustHit] = useState(false);\n const [justLanded, setJustLanded] = useState(false);\n const lastHealthRef = useRef(health);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (health < lastHealthRef.current) {\n setJustHit(true);\n timeoutId = setTimeout(() => setJustHit(false), 1000); // Clear after 1 second\n }\n\n lastHealthRef.current = health;\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [health]);\n\n useEffect(() => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n if (currentAnimation === \"attack\") {\n setJustLanded(true);\n timeoutId = setTimeout(() => setJustLanded(false), 500); // Clear after 0.5 seconds\n }\n\n return () => {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n };\n }, [currentAnimation]);\n\n const calculatedExpression = useMemo(() => {\n if (!enableFacialExpressions) {\n return FacialExpression.NEUTRAL;\n }\n\n if (facialExpression) {\n return facialExpression;\n }\n\n return getExpressionFromCombatState(\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n );\n }, [\n enableFacialExpressions,\n facialExpression,\n health,\n maxHealth,\n stamina,\n pain,\n consciousness,\n justHit,\n justLanded,\n ]);\n\n const calculatedFacialDamage = useMemo(() => {\n return facialDamage ?? createDefaultFacialDamage();\n }, [facialDamage]);\n\n const opponentPos = useMemo(() => {\n if (opponentPosition) {\n return new THREE.Vector3(...opponentPosition);\n }\n return new THREE.Vector3(facing === \"right\" ? 5 : -5, 2, 0);\n }, [opponentPosition, facing]);\n\n\n const frameCounter = useRef(0);\n\n useFrame((_state, delta) => {\n const safeDelta = Math.min(delta, MAX_VISUAL_FRAME_DELTA_SECONDS);\n const isWalkingAnimation =\n currentAnimation === \"walk\" ||\n (typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\"));\n\n if (\n bodyFacing &&\n opponentPosition &&\n onBodyFacingUpdate &&\n !isWalkingAnimation\n ) {\n const playerPos = { x: position[0], y: position[2] }; // X and Z for 2D top-down\n const opponentPos = { x: opponentPosition[0], y: opponentPosition[2] };\n\n const isStepAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"step_\");\n const isTurnAnimation =\n typeof currentAnimation === \"string\" &&\n currentAnimation.startsWith(\"turn_\");\n\n const shouldLock =\n currentAnimation === \"attack\" ||\n currentAnimation === \"defend\" ||\n isStepAnimation ||\n isTurnAnimation;\n\n let updatedFacing = bodyFacing;\n\n if (shouldLock && !bodyFacing.isLocked) {\n updatedFacing = lockFacing(bodyFacing);\n } else if (!shouldLock && bodyFacing.isLocked) {\n updatedFacing = unlockFacing(bodyFacing);\n }\n\n if (!updatedFacing.isLocked) {\n updatedFacing = updateFacingTowardOpponent(\n updatedFacing,\n playerPos,\n opponentPos,\n safeDelta,\n Date.now(),\n );\n }\n\n if (updatedFacing !== bodyFacing) {\n onBodyFacingUpdate(updatedFacing);\n }\n }\n\n\n frameCounter.current = (frameCounter.current + 1) % 10;\n\n updateRigAnimation(rig, safeDelta);\n\n updateHandAnimations(safeDelta);\n\n updateBalanceAnimations(safeDelta, frameCounter.current);\n\n updateMuscleActivations(safeDelta, frameCounter.current);\n\n if (bodyFacing) {\n const head = rig.bones.get(\"head\");\n if (head) {\n const headRotation = getHeadAngleRadians(bodyFacing);\n head.rotation.y = headRotation;\n }\n }\n });\n\n const effectiveRotation = diagonalRotationY ?? rotation;\n\n return (\n <group\n position={position}\n rotation={[0, effectiveRotation, 0]}\n scale={[facing === \"left\" ? -scale : scale, scale, scale]}\n name={`skeletal-player3d-${playerId}`}\n >\n {/* Inner group for sway animation and helpless lean */}\n <group position={swayPosition} rotation={[helplessRotation, 0, 0]}>\n {/* Skeletal rig rendering with bone-attached muscles */}\n <BoneRenderer\n rig={rig}\n color={bodyColor}\n showBones={true}\n renderMode={showSkeleton ? \"debug\" : \"solid\"}\n leftHandState={leftHandState}\n rightHandState={rightHandState}\n cameraDistance={10}\n facialExpression={calculatedExpression}\n facialDamage={calculatedFacialDamage}\n opponentPosition={opponentPos}\n enableFacialExpressions={enableFacialExpressions}\n enableEyeTracking={enableEyeTracking}\n physicalAttributes={{\n muscleMass: physicalAttributes.muscleMass,\n fatMass: physicalAttributes.fatMass,\n shoulderWidth: physicalAttributes.shoulderWidth,\n torsoLength: physicalAttributes.torsoLength,\n armLength: physicalAttributes.armLength,\n legLength: physicalAttributes.legLength,\n }}\n muscleStates={muscleStates}\n isExhausted={stamina < 20}\n archetype={archetype}\n />\n\n {/* Clothing is now rendered via BoneClothing inside BoneRenderer */}\n {/* This ensures clothing inherits bone transforms automatically */}\n\n {/* Blocking shield effect */}\n {isBlocking && (\n <mesh position={[0, 1.2, 0.3]}>\n <circleGeometry args={[0.5, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_BLUE}\n transparent\n opacity={0.5}\n side={2}\n />\n </mesh>\n )}\n\n {/* Counter indicator */}\n {isCountering && (\n <mesh position={[0, 1.5, 0]}>\n <torusGeometry args={[0.4, 0.05, 8, 32]} />\n <meshBasicMaterial color={KOREAN_COLORS.ACCENT_PURPLE} />\n </mesh>\n )}\n\n {/* Player name overlay */}\n {showDetails && name && (\n <Html\n position={[0, 2.8, 0]}\n center\n distanceFactor={isMobile ? 15 : 10}\n occlude={false}\n style={{ pointerEvents: \"none\", userSelect: \"none\" }}\n >\n <div\n style={{\n fontFamily: FONT_FAMILY.KOREAN,\n textAlign: \"center\",\n color: \"white\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n >\n {/* Player name */}\n <div\n style={{\n fontSize: isMobile ? \"12px\" : \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n }}\n data-testid=\"player-name\"\n >\n {name.korean}\n </div>\n\n {/* Trigram symbol */}\n {showStanceIndicator && (\n <div\n style={{\n fontSize: isMobile ? \"14px\" : \"16px\",\n color: toHexColor(stanceColor),\n }}\n data-testid=\"trigram-symbol\"\n >\n {trigramSymbol}\n </div>\n )}\n\n {/* Combat state text */}\n {(isBlocking || isStunned || isCountering) && (\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"12px\",\n fontWeight: \"bold\",\n color: \"#ffff00\",\n marginTop: \"4px\",\n }}\n data-testid=\"combat-state\"\n >\n {isBlocking ? \"방어\" : isStunned ? \"기절\" : \"반격\"}\n </div>\n )}\n </div>\n </Html>\n )}\n\n {/* State indicators (health, stamina, Ki, balance) */}\n {showDetails && (\n <PlayerStateIndicators\n health={health}\n maxHealth={maxHealth}\n stamina={stamina}\n ki={ki}\n balance={balance}\n consciousness={consciousness}\n pain={pain}\n bloodLoss={bloodLoss}\n isMobile={isMobile}\n />\n )}\n </group>{\" \"}\n {/* Close inner sway group */}\n </group>\n );\n};\n\nexport default SkeletalPlayer3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdaptiveQuality.js","names":[],"sources":["../../../../../src/components/shared/three/optimization/AdaptiveQuality.ts"],"sourcesContent":["/**\n * AdaptiveQuality - Dynamic quality adjustment system for mobile performance\n *\n * Monitors real-time FPS and automatically adjusts rendering quality to maintain\n * 55fps target on mobile devices. Provides quality presets and smooth transitions.\n *\n * Features:\n * - Real-time FPS monitoring\n * - Automatic quality adjustment (high/medium/low)\n * - Debounced quality changes to prevent thrashing\n * - Mobile-optimized thresholds\n * - React hook integration\n *\n * @module components/shared/three/optimization/AdaptiveQuality\n * @category Performance Optimization\n * @korean 적응형품질시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * Quality levels for adaptive rendering\n */\nexport type QualityLevel = \"high\" | \"medium\" | \"low\";\n\n/**\n * Quality settings for each level\n */\nexport interface QualitySettings {\n readonly level: QualityLevel;\n readonly shadowMapSize: number;\n readonly maxParticles: number;\n readonly postProcessing: boolean;\n readonly effectsQuality: number; // 0.0-1.0 multiplier\n}\n\n/**\n * FPS thresholds for quality adjustment\n */\nexport interface AdaptiveQualityThresholds {\n /** FPS threshold to downgrade quality */\n readonly downgradeThreshold: number;\n /** FPS threshold to upgrade quality */\n readonly upgradeThreshold: number;\n /** Minimum time between quality changes (ms) */\n readonly debounceTime: number;\n /** Number of frames to average for stability */\n readonly sampleSize: number;\n}\n\n/**\n * Default thresholds for mobile optimization\n */\nconst DEFAULT_MOBILE_THRESHOLDS: AdaptiveQualityThresholds = {\n downgradeThreshold: 45, // Downgrade if fps drops below 45\n upgradeThreshold: 58, // Upgrade if fps consistently above 58\n debounceTime: 2000, // 2 seconds between changes\n sampleSize: 60, // Average over 60 frames (~1 second at 60fps)\n};\n\n/**\n * Quality presets optimized for mobile performance\n */\nexport const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {\n high: {\n level: \"high\",\n shadowMapSize: 1536,\n maxParticles: 60,\n postProcessing: false, // Keep disabled on mobile\n effectsQuality: 1.0,\n },\n medium: {\n level: \"medium\",\n shadowMapSize: 1024,\n maxParticles: 40,\n postProcessing: false,\n effectsQuality: 0.75,\n },\n low: {\n level: \"low\",\n shadowMapSize: 512,\n maxParticles: 20,\n postProcessing: false,\n effectsQuality: 0.5,\n },\n} as const;\n\n/**\n * Adaptive quality system class\n */\nexport class AdaptiveQualitySystem {\n private currentQuality: QualityLevel = \"high\";\n private fpsHistory: number[] = [];\n private lastQualityChange = 0;\n private readonly thresholds: AdaptiveQualityThresholds;\n\n constructor(thresholds: Partial<AdaptiveQualityThresholds> = {}) {\n this.thresholds = { ...DEFAULT_MOBILE_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update system with current FPS\n * Returns new quality level if changed, null otherwise\n */\n update(currentFps: number): QualityLevel | null {\n const now = performance.now();\n\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n if (this.currentQuality === \"high\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"low\";\n }\n } else if (\n avgFps > this.thresholds.upgradeThreshold &&\n this.currentQuality !== \"high\"\n ) {\n if (this.currentQuality === \"low\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"high\";\n }\n }\n\n if (newQuality !== null) {\n this.currentQuality = newQuality;\n this.lastQualityChange = now;\n this.fpsHistory = []; // Reset history after change\n\n if (import.meta.env.DEV) {\n console.log(\n `[AdaptiveQuality] Quality changed to ${newQuality} (avg fps: ${avgFps.toFixed(1)})`,\n );\n }\n\n return newQuality;\n }\n\n return null;\n }\n\n /**\n * Get current quality level\n */\n getCurrentQuality(): QualityLevel {\n return this.currentQuality;\n }\n\n /**\n * Get settings for current quality level\n */\n getCurrentSettings(): QualitySettings {\n return QUALITY_PRESETS[this.currentQuality];\n }\n\n /**\n * Manually set quality level\n */\n setQuality(quality: QualityLevel): void {\n this.currentQuality = quality;\n this.lastQualityChange = performance.now();\n this.fpsHistory = [];\n }\n\n /**\n * Reset the system\n */\n reset(): void {\n this.fpsHistory = [];\n this.lastQualityChange = 0;\n }\n}\n\n/**\n * React hook for adaptive quality management\n *\n * Automatically monitors FPS and adjusts quality settings.\n * Returns current quality level and settings.\n *\n * @param enabled - Whether adaptive quality is enabled\n * @param isMobile - Whether device is mobile (stricter thresholds)\n * @param onQualityChange - Callback when quality level changes\n * @returns Current quality settings\n *\n * @example\n * ```tsx\n * function CombatScene({ isMobile }) {\n * const quality = useAdaptiveQuality(true, isMobile, (level) => {\n * console.log(`Quality changed to ${level}`);\n * });\n *\n * return (\n * <Canvas shadowMap={{ size: quality.shadowMapSize }}>\n * <ParticleSystem maxParticles={quality.maxParticles} />\n * </Canvas>\n * );\n * }\n * ```\n */\nexport function useAdaptiveQuality(\n enabled: boolean = true,\n isMobile: boolean = false,\n onQualityChange?: (quality: QualityLevel) => void,\n): QualitySettings {\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n const thresholds = isMobile\n ? {\n downgradeThreshold: 45, // More aggressive on mobile\n upgradeThreshold: 58,\n debounceTime: 2000,\n sampleSize: 60,\n }\n : {\n downgradeThreshold: 50,\n upgradeThreshold: 59,\n debounceTime: 3000,\n sampleSize: 90,\n };\n\n systemRef.current = new AdaptiveQualitySystem(thresholds);\n systemRef.current.setQuality(initialQuality);\n }, [isMobile, initialQuality]);\n\n const lastTimeRef = useRef(0);\n const frameCountRef = useRef(0);\n const initializedRef = useRef(false);\n\n useFrame(() => {\n if (!enabled || !systemRef.current) return;\n\n const now = performance.now();\n\n if (!initializedRef.current) {\n lastTimeRef.current = now;\n initializedRef.current = true;\n return;\n }\n\n const delta = now - lastTimeRef.current;\n\n if (delta > 0) {\n const fps = 1000 / delta;\n frameCountRef.current++;\n\n const newQuality = systemRef.current.update(fps);\n\n if (newQuality !== null) {\n setCurrentQuality(newQuality);\n onQualityChange?.(newQuality);\n }\n }\n\n lastTimeRef.current = now;\n });\n\n return QUALITY_PRESETS[currentQuality];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdaptiveQuality.js","names":[],"sources":["../../../../../src/components/shared/three/optimization/AdaptiveQuality.ts"],"sourcesContent":["/**\n * AdaptiveQuality - Dynamic quality adjustment system for mobile performance\n *\n * Monitors real-time FPS and automatically adjusts rendering quality to maintain\n * 55fps target on mobile devices. Provides quality presets and smooth transitions.\n *\n * Features:\n * - Real-time FPS monitoring\n * - Automatic quality adjustment (high/medium/low)\n * - Debounced quality changes to prevent thrashing\n * - Mobile-optimized thresholds\n * - React hook integration\n *\n * @module components/shared/three/optimization/AdaptiveQuality\n * @category Performance Optimization\n * @korean 적응형품질시스템\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { useEffect, useRef, useState } from \"react\";\n\n/**\n * Quality levels for adaptive rendering\n */\nexport type QualityLevel = \"high\" | \"medium\" | \"low\";\n\n/**\n * Quality settings for each level\n */\nexport interface QualitySettings {\n readonly level: QualityLevel;\n readonly shadowMapSize: number;\n readonly maxParticles: number;\n readonly postProcessing: boolean;\n readonly effectsQuality: number; // 0.0-1.0 multiplier\n}\n\n/**\n * FPS thresholds for quality adjustment\n */\nexport interface AdaptiveQualityThresholds {\n /** FPS threshold to downgrade quality */\n readonly downgradeThreshold: number;\n /** FPS threshold to upgrade quality */\n readonly upgradeThreshold: number;\n /** Minimum time between quality changes (ms) */\n readonly debounceTime: number;\n /** Number of frames to average for stability */\n readonly sampleSize: number;\n}\n\n/**\n * Default thresholds for mobile optimization\n */\nconst DEFAULT_MOBILE_THRESHOLDS: AdaptiveQualityThresholds = {\n downgradeThreshold: 45, // Downgrade if fps drops below 45\n upgradeThreshold: 58, // Upgrade if fps consistently above 58\n debounceTime: 2000, // 2 seconds between changes\n sampleSize: 60, // Average over 60 frames (~1 second at 60fps)\n};\n\n/**\n * Quality presets optimized for mobile performance\n */\nexport const QUALITY_PRESETS: Record<QualityLevel, QualitySettings> = {\n high: {\n level: \"high\",\n shadowMapSize: 1536,\n maxParticles: 60,\n postProcessing: false, // Keep disabled on mobile\n effectsQuality: 1.0,\n },\n medium: {\n level: \"medium\",\n shadowMapSize: 1024,\n maxParticles: 40,\n postProcessing: false,\n effectsQuality: 0.75,\n },\n low: {\n level: \"low\",\n shadowMapSize: 512,\n maxParticles: 20,\n postProcessing: false,\n effectsQuality: 0.5,\n },\n} as const;\n\n/**\n * Adaptive quality system class\n */\nexport class AdaptiveQualitySystem {\n private currentQuality: QualityLevel = \"high\";\n private fpsHistory: number[] = [];\n private lastQualityChange = 0;\n private readonly thresholds: AdaptiveQualityThresholds;\n\n constructor(thresholds: Partial<AdaptiveQualityThresholds> = {}) {\n this.thresholds = { ...DEFAULT_MOBILE_THRESHOLDS, ...thresholds };\n }\n\n /**\n * Update system with current FPS\n * Returns new quality level if changed, null otherwise\n */\n update(currentFps: number): QualityLevel | null {\n const now = performance.now();\n\n this.fpsHistory.push(currentFps);\n if (this.fpsHistory.length > this.thresholds.sampleSize) {\n this.fpsHistory.shift();\n }\n\n if (this.fpsHistory.length < this.thresholds.sampleSize) {\n return null;\n }\n\n if (now - this.lastQualityChange < this.thresholds.debounceTime) {\n return null;\n }\n\n const avgFps =\n this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;\n\n let newQuality: QualityLevel | null = null;\n\n if (\n avgFps < this.thresholds.downgradeThreshold &&\n this.currentQuality !== \"low\"\n ) {\n if (this.currentQuality === \"high\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"low\";\n }\n } else if (\n avgFps > this.thresholds.upgradeThreshold &&\n this.currentQuality !== \"high\"\n ) {\n if (this.currentQuality === \"low\") {\n newQuality = \"medium\";\n } else if (this.currentQuality === \"medium\") {\n newQuality = \"high\";\n }\n }\n\n if (newQuality !== null) {\n this.currentQuality = newQuality;\n this.lastQualityChange = now;\n this.fpsHistory = []; // Reset history after change\n\n if (import.meta.env.DEV) {\n console.log(\n `[AdaptiveQuality] Quality changed to ${newQuality} (avg fps: ${avgFps.toFixed(1)})`,\n );\n }\n\n return newQuality;\n }\n\n return null;\n }\n\n /**\n * Get current quality level\n */\n getCurrentQuality(): QualityLevel {\n return this.currentQuality;\n }\n\n /**\n * Get settings for current quality level\n */\n getCurrentSettings(): QualitySettings {\n return QUALITY_PRESETS[this.currentQuality];\n }\n\n /**\n * Manually set quality level\n */\n setQuality(quality: QualityLevel): void {\n this.currentQuality = quality;\n this.lastQualityChange = performance.now();\n this.fpsHistory = [];\n }\n\n /**\n * Reset the system\n */\n reset(): void {\n this.fpsHistory = [];\n this.lastQualityChange = 0;\n }\n}\n\n/**\n * React hook for adaptive quality management\n *\n * Automatically monitors FPS and adjusts quality settings.\n * Returns current quality level and settings.\n *\n * @param enabled - Whether adaptive quality is enabled\n * @param isMobile - Whether device is mobile (stricter thresholds)\n * @param onQualityChange - Callback when quality level changes\n * @returns Current quality settings\n *\n * @example\n * ```tsx\n * function CombatScene({ isMobile }) {\n * const quality = useAdaptiveQuality(true, isMobile, (level) => {\n * console.log(`Quality changed to ${level}`);\n * });\n *\n * return (\n * <Canvas shadowMap={{ size: quality.shadowMapSize }}>\n * <ParticleSystem maxParticles={quality.maxParticles} />\n * </Canvas>\n * );\n * }\n * ```\n */\nexport function useAdaptiveQuality(\n enabled: boolean = true,\n isMobile: boolean = false,\n onQualityChange?: (quality: QualityLevel) => void,\n): QualitySettings {\n const initialQuality: QualityLevel = isMobile ? \"medium\" : \"high\";\n\n const [currentQuality, setCurrentQuality] =\n useState<QualityLevel>(initialQuality);\n\n const systemRef = useRef<AdaptiveQualitySystem | null>(null);\n\n useEffect(() => {\n const thresholds = isMobile\n ? {\n downgradeThreshold: 45, // More aggressive on mobile\n upgradeThreshold: 58,\n debounceTime: 2000,\n sampleSize: 60,\n }\n : {\n downgradeThreshold: 50,\n upgradeThreshold: 59,\n debounceTime: 3000,\n sampleSize: 90,\n };\n\n systemRef.current = new AdaptiveQualitySystem(thresholds);\n systemRef.current.setQuality(initialQuality);\n }, [isMobile, initialQuality]);\n\n const lastTimeRef = useRef(0);\n const frameCountRef = useRef(0);\n const initializedRef = useRef(false);\n\n useFrame(() => {\n if (!enabled || !systemRef.current) return;\n\n const now = performance.now();\n\n if (!initializedRef.current) {\n lastTimeRef.current = now;\n initializedRef.current = true;\n return;\n }\n\n const delta = now - lastTimeRef.current;\n\n if (delta > 0) {\n const fps = 1000 / delta;\n frameCountRef.current++;\n\n const newQuality = systemRef.current.update(fps);\n\n if (newQuality !== null) {\n setCurrentQuality(newQuality);\n onQualityChange?.(newQuality);\n }\n }\n\n lastTimeRef.current = now;\n });\n\n return QUALITY_PRESETS[currentQuality];\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AtmosphericParticles3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/AtmosphericParticles3D.tsx"],"sourcesContent":["/**\n * AtmosphericParticles3D - Three.js 3D atmospheric particle effects\n *\n * Renders rain/mist particles for environmental depth in the combat arena\n * Creates an immersive cyberpunk urban night atmosphere\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\n\n/**\n * Props for the AtmosphericParticles3D component.\n */\nexport interface AtmosphericParticles3DProps {\n /** Number of particles to render. Defaults to 500 */\n readonly count?: number;\n /** Scale factor for particle spread (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Particle fall speed. Defaults to 2 */\n readonly speed?: number;\n}\n\n/**\n * Generate particle positions using a deterministic pattern\n * This avoids Math.random() in render functions for React purity\n */\nfunction generateParticlePositions(\n count: number,\n spreadX: number,\n spreadY: number,\n spreadZ: number\n): Float32Array {\n const pos = new Float32Array(count * 3);\n\n for (let i = 0; i < count; i++) {\n const t = i / count; // Normalized index [0, 1]\n\n const offsetX = Math.sin(t * 123.456) * Math.cos(t * 789.012);\n const offsetY = Math.sin(t * 234.567) * Math.cos(t * 890.123);\n const offsetZ = Math.sin(t * 345.678) * Math.cos(t * 901.234);\n\n pos[i * 3] = offsetX * spreadX * 0.5;\n pos[i * 3 + 1] = offsetY * spreadY * 0.5 + spreadY * 0.5; // Bias upward\n pos[i * 3 + 2] = offsetZ * spreadZ * 0.5;\n }\n\n return pos;\n}\n\n/**\n * AtmosphericParticles3D Component\n * Creates rain/mist particle effects for atmospheric depth\n *\n * Performance optimized with:\n * - BufferGeometry for efficient rendering\n * - Additive blending for transparent particles\n * - Configurable particle count for mobile optimization\n * - Deterministic position generation (no Math.random in render)\n */\nexport const AtmosphericParticles3D: React.FC<\n AtmosphericParticles3DProps\n> = ({ count = 500, scale = 1.0, speed = 2 }) => {\n const particlesRef = useRef<THREE.Points>(null);\n const [geometry] = useState(() => new THREE.BufferGeometry());\n\n const spreadX = 40 * scale;\n const spreadY = 20;\n const spreadZ = 40 * scale;\n\n useEffect(() => {\n const positions = generateParticlePositions(\n count,\n spreadX,\n spreadY,\n spreadZ\n );\n \n geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n\n return () => {\n geometry.dispose();\n };\n }, [count, spreadX, spreadY, spreadZ, geometry]);\n\n useFrame((_state, delta) => {\n if (!particlesRef.current) return;\n\n const positions =\n particlesRef.current.geometry.attributes.position.array as Float32Array;\n for (let i = 0; i < count; i++) {\n positions[i * 3 + 1] -= delta * speed; // Fall down\n if (positions[i * 3 + 1] < 0) {\n positions[i * 3 + 1] = spreadY; // Reset to top\n }\n }\n particlesRef.current.geometry.attributes.position.needsUpdate = true;\n });\n\n return (\n <points ref={particlesRef} geometry={geometry}>\n <pointsMaterial\n size={0.05}\n color={0xffffff}\n transparent\n opacity={0.3}\n sizeAttenuation\n blending={THREE.AdditiveBlending}\n depthWrite={false}\n />\n </points>\n );\n};\n\nexport default AtmosphericParticles3D;\n"],"mappings":";;;;;;;;;;;;;;;AA2BA,SAAS,0BACP,OACA,SACA,SACA,SACc;CACd,MAAM,MAAM,IAAI,aAAa,QAAQ,CAAC;CAEtC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;EAC9B,MAAM,IAAI,IAAI;EAEd,MAAM,UAAU,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO;EAC5D,MAAM,UAAU,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO;EAC5D,MAAM,UAAU,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO;EAE5D,IAAI,IAAI,KAAK,UAAU,UAAU;EACjC,IAAI,IAAI,IAAI,KAAK,UAAU,UAAU,KAAM,UAAU;EACrD,IAAI,IAAI,IAAI,KAAK,UAAU,UAAU;CACvC;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,IAAa,0BAER,EAAE,QAAQ,KAAK,QAAQ,GAAK,QAAQ,QAAQ;CAC/C,MAAM,eAAe,OAAqB,IAAI;CAC9C,MAAM,CAAC,YAAY,eAAe,IAAI,MAAM,eAAe,CAAC;CAE5D,MAAM,UAAU,KAAK;CACrB,MAAM,UAAU;CAChB,MAAM,UAAU,KAAK;CAErB,gBAAgB;EACd,MAAM,YAAY,0BAChB,OACA,SACA,SACA,OACF;EAEA,SAAS,aAAa,YAAY,IAAI,MAAM,gBAAgB,WAAW,CAAC,CAAC;EAEzE,aAAa;GACX,SAAS,QAAQ;EACnB;CACF,GAAG;EAAC;EAAO;EAAS;EAAS;EAAS;CAAQ,CAAC;CAE/C,UAAU,QAAQ,UAAU;EAC1B,IAAI,CAAC,aAAa,SAAS;EAE3B,MAAM,YACJ,aAAa,QAAQ,SAAS,WAAW,SAAS;EACpD,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,UAAU,IAAI,IAAI,MAAM,QAAQ;GAChC,IAAI,UAAU,IAAI,IAAI,KAAK,GACzB,UAAU,IAAI,IAAI,KAAK;EAE3B;EACA,aAAa,QAAQ,SAAS,WAAW,SAAS,cAAc;CAClE,CAAC;CAED,OACE,oBAAC,UAAD;EAAQ,KAAK;EAAwB;
|
|
1
|
+
{"version":3,"file":"AtmosphericParticles3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/AtmosphericParticles3D.tsx"],"sourcesContent":["/**\n * AtmosphericParticles3D - Three.js 3D atmospheric particle effects\n *\n * Renders rain/mist particles for environmental depth in the combat arena\n * Creates an immersive cyberpunk urban night atmosphere\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\n\n/**\n * Props for the AtmosphericParticles3D component.\n */\nexport interface AtmosphericParticles3DProps {\n /** Number of particles to render. Defaults to 500 */\n readonly count?: number;\n /** Scale factor for particle spread (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Particle fall speed. Defaults to 2 */\n readonly speed?: number;\n}\n\n/**\n * Generate particle positions using a deterministic pattern\n * This avoids Math.random() in render functions for React purity\n */\nfunction generateParticlePositions(\n count: number,\n spreadX: number,\n spreadY: number,\n spreadZ: number\n): Float32Array {\n const pos = new Float32Array(count * 3);\n\n for (let i = 0; i < count; i++) {\n const t = i / count; // Normalized index [0, 1]\n\n const offsetX = Math.sin(t * 123.456) * Math.cos(t * 789.012);\n const offsetY = Math.sin(t * 234.567) * Math.cos(t * 890.123);\n const offsetZ = Math.sin(t * 345.678) * Math.cos(t * 901.234);\n\n pos[i * 3] = offsetX * spreadX * 0.5;\n pos[i * 3 + 1] = offsetY * spreadY * 0.5 + spreadY * 0.5; // Bias upward\n pos[i * 3 + 2] = offsetZ * spreadZ * 0.5;\n }\n\n return pos;\n}\n\n/**\n * AtmosphericParticles3D Component\n * Creates rain/mist particle effects for atmospheric depth\n *\n * Performance optimized with:\n * - BufferGeometry for efficient rendering\n * - Additive blending for transparent particles\n * - Configurable particle count for mobile optimization\n * - Deterministic position generation (no Math.random in render)\n */\nexport const AtmosphericParticles3D: React.FC<\n AtmosphericParticles3DProps\n> = ({ count = 500, scale = 1.0, speed = 2 }) => {\n const particlesRef = useRef<THREE.Points>(null);\n const [geometry] = useState(() => new THREE.BufferGeometry());\n\n const spreadX = 40 * scale;\n const spreadY = 20;\n const spreadZ = 40 * scale;\n\n useEffect(() => {\n const positions = generateParticlePositions(\n count,\n spreadX,\n spreadY,\n spreadZ\n );\n \n geometry.setAttribute(\"position\", new THREE.BufferAttribute(positions, 3));\n\n return () => {\n geometry.dispose();\n };\n }, [count, spreadX, spreadY, spreadZ, geometry]);\n\n useFrame((_state, delta) => {\n if (!particlesRef.current) return;\n\n const positions =\n particlesRef.current.geometry.attributes.position.array as Float32Array;\n for (let i = 0; i < count; i++) {\n positions[i * 3 + 1] -= delta * speed; // Fall down\n if (positions[i * 3 + 1] < 0) {\n positions[i * 3 + 1] = spreadY; // Reset to top\n }\n }\n particlesRef.current.geometry.attributes.position.needsUpdate = true;\n });\n\n return (\n <points ref={particlesRef} geometry={geometry}>\n <pointsMaterial\n size={0.05}\n color={0xffffff}\n transparent\n opacity={0.3}\n sizeAttenuation\n blending={THREE.AdditiveBlending}\n depthWrite={false}\n />\n </points>\n );\n};\n\nexport default AtmosphericParticles3D;\n"],"mappings":";;;;;;;;;;;;;;;AA2BA,SAAS,0BACP,OACA,SACA,SACA,SACc;CACd,MAAM,MAAM,IAAI,aAAa,QAAQ,CAAC;CAEtC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;EAC9B,MAAM,IAAI,IAAI;EAEd,MAAM,UAAU,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO;EAC5D,MAAM,UAAU,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO;EAC5D,MAAM,UAAU,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO;EAE5D,IAAI,IAAI,KAAK,UAAU,UAAU;EACjC,IAAI,IAAI,IAAI,KAAK,UAAU,UAAU,KAAM,UAAU;EACrD,IAAI,IAAI,IAAI,KAAK,UAAU,UAAU;CACvC;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,IAAa,0BAER,EAAE,QAAQ,KAAK,QAAQ,GAAK,QAAQ,QAAQ;CAC/C,MAAM,eAAe,OAAqB,IAAI;CAC9C,MAAM,CAAC,YAAY,eAAe,IAAI,MAAM,eAAe,CAAC;CAE5D,MAAM,UAAU,KAAK;CACrB,MAAM,UAAU;CAChB,MAAM,UAAU,KAAK;CAErB,gBAAgB;EACd,MAAM,YAAY,0BAChB,OACA,SACA,SACA,OACF;EAEA,SAAS,aAAa,YAAY,IAAI,MAAM,gBAAgB,WAAW,CAAC,CAAC;EAEzE,aAAa;GACX,SAAS,QAAQ;EACnB;CACF,GAAG;EAAC;EAAO;EAAS;EAAS;EAAS;CAAQ,CAAC;CAE/C,UAAU,QAAQ,UAAU;EAC1B,IAAI,CAAC,aAAa,SAAS;EAE3B,MAAM,YACJ,aAAa,QAAQ,SAAS,WAAW,SAAS;EACpD,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK;GAC9B,UAAU,IAAI,IAAI,MAAM,QAAQ;GAChC,IAAI,UAAU,IAAI,IAAI,KAAK,GACzB,UAAU,IAAI,IAAI,KAAK;EAE3B;EACA,aAAa,QAAQ,SAAS,WAAW,SAAS,cAAc;CAClE,CAAC;CAED,OACE,oBAAC,UAAD;EAAQ,KAAK;EAAwB;EACnC,UAAA,oBAAC,kBAAD;GACE,MAAM;GACN,OAAO;GACP,aAAA;GACA,SAAS;GACT,iBAAA;GACA,UAAU,MAAM;GAChB,YAAY;EACb,CAAA;CACK,CAAA;AAEZ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CombatArena3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/CombatArena3D.tsx"],"sourcesContent":["/**\n * CombatArena3D - Three.js 3D arena environment\n *\n * Renders the 3D combat arena with Korean dojang aesthetic\n * Includes floor, lighting, and atmospheric effects\n *\n * Enhanced features:\n * - Korean cyberpunk lighting with neon point lights (cyan, gold, blue)\n * - Reflective wet concrete floor with clearcoat and emissive glow\n * - Upgraded shadow quality (2048x2048 shadow maps)\n * - Atmospheric fog with Korean color gradient\n * - Korean signage with emissive neon glow (전투, 흑괘, 급소격)\n * - Optional atmospheric particles (rain/mist)\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../utils/threeObjectPool\";\nimport AtmosphericParticles3D from \"./AtmosphericParticles3D\";\nimport KoreanSignage3D from \"./KoreanSignage3D\";\n\n/**\n * Props for the CombatArena3D component.\n * Configures the lighting and atmosphere of the 3D arena.\n */\nexport interface CombatArena3DProps {\n /** Lighting theme affecting ambiance and colors. Defaults to \"cyberpunk\" */\n readonly lighting?: \"cyberpunk\" | \"traditional\" | \"neutral\";\n /** Scale factor for arena size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Physical arena width in meters. Defaults to 10m (desktop standard) */\n readonly worldWidthMeters?: number;\n /** Physical arena depth in meters. Defaults to worldWidthMeters for square arena */\n readonly worldDepthMeters?: number;\n /** Enable atmospheric particles (rain/mist). Defaults to true on desktop, false on mobile */\n readonly enableParticles?: boolean;\n}\n\nconst FLOOR_SCALE_FACTOR = 1.5;\n\nconst SHADOW_MAP_SIZE_MOBILE: [number, number] = [1024, 1024]; // Upgraded from 512x512\nconst SHADOW_MAP_SIZE_DESKTOP: [number, number] = [2048, 2048]; // Upgraded from 1024x1024\n\n/**\n * CombatArena3D Component\n * Creates a Korean-themed 3D arena environment with cyberpunk aesthetic\n */\nexport const CombatArena3D: React.FC<CombatArena3DProps> = ({\n lighting = \"cyberpunk\",\n scale = 1.0,\n worldWidthMeters = 10, // Default desktop standard\n worldDepthMeters,\n enableParticles = scale >= 1.0, // Enable particles on desktop by default\n}) => {\n const gridRef = useRef<THREE.GridHelper>(null);\n\n useFrame(() => {\n if (gridRef.current) {\n gridRef.current.rotation.y += 0.0002;\n }\n });\n\n const effectiveDepth = worldDepthMeters ?? worldWidthMeters;\n const floorWidth = worldWidthMeters * FLOOR_SCALE_FACTOR;\n const floorDepth = effectiveDepth * FLOOR_SCALE_FACTOR;\n const gridSize = worldWidthMeters * FLOOR_SCALE_FACTOR;\n const markerDistance = worldWidthMeters * 0.8;\n const markerDepth = effectiveDepth * 0.4;\n\n const floorMaterial = useMemo(\n () => {\n const pooledBaseColor = ThreeObjectPools.color.acquire();\n const pooledEmissiveColor = ThreeObjectPools.color.acquire();\n \n try {\n pooledBaseColor.set(0x2a2a2a); // Dark concrete\n pooledEmissiveColor.set(KOREAN_COLORS.PRIMARY_CYAN);\n \n const material = new THREE.MeshPhysicalMaterial({\n color: pooledBaseColor.clone(), // Material takes ownership of cloned color\n roughness: 0.3, // Wet/reflective surface\n metalness: 0.1,\n clearcoat: 0.3, // Wet sheen\n clearcoatRoughness: 0.4,\n envMapIntensity: 1.5, // Enhanced reflections from Environment preset\n emissive: pooledEmissiveColor.clone(), // Material takes ownership of cloned color\n emissiveIntensity: 0.05,\n });\n \n return material;\n } finally {\n ThreeObjectPools.color.release(pooledBaseColor);\n ThreeObjectPools.color.release(pooledEmissiveColor);\n }\n },\n [],\n );\n\n useEffect(() => {\n return () => {\n floorMaterial.dispose();\n };\n }, [floorMaterial]);\n\n return (\n <group>\n {/* Lighting based on theme */}\n {lighting === \"cyberpunk\" && (\n <>\n {/* Base ambient light with Korean cyan tint */}\n <ambientLight intensity={0.5} color={KOREAN_COLORS.PRIMARY_CYAN} />\n\n {/* Primary directional light (moonlight) with upgraded shadows */}\n <directionalLight\n position={[15, 20, 10]}\n intensity={1.5}\n color={0xffffff}\n castShadow\n shadow-mapSize={\n scale < 1.0 ? SHADOW_MAP_SIZE_MOBILE : SHADOW_MAP_SIZE_DESKTOP\n }\n shadow-camera-far={50}\n shadow-camera-left={-20}\n shadow-camera-right={20}\n shadow-camera-top={20}\n shadow-camera-bottom={-20}\n shadow-bias={-0.0001}\n />\n\n {/* Korean neon accent lights */}\n {/* Cyan neon light (left side) */}\n <pointLight\n position={[-10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n />\n\n {/* Gold neon light (right side) */}\n <pointLight\n position={[10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.ACCENT_GOLD}\n />\n\n {/* Blue accent light (behind) */}\n <pointLight\n position={[0, 5, -15]}\n intensity={3}\n distance={30}\n decay={2}\n color={KOREAN_COLORS.ACCENT_BLUE}\n />\n </>\n )}\n\n {lighting === \"traditional\" && (\n <>\n <ambientLight intensity={0.6} color={0xffffee} />\n <directionalLight\n position={[5, 10, 5]}\n intensity={0.8}\n castShadow\n shadow-mapSize={[2048, 2048]}\n />\n </>\n )}\n\n {lighting === \"neutral\" && (\n <>\n <ambientLight intensity={0.5} />\n <directionalLight position={[10, 10, 5]} intensity={1} castShadow />\n </>\n )}\n\n {/* Arena floor - dojang mat with reflective wet concrete aesthetic */}\n {/* Floor dimensions use pre-calculated floorWidth/floorDepth which already include FLOOR_SCALE_FACTOR */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>\n <planeGeometry args={[floorWidth, floorDepth]} />\n {lighting === \"cyberpunk\" ? (\n <primitive object={floorMaterial} attach=\"material\" />\n ) : (\n <meshPhysicalMaterial\n color={KOREAN_COLORS.UI_BACKGROUND_MEDIUM}\n roughness={0.7}\n metalness={0.1}\n clearcoat={0}\n clearcoatRoughness={0.2}\n />\n )}\n </mesh>\n\n {/* Cyberpunk grid overlay (scale-aware) */}\n <gridHelper\n ref={gridRef}\n args={[\n gridSize,\n 20,\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n ]}\n position={[0, 0.01, 0]}\n />\n\n {/* Korean traditional boundary markers (scale-aware) */}\n {[\n [-markerDistance, 0, -markerDepth],\n [-markerDistance, 0, markerDepth],\n [markerDistance, 0, -markerDepth],\n [markerDistance, 0, markerDepth],\n ].map((pos, i) => (\n <mesh key={i} position={pos as [number, number, number]} castShadow>\n <cylinderGeometry args={[0.1 * scale, 0.15 * scale, 0.8, 8]} />\n <meshPhysicalMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n emissive={KOREAN_COLORS.ACCENT_GOLD}\n emissiveIntensity={0.5}\n metalness={0.8}\n roughness={0.2}\n clearcoat={1.0}\n />\n </mesh>\n ))}\n\n {/* Center marker - Yin Yang inspired (scale-aware) */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0]}>\n <ringGeometry args={[0.8 * scale, 1.0 * scale, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n\n {/* Korean Signage with neon glow */}\n {lighting === \"cyberpunk\" && <KoreanSignage3D scale={scale} />}\n\n {/* Atmospheric Particles (rain/mist) */}\n {lighting === \"cyberpunk\" && enableParticles && (\n <AtmosphericParticles3D\n count={scale >= 1.0 ? 500 : 250}\n scale={scale}\n speed={2}\n />\n )}\n </group>\n );\n};\n\nexport default CombatArena3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CombatArena3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/CombatArena3D.tsx"],"sourcesContent":["/**\n * CombatArena3D - Three.js 3D arena environment\n *\n * Renders the 3D combat arena with Korean dojang aesthetic\n * Includes floor, lighting, and atmospheric effects\n *\n * Enhanced features:\n * - Korean cyberpunk lighting with neon point lights (cyan, gold, blue)\n * - Reflective wet concrete floor with clearcoat and emissive glow\n * - Upgraded shadow quality (2048x2048 shadow maps)\n * - Atmospheric fog with Korean color gradient\n * - Korean signage with emissive neon glow (전투, 흑괘, 급소격)\n * - Optional atmospheric particles (rain/mist)\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../utils/threeObjectPool\";\nimport AtmosphericParticles3D from \"./AtmosphericParticles3D\";\nimport KoreanSignage3D from \"./KoreanSignage3D\";\n\n/**\n * Props for the CombatArena3D component.\n * Configures the lighting and atmosphere of the 3D arena.\n */\nexport interface CombatArena3DProps {\n /** Lighting theme affecting ambiance and colors. Defaults to \"cyberpunk\" */\n readonly lighting?: \"cyberpunk\" | \"traditional\" | \"neutral\";\n /** Scale factor for arena size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n /** Physical arena width in meters. Defaults to 10m (desktop standard) */\n readonly worldWidthMeters?: number;\n /** Physical arena depth in meters. Defaults to worldWidthMeters for square arena */\n readonly worldDepthMeters?: number;\n /** Enable atmospheric particles (rain/mist). Defaults to true on desktop, false on mobile */\n readonly enableParticles?: boolean;\n}\n\nconst FLOOR_SCALE_FACTOR = 1.5;\n\nconst SHADOW_MAP_SIZE_MOBILE: [number, number] = [1024, 1024]; // Upgraded from 512x512\nconst SHADOW_MAP_SIZE_DESKTOP: [number, number] = [2048, 2048]; // Upgraded from 1024x1024\n\n/**\n * CombatArena3D Component\n * Creates a Korean-themed 3D arena environment with cyberpunk aesthetic\n */\nexport const CombatArena3D: React.FC<CombatArena3DProps> = ({\n lighting = \"cyberpunk\",\n scale = 1.0,\n worldWidthMeters = 10, // Default desktop standard\n worldDepthMeters,\n enableParticles = scale >= 1.0, // Enable particles on desktop by default\n}) => {\n const gridRef = useRef<THREE.GridHelper>(null);\n\n useFrame(() => {\n if (gridRef.current) {\n gridRef.current.rotation.y += 0.0002;\n }\n });\n\n const effectiveDepth = worldDepthMeters ?? worldWidthMeters;\n const floorWidth = worldWidthMeters * FLOOR_SCALE_FACTOR;\n const floorDepth = effectiveDepth * FLOOR_SCALE_FACTOR;\n const gridSize = worldWidthMeters * FLOOR_SCALE_FACTOR;\n const markerDistance = worldWidthMeters * 0.8;\n const markerDepth = effectiveDepth * 0.4;\n\n const floorMaterial = useMemo(\n () => {\n const pooledBaseColor = ThreeObjectPools.color.acquire();\n const pooledEmissiveColor = ThreeObjectPools.color.acquire();\n \n try {\n pooledBaseColor.set(0x2a2a2a); // Dark concrete\n pooledEmissiveColor.set(KOREAN_COLORS.PRIMARY_CYAN);\n \n const material = new THREE.MeshPhysicalMaterial({\n color: pooledBaseColor.clone(), // Material takes ownership of cloned color\n roughness: 0.3, // Wet/reflective surface\n metalness: 0.1,\n clearcoat: 0.3, // Wet sheen\n clearcoatRoughness: 0.4,\n envMapIntensity: 1.5, // Enhanced reflections from Environment preset\n emissive: pooledEmissiveColor.clone(), // Material takes ownership of cloned color\n emissiveIntensity: 0.05,\n });\n \n return material;\n } finally {\n ThreeObjectPools.color.release(pooledBaseColor);\n ThreeObjectPools.color.release(pooledEmissiveColor);\n }\n },\n [],\n );\n\n useEffect(() => {\n return () => {\n floorMaterial.dispose();\n };\n }, [floorMaterial]);\n\n return (\n <group>\n {/* Lighting based on theme */}\n {lighting === \"cyberpunk\" && (\n <>\n {/* Base ambient light with Korean cyan tint */}\n <ambientLight intensity={0.5} color={KOREAN_COLORS.PRIMARY_CYAN} />\n\n {/* Primary directional light (moonlight) with upgraded shadows */}\n <directionalLight\n position={[15, 20, 10]}\n intensity={1.5}\n color={0xffffff}\n castShadow\n shadow-mapSize={\n scale < 1.0 ? SHADOW_MAP_SIZE_MOBILE : SHADOW_MAP_SIZE_DESKTOP\n }\n shadow-camera-far={50}\n shadow-camera-left={-20}\n shadow-camera-right={20}\n shadow-camera-top={20}\n shadow-camera-bottom={-20}\n shadow-bias={-0.0001}\n />\n\n {/* Korean neon accent lights */}\n {/* Cyan neon light (left side) */}\n <pointLight\n position={[-10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.PRIMARY_CYAN}\n />\n\n {/* Gold neon light (right side) */}\n <pointLight\n position={[10, 3, 0]}\n intensity={4}\n distance={25}\n decay={2}\n color={KOREAN_COLORS.ACCENT_GOLD}\n />\n\n {/* Blue accent light (behind) */}\n <pointLight\n position={[0, 5, -15]}\n intensity={3}\n distance={30}\n decay={2}\n color={KOREAN_COLORS.ACCENT_BLUE}\n />\n </>\n )}\n\n {lighting === \"traditional\" && (\n <>\n <ambientLight intensity={0.6} color={0xffffee} />\n <directionalLight\n position={[5, 10, 5]}\n intensity={0.8}\n castShadow\n shadow-mapSize={[2048, 2048]}\n />\n </>\n )}\n\n {lighting === \"neutral\" && (\n <>\n <ambientLight intensity={0.5} />\n <directionalLight position={[10, 10, 5]} intensity={1} castShadow />\n </>\n )}\n\n {/* Arena floor - dojang mat with reflective wet concrete aesthetic */}\n {/* Floor dimensions use pre-calculated floorWidth/floorDepth which already include FLOOR_SCALE_FACTOR */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>\n <planeGeometry args={[floorWidth, floorDepth]} />\n {lighting === \"cyberpunk\" ? (\n <primitive object={floorMaterial} attach=\"material\" />\n ) : (\n <meshPhysicalMaterial\n color={KOREAN_COLORS.UI_BACKGROUND_MEDIUM}\n roughness={0.7}\n metalness={0.1}\n clearcoat={0}\n clearcoatRoughness={0.2}\n />\n )}\n </mesh>\n\n {/* Cyberpunk grid overlay (scale-aware) */}\n <gridHelper\n ref={gridRef}\n args={[\n gridSize,\n 20,\n KOREAN_COLORS.PRIMARY_CYAN,\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n ]}\n position={[0, 0.01, 0]}\n />\n\n {/* Korean traditional boundary markers (scale-aware) */}\n {[\n [-markerDistance, 0, -markerDepth],\n [-markerDistance, 0, markerDepth],\n [markerDistance, 0, -markerDepth],\n [markerDistance, 0, markerDepth],\n ].map((pos, i) => (\n <mesh key={i} position={pos as [number, number, number]} castShadow>\n <cylinderGeometry args={[0.1 * scale, 0.15 * scale, 0.8, 8]} />\n <meshPhysicalMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n emissive={KOREAN_COLORS.ACCENT_GOLD}\n emissiveIntensity={0.5}\n metalness={0.8}\n roughness={0.2}\n clearcoat={1.0}\n />\n </mesh>\n ))}\n\n {/* Center marker - Yin Yang inspired (scale-aware) */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0]}>\n <ringGeometry args={[0.8 * scale, 1.0 * scale, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.6}\n side={THREE.DoubleSide}\n />\n </mesh>\n\n {/* Korean Signage with neon glow */}\n {lighting === \"cyberpunk\" && <KoreanSignage3D scale={scale} />}\n\n {/* Atmospheric Particles (rain/mist) */}\n {lighting === \"cyberpunk\" && enableParticles && (\n <AtmosphericParticles3D\n count={scale >= 1.0 ? 500 : 250}\n scale={scale}\n speed={2}\n />\n )}\n </group>\n );\n};\n\nexport default CombatArena3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KoreanSignage3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/KoreanSignage3D.tsx"],"sourcesContent":["/**\n * KoreanSignage3D - Three.js 3D Korean text signage with emissive glow\n *\n * Renders Korean text signs with neon emissive effects positioned around the arena\n * Creates an immersive cyberpunk Korean martial arts atmosphere\n *\n * Note: Uses drei's Text component with default Inter font which supports Korean\n * The `font` prop requires a font FILE URL (.ttf/.woff), not a CSS font-family string\n */\n\nimport { Text } from \"@react-three/drei\";\nimport React, { Suspense, useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Props for the KoreanSignage3D component.\n */\nexport interface KoreanSignage3DProps {\n /** Scale factor for signage size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n}\n\n/**\n * KoreanSignage3D Component\n * Creates Korean text signs with neon emissive glow around the combat arena\n *\n * Signs:\n * - \"전투\" (Combat) - Left wall, gold accent\n * - \"흑괘\" (Black Trigram) - Right wall, cyan accent\n * - \"급소격\" (Vital Point Strike) - Back wall, red accent\n */\nexport const KoreanSignage3D: React.FC<KoreanSignage3DProps> = ({\n scale = 1.0,\n}) => {\n const goldNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.ACCENT_GOLD,\n toneMapped: false, // Prevent tone mapping for bloom effect\n }),\n [],\n );\n\n const cyanNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.PRIMARY_CYAN,\n toneMapped: false,\n }),\n [],\n );\n\n const redNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.KOREAN_RED,\n toneMapped: false,\n }),\n [],\n );\n\n const leftWallX = -12 * scale;\n const rightWallX = 12 * scale;\n const backWallZ = -14 * scale;\n const signHeight = 5 * scale;\n const fontSize = 1.5 * scale;\n const outlineWidth = 0.05 * scale;\n\n useEffect(() => {\n return () => {\n goldNeonMaterial.dispose();\n cyanNeonMaterial.dispose();\n redNeonMaterial.dispose();\n };\n }, [goldNeonMaterial, cyanNeonMaterial, redNeonMaterial]);\n\n return (\n <Suspense fallback={null}>\n <group>\n {/* \"전투\" (Combat) sign - left wall */}\n {/* Note: material prop takes precedence over color prop in Text component */}\n {/* Note: Omitting font prop uses default Inter font which supports Korean */}\n <Text\n position={[leftWallX, signHeight, 0]}\n rotation={[0, Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.PRIMARY_CYAN}\n outlineWidth={outlineWidth}\n material={goldNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 전투\n </Text>\n\n {/* \"흑괘\" (Black Trigram) sign - right wall */}\n <Text\n position={[rightWallX, signHeight, 0]}\n rotation={[0, -Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={cyanNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 흑괘\n </Text>\n\n {/* \"급소격\" (Vital Point Strike) sign - back wall */}\n <Text\n position={[0, signHeight, backWallZ]}\n rotation={[0, 0, 0]}\n fontSize={fontSize * 0.8} // Slightly smaller for back wall\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={redNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 급소격\n </Text>\n </group>\n </Suspense>\n );\n};\n\nexport default KoreanSignage3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,mBAAmD,EAC9D,QAAQ,QACJ;CACJ,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;CACd,CAAC,GACH,CAAC,CACH;CAEA,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;CACd,CAAC,GACH,CAAC,CACH;CAEA,MAAM,kBAAkB,cAEpB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;CACd,CAAC,GACH,CAAC,CACH;CAEA,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,KAAK;CACxB,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,IAAI;CACvB,MAAM,WAAW,MAAM;CACvB,MAAM,eAAe,MAAO;CAE5B,gBAAgB;EACd,aAAa;GACX,iBAAiB,QAAQ;GACzB,iBAAiB,QAAQ;GACzB,gBAAgB,QAAQ;EAC1B;CACF,GAAG;EAAC;EAAkB;EAAkB;CAAe,CAAC;CAExD,OACE,oBAAC,UAAD;EAAU,UAAU;
|
|
1
|
+
{"version":3,"file":"KoreanSignage3D.js","names":[],"sources":["../../../../../src/components/shared/three/scene/KoreanSignage3D.tsx"],"sourcesContent":["/**\n * KoreanSignage3D - Three.js 3D Korean text signage with emissive glow\n *\n * Renders Korean text signs with neon emissive effects positioned around the arena\n * Creates an immersive cyberpunk Korean martial arts atmosphere\n *\n * Note: Uses drei's Text component with default Inter font which supports Korean\n * The `font` prop requires a font FILE URL (.ttf/.woff), not a CSS font-family string\n */\n\nimport { Text } from \"@react-three/drei\";\nimport React, { Suspense, useEffect, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Props for the KoreanSignage3D component.\n */\nexport interface KoreanSignage3DProps {\n /** Scale factor for signage size (1.0 = desktop, <1.0 = mobile). Defaults to 1.0 */\n readonly scale?: number;\n}\n\n/**\n * KoreanSignage3D Component\n * Creates Korean text signs with neon emissive glow around the combat arena\n *\n * Signs:\n * - \"전투\" (Combat) - Left wall, gold accent\n * - \"흑괘\" (Black Trigram) - Right wall, cyan accent\n * - \"급소격\" (Vital Point Strike) - Back wall, red accent\n */\nexport const KoreanSignage3D: React.FC<KoreanSignage3DProps> = ({\n scale = 1.0,\n}) => {\n const goldNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.ACCENT_GOLD,\n toneMapped: false, // Prevent tone mapping for bloom effect\n }),\n [],\n );\n\n const cyanNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.PRIMARY_CYAN,\n toneMapped: false,\n }),\n [],\n );\n\n const redNeonMaterial = useMemo(\n () =>\n new THREE.MeshBasicMaterial({\n color: KOREAN_COLORS.KOREAN_RED,\n toneMapped: false,\n }),\n [],\n );\n\n const leftWallX = -12 * scale;\n const rightWallX = 12 * scale;\n const backWallZ = -14 * scale;\n const signHeight = 5 * scale;\n const fontSize = 1.5 * scale;\n const outlineWidth = 0.05 * scale;\n\n useEffect(() => {\n return () => {\n goldNeonMaterial.dispose();\n cyanNeonMaterial.dispose();\n redNeonMaterial.dispose();\n };\n }, [goldNeonMaterial, cyanNeonMaterial, redNeonMaterial]);\n\n return (\n <Suspense fallback={null}>\n <group>\n {/* \"전투\" (Combat) sign - left wall */}\n {/* Note: material prop takes precedence over color prop in Text component */}\n {/* Note: Omitting font prop uses default Inter font which supports Korean */}\n <Text\n position={[leftWallX, signHeight, 0]}\n rotation={[0, Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.PRIMARY_CYAN}\n outlineWidth={outlineWidth}\n material={goldNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 전투\n </Text>\n\n {/* \"흑괘\" (Black Trigram) sign - right wall */}\n <Text\n position={[rightWallX, signHeight, 0]}\n rotation={[0, -Math.PI / 2, 0]}\n fontSize={fontSize}\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={cyanNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 흑괘\n </Text>\n\n {/* \"급소격\" (Vital Point Strike) sign - back wall */}\n <Text\n position={[0, signHeight, backWallZ]}\n rotation={[0, 0, 0]}\n fontSize={fontSize * 0.8} // Slightly smaller for back wall\n outlineColor={KOREAN_COLORS.ACCENT_GOLD}\n outlineWidth={outlineWidth}\n material={redNeonMaterial}\n anchorX=\"center\"\n anchorY=\"middle\"\n >\n 급소격\n </Text>\n </group>\n </Suspense>\n );\n};\n\nexport default KoreanSignage3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,mBAAmD,EAC9D,QAAQ,QACJ;CACJ,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;CACd,CAAC,GACH,CAAC,CACH;CAEA,MAAM,mBAAmB,cAErB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;CACd,CAAC,GACH,CAAC,CACH;CAEA,MAAM,kBAAkB,cAEpB,IAAI,MAAM,kBAAkB;EAC1B,OAAO,cAAc;EACrB,YAAY;CACd,CAAC,GACH,CAAC,CACH;CAEA,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,KAAK;CACxB,MAAM,YAAY,MAAM;CACxB,MAAM,aAAa,IAAI;CACvB,MAAM,WAAW,MAAM;CACvB,MAAM,eAAe,MAAO;CAE5B,gBAAgB;EACd,aAAa;GACX,iBAAiB,QAAQ;GACzB,iBAAiB,QAAQ;GACzB,gBAAgB,QAAQ;EAC1B;CACF,GAAG;EAAC;EAAkB;EAAkB;CAAe,CAAC;CAExD,OACE,oBAAC,UAAD;EAAU,UAAU;EAClB,UAAA,qBAAC,SAAD,EAAA,UAAA;GAIE,oBAAC,MAAD;IACE,UAAU;KAAC;KAAW;KAAY;IAAC;IACnC,UAAU;KAAC;KAAG,KAAK,KAAK;KAAG;IAAC;IAClB;IACV,cAAc,cAAc;IACd;IACd,UAAU;IACV,SAAQ;IACR,SAAQ;IACT,UAAA;GAEK,CAAA;GAGN,oBAAC,MAAD;IACE,UAAU;KAAC;KAAY;KAAY;IAAC;IACpC,UAAU;KAAC;KAAG,CAAC,KAAK,KAAK;KAAG;IAAC;IACnB;IACV,cAAc,cAAc;IACd;IACd,UAAU;IACV,SAAQ;IACR,SAAQ;IACT,UAAA;GAEK,CAAA;GAGN,oBAAC,MAAD;IACE,UAAU;KAAC;KAAG;KAAY;IAAS;IACnC,UAAU;KAAC;KAAG;KAAG;IAAC;IAClB,UAAU,WAAW;IACrB,cAAc,cAAc;IACd;IACd,UAAU;IACV,SAAQ;IACR,SAAQ;IACT,UAAA;GAEK,CAAA;EACD,EAAA,CAAA;CACC,CAAA;AAEd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArchetypeCard.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ArchetypeCard.tsx"],"sourcesContent":["/**\n * ArchetypeCard - Three.js-compatible character archetype card\n * \n * Displays player archetype information with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { PlayerArchetype } from \"../../../../types/common\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { PLAYER_ARCHETYPES_DATA } from \"../../../../systems/types\";\n\n/**\n * Props for ArchetypeCard component\n */\nexport interface ArchetypeCardProps {\n readonly archetype: PlayerArchetype;\n readonly onSelect?: (archetype: PlayerArchetype) => void;\n readonly isSelected?: boolean;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly showStats?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ArchetypeCard Component\n * \n * A card component for displaying player archetypes with Korean martial arts styling.\n * Includes archetype name, description, and optional stats.\n * \n * @example\n * ```tsx\n * <ArchetypeCard\n * archetype={PlayerArchetype.MUSA}\n * onSelect={(archetype) => console.log(archetype)}\n * showStats\n * />\n * ```\n */\nexport const ArchetypeCard: React.FC<ArchetypeCardProps> = ({\n archetype,\n onSelect,\n isSelected = false,\n position = [0, 0, 0],\n width = 320,\n showStats = true,\n testId,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n\n const archetypeData = useMemo(\n () => PLAYER_ARCHETYPES_DATA[archetype],\n [archetype]\n );\n\n const handleClick = useCallback(() => {\n if (onSelect) {\n onSelect(archetype);\n }\n }, [onSelect, archetype]);\n\n const handleMouseEnter = useCallback(() => {\n setIsHovered(true);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n }, []);\n\n const cardStyle = useMemo<React.CSSProperties>(() => {\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.4);\n let boxShadow = `0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.3)}`;\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)},\n 0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}\n `;\n } else if (isHovered) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.1);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.4)},\n 0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}\n `;\n }\n\n return {\n width: `${width}px`,\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"8px\",\n padding: \"16px\",\n cursor: onSelect ? \"pointer\" : \"default\",\n transition: \"all 0.3s ease\",\n boxShadow,\n transform: isHovered && onSelect ? \"translateY(-4px)\" : \"translateY(0)\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n }, [width, isSelected, isHovered, onSelect]);\n\n const headerStyle = useMemo<React.CSSProperties>(\n () => ({\n marginBottom: \"12px\",\n paddingBottom: \"8px\",\n borderBottom: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}`,\n }),\n []\n );\n\n const titleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"20px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n marginBottom: \"4px\",\n }),\n []\n );\n\n const subtitleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n fontStyle: \"italic\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n opacity: 0.8,\n }),\n []\n );\n\n const descriptionStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n lineHeight: \"1.6\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n marginBottom: showStats ? \"12px\" : \"0\",\n }),\n [showStats]\n );\n\n const statsContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"grid\",\n gridTemplateColumns: \"repeat(2, 1fr)\",\n gap: \"8px\",\n marginTop: \"12px\",\n }),\n []\n );\n\n const statItemStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n []\n );\n\n const statLabelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n fontWeight: \"bold\",\n }),\n []\n );\n\n const statValueStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"16px\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontWeight: \"bold\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n style={cardStyle}\n data-testid={testId ?? `archetype-card-${archetype}`}\n >\n {/* Header */}\n <div style={headerStyle}>\n <div style={titleStyle}>{archetypeData.name.korean}</div>\n <div style={subtitleStyle}>{archetypeData.name.english}</div>\n </div>\n\n {/* Description */}\n <div style={descriptionStyle}>\n {archetypeData.description.korean}\n </div>\n\n {/* Stats */}\n {showStats && (\n <div style={statsContainerStyle}>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>공격 | Attack</div>\n <div style={statValueStyle}>{archetypeData.stats.attackPower}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>방어 | Defense</div>\n <div style={statValueStyle}>{archetypeData.stats.defense}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>속도 | Speed</div>\n <div style={statValueStyle}>{archetypeData.stats.speed}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>기력 | Ki</div>\n <div style={statValueStyle}>{archetypeData.baseKi}</div>\n </div>\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nArchetypeCard.displayName = \"ArchetypeCard\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ArchetypeCard.js","names":[],"sources":["../../../../../src/components/shared/three/ui/ArchetypeCard.tsx"],"sourcesContent":["/**\n * ArchetypeCard - Three.js-compatible character archetype card\n * \n * Displays player archetype information with Korean theming\n * \n * @module components/three\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport { PlayerArchetype } from \"../../../../types/common\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { PLAYER_ARCHETYPES_DATA } from \"../../../../systems/types\";\n\n/**\n * Props for ArchetypeCard component\n */\nexport interface ArchetypeCardProps {\n readonly archetype: PlayerArchetype;\n readonly onSelect?: (archetype: PlayerArchetype) => void;\n readonly isSelected?: boolean;\n readonly position?: [number, number, number];\n readonly width?: number;\n readonly showStats?: boolean;\n readonly testId?: string;\n}\n\n/**\n * ArchetypeCard Component\n * \n * A card component for displaying player archetypes with Korean martial arts styling.\n * Includes archetype name, description, and optional stats.\n * \n * @example\n * ```tsx\n * <ArchetypeCard\n * archetype={PlayerArchetype.MUSA}\n * onSelect={(archetype) => console.log(archetype)}\n * showStats\n * />\n * ```\n */\nexport const ArchetypeCard: React.FC<ArchetypeCardProps> = ({\n archetype,\n onSelect,\n isSelected = false,\n position = [0, 0, 0],\n width = 320,\n showStats = true,\n testId,\n}) => {\n const [isHovered, setIsHovered] = useState(false);\n\n const archetypeData = useMemo(\n () => PLAYER_ARCHETYPES_DATA[archetype],\n [archetype]\n );\n\n const handleClick = useCallback(() => {\n if (onSelect) {\n onSelect(archetype);\n }\n }, [onSelect, archetype]);\n\n const handleMouseEnter = useCallback(() => {\n setIsHovered(true);\n }, []);\n\n const handleMouseLeave = useCallback(() => {\n setIsHovered(false);\n }, []);\n\n const cardStyle = useMemo<React.CSSProperties>(() => {\n let background = hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.95);\n let borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.4);\n let boxShadow = `0 2px 8px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.3)}`;\n\n if (isSelected) {\n background = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.15);\n borderColor = hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.9);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)},\n 0 0 20px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.4)}\n `;\n } else if (isHovered) {\n background = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.1);\n borderColor = hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.7);\n boxShadow = `\n 0 4px 12px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.4)},\n 0 0 15px ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}\n `;\n }\n\n return {\n width: `${width}px`,\n background,\n border: `2px solid ${borderColor}`,\n borderRadius: \"8px\",\n padding: \"16px\",\n cursor: onSelect ? \"pointer\" : \"default\",\n transition: \"all 0.3s ease\",\n boxShadow,\n transform: isHovered && onSelect ? \"translateY(-4px)\" : \"translateY(0)\",\n userSelect: \"none\",\n WebkitUserSelect: \"none\",\n };\n }, [width, isSelected, isHovered, onSelect]);\n\n const headerStyle = useMemo<React.CSSProperties>(\n () => ({\n marginBottom: \"12px\",\n paddingBottom: \"8px\",\n borderBottom: `1px solid ${hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.3)}`,\n }),\n []\n );\n\n const titleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"20px\",\n fontWeight: \"bold\",\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD),\n textShadow: `0 2px 4px ${hexToRgbaString(KOREAN_COLORS.BLACK_SOLID, 0.5)}`,\n marginBottom: \"4px\",\n }),\n []\n );\n\n const subtitleStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n fontStyle: \"italic\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n opacity: 0.8,\n }),\n []\n );\n\n const descriptionStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"14px\",\n lineHeight: \"1.6\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY),\n marginBottom: showStats ? \"12px\" : \"0\",\n }),\n [showStats]\n );\n\n const statsContainerStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"grid\",\n gridTemplateColumns: \"repeat(2, 1fr)\",\n gap: \"8px\",\n marginTop: \"12px\",\n }),\n []\n );\n\n const statItemStyle = useMemo<React.CSSProperties>(\n () => ({\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }),\n []\n );\n\n const statLabelStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"12px\",\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY),\n fontWeight: \"bold\",\n }),\n []\n );\n\n const statValueStyle = useMemo<React.CSSProperties>(\n () => ({\n fontFamily: FONT_FAMILY.KOREAN,\n fontSize: \"16px\",\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN),\n fontWeight: \"bold\",\n }),\n []\n );\n\n return (\n <Html position={position} center>\n <div\n onClick={handleClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n style={cardStyle}\n data-testid={testId ?? `archetype-card-${archetype}`}\n >\n {/* Header */}\n <div style={headerStyle}>\n <div style={titleStyle}>{archetypeData.name.korean}</div>\n <div style={subtitleStyle}>{archetypeData.name.english}</div>\n </div>\n\n {/* Description */}\n <div style={descriptionStyle}>\n {archetypeData.description.korean}\n </div>\n\n {/* Stats */}\n {showStats && (\n <div style={statsContainerStyle}>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>공격 | Attack</div>\n <div style={statValueStyle}>{archetypeData.stats.attackPower}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>방어 | Defense</div>\n <div style={statValueStyle}>{archetypeData.stats.defense}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>속도 | Speed</div>\n <div style={statValueStyle}>{archetypeData.stats.speed}</div>\n </div>\n <div style={statItemStyle}>\n <div style={statLabelStyle}>기력 | Ki</div>\n <div style={statValueStyle}>{archetypeData.baseKi}</div>\n </div>\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nArchetypeCard.displayName = \"ArchetypeCard\";\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BodyPartHealthDisplay.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BodyPartHealthDisplay.tsx"],"sourcesContent":["/**\n * BodyPartHealthDisplay Component - Individual body part health visualization\n *\n * Displays health bars for all 8 body parts:\n * - Head (두부)\n * - Neck (경부)\n * - Torso Upper (상부 몸통)\n * - Torso Lower (하부 몸통)\n * - Left Arm (좌팔)\n * - Right Arm (우팔)\n * - Left Leg (좌다리)\n * - Right Leg (우다리)\n *\n * @module components/shared/three/ui/BodyPartHealthDisplay\n * @category Shared UI\n * @korean 신체부위체력표시\n */\n\nimport React, { useMemo } from \"react\";\nimport { BodyPart, BodyPartHealth } from \"../../../../systems/bodypart/types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\nexport interface BodyPartHealthDisplayProps {\n /** Body part health data */\n readonly bodyPartHealth: BodyPartHealth;\n /** Player identifier for test IDs */\n readonly playerId: string;\n /** Position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * Body part names in Korean and English\n */\nconst BODY_PART_NAMES: Record<BodyPart, { korean: string; english: string }> = {\n [BodyPart.HEAD]: { korean: \"두부\", english: \"Head\" },\n [BodyPart.NECK]: { korean: \"경부\", english: \"Neck\" },\n [BodyPart.TORSO_UPPER]: { korean: \"상부\", english: \"Upper\" },\n [BodyPart.TORSO_LOWER]: { korean: \"하부\", english: \"Lower\" },\n [BodyPart.ARM_LEFT]: { korean: \"좌팔\", english: \"L.Arm\" },\n [BodyPart.ARM_RIGHT]: { korean: \"우팔\", english: \"R.Arm\" },\n [BodyPart.LEG_LEFT]: { korean: \"좌다리\", english: \"L.Leg\" },\n [BodyPart.LEG_RIGHT]: { korean: \"우다리\", english: \"R.Leg\" },\n};\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (health: number): number => {\n if (health >= 80) return KOREAN_COLORS.HEALTH_FULL;\n if (health >= 60) return KOREAN_COLORS.ACCENT_GOLD;\n if (health >= 40) return KOREAN_COLORS.WARNING_ORANGE;\n if (health >= 20) return KOREAN_COLORS.PAIN_INDICATOR;\n return KOREAN_COLORS.NEGATIVE_RED_DARK;\n};\n\n/**\n * BodyPartHealthDisplay - Shows health bars for all 8 body parts\n *\n * @example\n * ```tsx\n * <BodyPartHealthDisplay\n * bodyPartHealth={player.bodyPartHealth}\n * playerId=\"player-1\"\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BodyPartHealthDisplay: React.FC<BodyPartHealthDisplayProps> = ({\n bodyPartHealth,\n playerId,\n position,\n isMobile,\n}) => {\n const isLeft = position === \"left\";\n\n const barWidth = isMobile ? 100 : 140;\n const barHeight = isMobile ? 8 : 10;\n const fontSize = isMobile ? 9 : 10;\n const gap = isMobile ? \"4px\" : \"5px\";\n\n const bodyPartGroups = useMemo(\n () => [\n {\n label: \"상체 | Upper\",\n parts: [\n BodyPart.HEAD,\n BodyPart.NECK,\n BodyPart.TORSO_UPPER,\n BodyPart.TORSO_LOWER,\n ],\n },\n { label: \"팔 | Arms\", parts: [BodyPart.ARM_LEFT, BodyPart.ARM_RIGHT] },\n { label: \"다리 | Legs\", parts: [BodyPart.LEG_LEFT, BodyPart.LEG_RIGHT] },\n ],\n [],\n );\n\n return (\n <div\n data-testid={`body-part-health-${playerId}`}\n style={{\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n borderRadius: \"8px\",\n padding: isMobile ? \"6px\" : \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)}`,\n boxShadow: `0 0 12px ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n pointerEvents: \"none\",\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n fontSize: isMobile ? \"9px\" : \"10px\",\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: isLeft ? \"left\" : \"right\",\n marginBottom: \"2px\",\n }}\n >\n 신체 | Body Parts\n </div>\n\n {/* Body part groups */}\n {bodyPartGroups.map((group) => (\n <div\n key={group.label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"3px\",\n }}\n >\n {/* Group label - optional, can be hidden for compact display */}\n <div\n style={{\n fontSize: `${fontSize - 1}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 0.8),\n textAlign: isLeft ? \"left\" : \"right\",\n marginTop: group.label === bodyPartGroups[0].label ? \"0\" : \"4px\",\n }}\n >\n {group.label}\n </div>\n\n {/* Body parts in group */}\n {group.parts.map((part) => {\n const health = bodyPartHealth[part];\n const names = BODY_PART_NAMES[part];\n const healthColor = getHealthColor(health);\n const shouldPulse = health < 20;\n\n return (\n <div\n key={part}\n data-testid={`body-part-${playerId}-${part}`}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Body part name and value */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(healthColor, 1),\n }}\n >\n <span style={{ fontWeight: health < 40 ? \"bold\" : \"normal\" }}>\n {isMobile\n ? names.korean\n : `${names.korean} | ${names.english}`}\n </span>\n <span style={{ fontSize: `${fontSize - 1}px` }}>\n {Math.round(health)}%\n </span>\n </div>\n\n {/* Health bar */}\n <div\n style={{\n width: `${barWidth}px`,\n height: `${barHeight}px`,\n backgroundColor: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 1,\n ),\n borderRadius: \"2px\",\n overflow: \"hidden\",\n animation: shouldPulse\n ? \"healthPulse 0.8s infinite\"\n : \"none\",\n }}\n >\n <div\n style={{\n width: `${health}%`,\n height: \"100%\",\n backgroundColor: hexToRgbaString(healthColor, 1),\n transition:\n \"width 0.3s ease-in-out, background-color 0.3s ease-in-out\",\n boxShadow: `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`,\n }}\n />\n </div>\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport default BodyPartHealthDisplay;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BodyPartHealthDisplay.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BodyPartHealthDisplay.tsx"],"sourcesContent":["/**\n * BodyPartHealthDisplay Component - Individual body part health visualization\n *\n * Displays health bars for all 8 body parts:\n * - Head (두부)\n * - Neck (경부)\n * - Torso Upper (상부 몸통)\n * - Torso Lower (하부 몸통)\n * - Left Arm (좌팔)\n * - Right Arm (우팔)\n * - Left Leg (좌다리)\n * - Right Leg (우다리)\n *\n * @module components/shared/three/ui/BodyPartHealthDisplay\n * @category Shared UI\n * @korean 신체부위체력표시\n */\n\nimport React, { useMemo } from \"react\";\nimport { BodyPart, BodyPartHealth } from \"../../../../systems/bodypart/types\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\nexport interface BodyPartHealthDisplayProps {\n /** Body part health data */\n readonly bodyPartHealth: BodyPartHealth;\n /** Player identifier for test IDs */\n readonly playerId: string;\n /** Position: 'left' for player 1, 'right' for player 2 */\n readonly position: \"left\" | \"right\";\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * Body part names in Korean and English\n */\nconst BODY_PART_NAMES: Record<BodyPart, { korean: string; english: string }> = {\n [BodyPart.HEAD]: { korean: \"두부\", english: \"Head\" },\n [BodyPart.NECK]: { korean: \"경부\", english: \"Neck\" },\n [BodyPart.TORSO_UPPER]: { korean: \"상부\", english: \"Upper\" },\n [BodyPart.TORSO_LOWER]: { korean: \"하부\", english: \"Lower\" },\n [BodyPart.ARM_LEFT]: { korean: \"좌팔\", english: \"L.Arm\" },\n [BodyPart.ARM_RIGHT]: { korean: \"우팔\", english: \"R.Arm\" },\n [BodyPart.LEG_LEFT]: { korean: \"좌다리\", english: \"L.Leg\" },\n [BodyPart.LEG_RIGHT]: { korean: \"우다리\", english: \"R.Leg\" },\n};\n\n/**\n * Get health bar color based on health percentage\n */\nconst getHealthColor = (health: number): number => {\n if (health >= 80) return KOREAN_COLORS.HEALTH_FULL;\n if (health >= 60) return KOREAN_COLORS.ACCENT_GOLD;\n if (health >= 40) return KOREAN_COLORS.WARNING_ORANGE;\n if (health >= 20) return KOREAN_COLORS.PAIN_INDICATOR;\n return KOREAN_COLORS.NEGATIVE_RED_DARK;\n};\n\n/**\n * BodyPartHealthDisplay - Shows health bars for all 8 body parts\n *\n * @example\n * ```tsx\n * <BodyPartHealthDisplay\n * bodyPartHealth={player.bodyPartHealth}\n * playerId=\"player-1\"\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n */\nexport const BodyPartHealthDisplay: React.FC<BodyPartHealthDisplayProps> = ({\n bodyPartHealth,\n playerId,\n position,\n isMobile,\n}) => {\n const isLeft = position === \"left\";\n\n const barWidth = isMobile ? 100 : 140;\n const barHeight = isMobile ? 8 : 10;\n const fontSize = isMobile ? 9 : 10;\n const gap = isMobile ? \"4px\" : \"5px\";\n\n const bodyPartGroups = useMemo(\n () => [\n {\n label: \"상체 | Upper\",\n parts: [\n BodyPart.HEAD,\n BodyPart.NECK,\n BodyPart.TORSO_UPPER,\n BodyPart.TORSO_LOWER,\n ],\n },\n { label: \"팔 | Arms\", parts: [BodyPart.ARM_LEFT, BodyPart.ARM_RIGHT] },\n { label: \"다리 | Legs\", parts: [BodyPart.LEG_LEFT, BodyPart.LEG_RIGHT] },\n ],\n [],\n );\n\n return (\n <div\n data-testid={`body-part-health-${playerId}`}\n style={{\n position: \"relative\",\n display: \"flex\",\n flexDirection: \"column\",\n gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.9),\n borderRadius: \"8px\",\n padding: isMobile ? \"6px\" : \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.7)}`,\n boxShadow: `0 0 12px ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n pointerEvents: \"none\",\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n fontSize: isMobile ? \"9px\" : \"10px\",\n }}\n >\n {/* Title */}\n <div\n style={{\n fontSize: isMobile ? \"10px\" : \"11px\",\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: isLeft ? \"left\" : \"right\",\n marginBottom: \"2px\",\n }}\n >\n 신체 | Body Parts\n </div>\n\n {/* Body part groups */}\n {bodyPartGroups.map((group) => (\n <div\n key={group.label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"3px\",\n }}\n >\n {/* Group label - optional, can be hidden for compact display */}\n <div\n style={{\n fontSize: `${fontSize - 1}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 0.8),\n textAlign: isLeft ? \"left\" : \"right\",\n marginTop: group.label === bodyPartGroups[0].label ? \"0\" : \"4px\",\n }}\n >\n {group.label}\n </div>\n\n {/* Body parts in group */}\n {group.parts.map((part) => {\n const health = bodyPartHealth[part];\n const names = BODY_PART_NAMES[part];\n const healthColor = getHealthColor(health);\n const shouldPulse = health < 20;\n\n return (\n <div\n key={part}\n data-testid={`body-part-${playerId}-${part}`}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Body part name and value */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexToRgbaString(healthColor, 1),\n }}\n >\n <span style={{ fontWeight: health < 40 ? \"bold\" : \"normal\" }}>\n {isMobile\n ? names.korean\n : `${names.korean} | ${names.english}`}\n </span>\n <span style={{ fontSize: `${fontSize - 1}px` }}>\n {Math.round(health)}%\n </span>\n </div>\n\n {/* Health bar */}\n <div\n style={{\n width: `${barWidth}px`,\n height: `${barHeight}px`,\n backgroundColor: hexToRgbaString(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n 1,\n ),\n borderRadius: \"2px\",\n overflow: \"hidden\",\n animation: shouldPulse\n ? \"healthPulse 0.8s infinite\"\n : \"none\",\n }}\n >\n <div\n style={{\n width: `${health}%`,\n height: \"100%\",\n backgroundColor: hexToRgbaString(healthColor, 1),\n transition:\n \"width 0.3s ease-in-out, background-color 0.3s ease-in-out\",\n boxShadow: `0 0 8px ${hexToRgbaString(healthColor, 0.4)}`,\n }}\n />\n </div>\n </div>\n );\n })}\n </div>\n ))}\n </div>\n );\n};\n\nexport default BodyPartHealthDisplay;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,IAAM,kBAAyE;EAC5E,SAAS,OAAO;EAAE,QAAQ;EAAM,SAAS;CAAO;EAChD,SAAS,OAAO;EAAE,QAAQ;EAAM,SAAS;CAAO;EAChD,SAAS,cAAc;EAAE,QAAQ;EAAM,SAAS;CAAQ;EACxD,SAAS,cAAc;EAAE,QAAQ;EAAM,SAAS;CAAQ;EACxD,SAAS,WAAW;EAAE,QAAQ;EAAM,SAAS;CAAQ;EACrD,SAAS,YAAY;EAAE,QAAQ;EAAM,SAAS;CAAQ;EACtD,SAAS,WAAW;EAAE,QAAQ;EAAO,SAAS;CAAQ;EACtD,SAAS,YAAY;EAAE,QAAQ;EAAO,SAAS;CAAQ;AAC1D;;;;AAKA,IAAM,kBAAkB,WAA2B;CACjD,IAAI,UAAU,IAAI,OAAO,cAAc;CACvC,IAAI,UAAU,IAAI,OAAO,cAAc;CACvC,IAAI,UAAU,IAAI,OAAO,cAAc;CACvC,IAAI,UAAU,IAAI,OAAO,cAAc;CACvC,OAAO,cAAc;AACvB;;;;;;;;;;;;;;AAeA,IAAa,yBAA+D,EAC1E,gBACA,UACA,UACA,eACI;CACJ,MAAM,SAAS,aAAa;CAE5B,MAAM,WAAW,WAAW,MAAM;CAClC,MAAM,YAAY,WAAW,IAAI;CACjC,MAAM,WAAW,WAAW,IAAI;CAChC,MAAM,MAAM,WAAW,QAAQ;CAE/B,MAAM,iBAAiB,cACf;EACJ;GACE,OAAO;GACP,OAAO;IACL,SAAS;IACT,SAAS;IACT,SAAS;IACT,SAAS;GACX;EACF;EACA;GAAE,OAAO;GAAY,OAAO,CAAC,SAAS,UAAU,SAAS,SAAS;EAAE;EACpE;GAAE,OAAO;GAAa,OAAO,CAAC,SAAS,UAAU,SAAS,SAAS;EAAE;CACvE,GACA,CAAC,CACH;CAEA,OACE,qBAAC,OAAD;EACE,eAAa,oBAAoB;EACjC,OAAO;GACL,UAAU;GACV,SAAS;GACT,eAAe;GACf;GACA,iBAAiB,gBAAgB,cAAc,oBAAoB,EAAG;GACtE,cAAc;GACd,SAAS,WAAW,QAAQ;GAC5B,QAAQ,aAAa,gBAAgB,cAAc,cAAc,EAAG;GACpE,WAAW,YAAY,gBACrB,cAAc,cACd,EACF;GACA,eAAe;GACf,OAAO;GACP,WAAW;GACX,UAAU,WAAW,QAAQ;EAC/B;EAnBF,UAAA,CAsBE,oBAAC,OAAD;GACE,OAAO;IACL,UAAU,WAAW,SAAS;IAC9B,YAAY;IACZ,YAAY,YAAY;IACxB,OAAO,gBAAgB,cAAc,cAAc,CAAC;IACpD,WAAW,SAAS,SAAS;IAC7B,cAAc;GAChB;GACD,UAAA;EAEI,CAAA,GAGJ,eAAe,KAAK,UACnB,qBAAC,OAAD;GAEE,OAAO;IACL,SAAS;IACT,eAAe;IACf,KAAK;GACP;GANF,UAAA,CASE,oBAAC,OAAD;IACE,OAAO;KACL,UAAU,GAAG,WAAW,EAAE;KAC1B,YAAY,YAAY;KACxB,OAAO,gBAAgB,cAAc,gBAAgB,EAAG;KACxD,WAAW,SAAS,SAAS;KAC7B,WAAW,MAAM,UAAU,eAAe,EAAE,CAAC,QAAQ,MAAM;IAC7D;IAEC,UAAA,MAAM;GACJ,CAAA,GAGJ,MAAM,MAAM,KAAK,SAAS;IACzB,MAAM,SAAS,eAAe;IAC9B,MAAM,QAAQ,gBAAgB;IAC9B,MAAM,cAAc,eAAe,MAAM;IACzC,MAAM,cAAc,SAAS;IAE7B,OACE,qBAAC,OAAD;KAEE,eAAa,aAAa,SAAS,GAAG;KACtC,OAAO;MACL,SAAS;MACT,eAAe;MACf,KAAK;KACP;KAPF,UAAA,CAUE,qBAAC,OAAD;MACE,OAAO;OACL,SAAS;OACT,gBAAgB;OAChB,YAAY;OACZ,UAAU,GAAG,SAAS;OACtB,YAAY,YAAY;OACxB,OAAO,gBAAgB,aAAa,CAAC;MACvC;MARF,UAAA,CAUE,oBAAC,QAAD;OAAM,OAAO,EAAE,YAAY,SAAS,KAAK,SAAS,SAAS;OACxD,UAAA,WACG,MAAM,SACN,GAAG,MAAM,OAAO,KAAK,MAAM;MAC3B,CAAA,GACN,qBAAC,QAAD;OAAM,OAAO,EAAE,UAAU,GAAG,WAAW,EAAE,IAAI;OAA7C,UAAA,CACG,KAAK,MAAM,MAAM,GAAE,GAChB;MACH,CAAA,CAAA;KAGL,CAAA,GAAA,oBAAC,OAAD;MACE,OAAO;OACL,OAAO,GAAG,SAAS;OACnB,QAAQ,GAAG,UAAU;OACrB,iBAAiB,gBACf,cAAc,sBACd,CACF;OACA,cAAc;OACd,UAAU;OACV,WAAW,cACP,8BACA;MACN;MAEA,UAAA,oBAAC,OAAD,EACE,OAAO;OACL,OAAO,GAAG,OAAO;OACjB,QAAQ;OACR,iBAAiB,gBAAgB,aAAa,CAAC;OAC/C,YACE;OACF,WAAW,WAAW,gBAAgB,aAAa,EAAG;MACxD,EACD,CAAA;KACE,CAAA,CACF;IAxDE,GAAA,IAwDF;GAET,CAAC,CACE;EAxFE,GAAA,MAAM,KAwFR,CACN,CACE;;AAET"}
|
|
@@ -32,7 +32,9 @@ var BreathingIndicator = ({ player, isMobile = false }) => {
|
|
|
32
32
|
const breathingState = useMemo(() => {
|
|
33
33
|
const level = BreathingDisruptionSystem.getCurrentLevel(player);
|
|
34
34
|
const activeEffect = BreathingDisruptionSystem.getActiveEffect(player);
|
|
35
|
-
|
|
35
|
+
const timeRemaining = activeEffect ? Math.max(0, activeEffect.endTime - currentTime) : 0;
|
|
36
|
+
const isRecovering = BreathingDisruptionSystem.canRecover(player);
|
|
37
|
+
return createBreathingIndicator(level, timeRemaining, isRecovering);
|
|
36
38
|
}, [player, currentTime]);
|
|
37
39
|
if (!breathingState.visible) return null;
|
|
38
40
|
const iconSize = isMobile ? 24 : 32;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BreathingIndicator2.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BreathingIndicator.tsx"],"sourcesContent":["/**\n * BreathingIndicator Component - Visual feedback for breathing disruption status\n * \n * **Korean**: 호흡곤란 표시기\n * \n * Displays breathing difficulty with:\n * - Color-coded lungs icon (🫁)\n * - Bilingual label (Korean | English)\n * - Time remaining until recovery\n * - Pulsing animation based on severity\n */\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport {\n BreathingDisruptionSystem,\n createBreathingIndicator,\n} from \"../../../../systems/breathing\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./BreathingIndicator.css\";\n\nexport interface BreathingIndicatorProps {\n /** Player state to check for breathing disruption */\n readonly player: PlayerState;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n}\n\n/**\n * BreathingIndicator - Shows breathing disruption status with Korean-English labels\n */\nexport const BreathingIndicator: React.FC<BreathingIndicatorProps> = ({\n player,\n isMobile = false,\n}) => {\n const [currentTime, setCurrentTime] = useState(() => Date.now());\n\n useEffect(() => {\n const interval = setInterval(() => {\n setCurrentTime(Date.now());\n }, 100);\n\n return () => clearInterval(interval);\n }, []);\n\n const breathingState = useMemo(() => {\n const level = BreathingDisruptionSystem.getCurrentLevel(player);\n const activeEffect = BreathingDisruptionSystem.getActiveEffect(player);\n const timeRemaining = activeEffect\n ? Math.max(0, activeEffect.endTime - currentTime)\n : 0;\n const isRecovering = BreathingDisruptionSystem.canRecover(player);\n\n return createBreathingIndicator(level, timeRemaining, isRecovering);\n }, [player, currentTime]);\n\n if (!breathingState.visible) {\n return null;\n }\n\n const iconSize = isMobile ? 24 : 32;\n const fontSize = isMobile ? 10 : 12;\n const padding = isMobile ? \"4px 8px\" : \"6px 12px\";\n\n const secondsRemaining = Math.ceil(breathingState.timeRemaining / 1000);\n\n return (\n <div\n data-testid=\"breathing-indicator\"\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: isMobile ? \"6px\" : \"8px\",\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(breathingState.color, breathingState.opacity)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(breathingState.color, 0.5)}`,\n animation: \"breathing-pulse 1s ease-in-out infinite\",\n pointerEvents: \"none\",\n }}\n >\n {/* Lungs icon */}\n <div\n data-testid=\"breathing-icon\"\n style={{\n fontSize: `${iconSize}px`,\n lineHeight: 1,\n transform: `scale(${breathingState.scale})`,\n filter: `drop-shadow(0 0 6px ${hexToRgbaString(breathingState.color, 0.6)})`,\n }}\n >\n {breathingState.icon}\n </div>\n\n {/* Label and time */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Bilingual label */}\n <div\n data-testid=\"breathing-label\"\n style={{\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n color: hexToRgbaString(breathingState.color, 1),\n textShadow: `0 0 4px ${hexToRgbaString(breathingState.color, 0.8)}`,\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.label.korean} | {breathingState.label.english}\n </div>\n\n {/* Time remaining */}\n <div\n data-testid=\"breathing-timer\"\n style={{\n fontSize: `${fontSize - 2}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: breathingState.isRecovering\n ? hexToRgbaString(KOREAN_COLORS.POSITIVE_GREEN, 0.8)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.6),\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.isRecovering ? \"회복중 | Recovering\" : `${secondsRemaining}s`}\n </div>\n </div>\n </div>\n );\n};\n\nBreathingIndicator.displayName = \"BreathingIndicator\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,sBAAyD,EACpE,QACA,WAAW,YACP;CACJ,MAAM,CAAC,aAAa,kBAAkB,eAAe,KAAK,IAAI,CAAC;CAE/D,gBAAgB;EACd,MAAM,WAAW,kBAAkB;GACjC,eAAe,KAAK,IAAI,CAAC;EAC3B,GAAG,GAAG;EAEN,aAAa,cAAc,QAAQ;CACrC,GAAG,CAAC,CAAC;CAEL,MAAM,iBAAiB,cAAc;EACnC,MAAM,QAAQ,0BAA0B,gBAAgB,MAAM;EAC9D,MAAM,eAAe,0BAA0B,gBAAgB,MAAM;
|
|
1
|
+
{"version":3,"file":"BreathingIndicator2.js","names":[],"sources":["../../../../../src/components/shared/three/ui/BreathingIndicator.tsx"],"sourcesContent":["/**\n * BreathingIndicator Component - Visual feedback for breathing disruption status\n * \n * **Korean**: 호흡곤란 표시기\n * \n * Displays breathing difficulty with:\n * - Color-coded lungs icon (🫁)\n * - Bilingual label (Korean | English)\n * - Time remaining until recovery\n * - Pulsing animation based on severity\n */\n\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport {\n BreathingDisruptionSystem,\n createBreathingIndicator,\n} from \"../../../../systems/breathing\";\nimport { PlayerState } from \"../../../../systems/player\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport \"./BreathingIndicator.css\";\n\nexport interface BreathingIndicatorProps {\n /** Player state to check for breathing disruption */\n readonly player: PlayerState;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n}\n\n/**\n * BreathingIndicator - Shows breathing disruption status with Korean-English labels\n */\nexport const BreathingIndicator: React.FC<BreathingIndicatorProps> = ({\n player,\n isMobile = false,\n}) => {\n const [currentTime, setCurrentTime] = useState(() => Date.now());\n\n useEffect(() => {\n const interval = setInterval(() => {\n setCurrentTime(Date.now());\n }, 100);\n\n return () => clearInterval(interval);\n }, []);\n\n const breathingState = useMemo(() => {\n const level = BreathingDisruptionSystem.getCurrentLevel(player);\n const activeEffect = BreathingDisruptionSystem.getActiveEffect(player);\n const timeRemaining = activeEffect\n ? Math.max(0, activeEffect.endTime - currentTime)\n : 0;\n const isRecovering = BreathingDisruptionSystem.canRecover(player);\n\n return createBreathingIndicator(level, timeRemaining, isRecovering);\n }, [player, currentTime]);\n\n if (!breathingState.visible) {\n return null;\n }\n\n const iconSize = isMobile ? 24 : 32;\n const fontSize = isMobile ? 10 : 12;\n const padding = isMobile ? \"4px 8px\" : \"6px 12px\";\n\n const secondsRemaining = Math.ceil(breathingState.timeRemaining / 1000);\n\n return (\n <div\n data-testid=\"breathing-indicator\"\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: isMobile ? \"6px\" : \"8px\",\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.BLACK, 0.7),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(breathingState.color, breathingState.opacity)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(breathingState.color, 0.5)}`,\n animation: \"breathing-pulse 1s ease-in-out infinite\",\n pointerEvents: \"none\",\n }}\n >\n {/* Lungs icon */}\n <div\n data-testid=\"breathing-icon\"\n style={{\n fontSize: `${iconSize}px`,\n lineHeight: 1,\n transform: `scale(${breathingState.scale})`,\n filter: `drop-shadow(0 0 6px ${hexToRgbaString(breathingState.color, 0.6)})`,\n }}\n >\n {breathingState.icon}\n </div>\n\n {/* Label and time */}\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"2px\",\n }}\n >\n {/* Bilingual label */}\n <div\n data-testid=\"breathing-label\"\n style={{\n fontSize: `${fontSize}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n fontWeight: \"bold\",\n color: hexToRgbaString(breathingState.color, 1),\n textShadow: `0 0 4px ${hexToRgbaString(breathingState.color, 0.8)}`,\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.label.korean} | {breathingState.label.english}\n </div>\n\n {/* Time remaining */}\n <div\n data-testid=\"breathing-timer\"\n style={{\n fontSize: `${fontSize - 2}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: breathingState.isRecovering\n ? hexToRgbaString(KOREAN_COLORS.POSITIVE_GREEN, 0.8)\n : hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.6),\n whiteSpace: \"nowrap\",\n }}\n >\n {breathingState.isRecovering ? \"회복중 | Recovering\" : `${secondsRemaining}s`}\n </div>\n </div>\n </div>\n );\n};\n\nBreathingIndicator.displayName = \"BreathingIndicator\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,sBAAyD,EACpE,QACA,WAAW,YACP;CACJ,MAAM,CAAC,aAAa,kBAAkB,eAAe,KAAK,IAAI,CAAC;CAE/D,gBAAgB;EACd,MAAM,WAAW,kBAAkB;GACjC,eAAe,KAAK,IAAI,CAAC;EAC3B,GAAG,GAAG;EAEN,aAAa,cAAc,QAAQ;CACrC,GAAG,CAAC,CAAC;CAEL,MAAM,iBAAiB,cAAc;EACnC,MAAM,QAAQ,0BAA0B,gBAAgB,MAAM;EAC9D,MAAM,eAAe,0BAA0B,gBAAgB,MAAM;EACrE,MAAM,gBAAgB,eAClB,KAAK,IAAI,GAAG,aAAa,UAAU,WAAW,IAC9C;EACJ,MAAM,eAAe,0BAA0B,WAAW,MAAM;EAEhE,OAAO,yBAAyB,OAAO,eAAe,YAAY;CACpE,GAAG,CAAC,QAAQ,WAAW,CAAC;CAExB,IAAI,CAAC,eAAe,SAClB,OAAO;CAGT,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,YAAY;CAEvC,MAAM,mBAAmB,KAAK,KAAK,eAAe,gBAAgB,GAAI;CAEtE,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,SAAS;GACT,YAAY;GACZ,KAAK,WAAW,QAAQ;GACxB;GACA,iBAAiB,gBAAgB,cAAc,OAAO,EAAG;GACzD,cAAc;GACd,QAAQ,aAAa,gBAAgB,eAAe,OAAO,eAAe,OAAO;GACjF,WAAW,YAAY,gBAAgB,eAAe,OAAO,EAAG;GAChE,WAAW;GACX,eAAe;EACjB;EAbF,UAAA,CAgBI,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,UAAU,GAAG,SAAS;IACtB,YAAY;IACZ,WAAW,SAAS,eAAe,MAAM;IACzC,QAAQ,uBAAuB,gBAAgB,eAAe,OAAO,EAAG,EAAE;GAC5E;GAEC,UAAA,eAAe;EACb,CAAA,GAGL,qBAAC,OAAD;GACE,OAAO;IACL,SAAS;IACT,eAAe;IACf,KAAK;GACP;GALF,UAAA,CAQE,qBAAC,OAAD;IACE,eAAY;IACZ,OAAO;KACL,UAAU,GAAG,SAAS;KACtB,YAAY,YAAY;KACxB,YAAY;KACZ,OAAO,gBAAgB,eAAe,OAAO,CAAC;KAC9C,YAAY,WAAW,gBAAgB,eAAe,OAAO,EAAG;KAChE,YAAY;IACd;IATF,UAAA;KAWG,eAAe,MAAM;KAAO;KAAI,eAAe,MAAM;IACnD;GAGL,CAAA,GAAA,oBAAC,OAAD;IACE,eAAY;IACZ,OAAO;KACL,UAAU,GAAG,WAAW,EAAE;KAC1B,YAAY,YAAY;KACxB,OAAO,eAAe,eAClB,gBAAgB,cAAc,gBAAgB,EAAG,IACjD,gBAAgB,cAAc,cAAc,EAAG;KACnD,YAAY;IACd;IAEC,UAAA,eAAe,eAAe,qBAAqB,GAAG,iBAAiB;GACrE,CAAA,CACF;EACF,CAAA,CAAA;;AAEX;AAEA,mBAAmB,cAAc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CombatReadinessBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/CombatReadinessBar.tsx"],"sourcesContent":["/**\n * CombatReadinessBar Component - 10-bar segmented combat readiness display\n * \n * Displays comprehensive combat readiness with:\n * - 10 segmented bars representing 10% each\n * - Color transitions: Green (>80%), Yellow (60-79%), Orange (40-59%), Red (20-39%), Dark Red (<20%)\n * - Smooth 0.3s transition animations\n * - Korean/English bilingual labels\n * - Numeric percentage display\n * - Responsive sizing for mobile/tablet/desktop\n * - Real-time calculation from body health, pain, consciousness, and balance\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { \n calculateCombatReadiness, \n getCombatReadinessColor, \n getCombatReadinessLabel,\n getCombatReadinessBars\n} from \"../../../../utils/combatReadiness\";\nimport type { PlayerState } from \"../../../../systems/player\";\n\nexport interface CombatReadinessBarProps {\n /** Player state containing all combat factors */\n readonly player: PlayerState;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * CombatReadinessBar - 10-bar segmented combat readiness display with Korean theming\n * \n * Calculates and displays overall combat readiness from multiple factors:\n * - Body part health (40% weight)\n * - Pain level (20% weight)\n * - Consciousness (20% weight)\n * - Balance state (20% weight)\n * \n * @example\n * ```tsx\n * <CombatReadinessBar \n * player={playerState} \n * playerId=\"player-1\"\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReadinessBar: React.FC<CombatReadinessBarProps> = ({\n player,\n playerId,\n isMobile,\n}) => {\n const readiness = useMemo(\n () => calculateCombatReadiness(player),\n [player]\n );\n\n const segments = 10;\n const filledSegments = getCombatReadinessBars(readiness, segments);\n const readinessColor = getCombatReadinessColor(readiness);\n const readinessLabel = getCombatReadinessLabel(readiness);\n const shouldPulse = readiness < 20;\n\n const barWidth = isMobile ? 180 : 250;\n const barHeight = isMobile ? 16 : 20;\n const fontSize = isMobile ? 11 : 13;\n const padding = isMobile ? \"8px 12px\" : \"12px 16px\";\n\n const statusText = `${readiness}% ${readinessLabel.korean}`;\n\n return (\n <div\n data-testid={`combat-readiness-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"전투 준비도 | Combat Readiness\"\n aria-valuenow={readiness}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuetext={`${readiness}% Combat Readiness - ${readinessLabel.english}`}\n style={{\n width: `${barWidth}px`,\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }}\n >\n {/* Label and numeric/status display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n }}\n >\n <span>전투 준비도 | Combat Readiness</span>\n <span data-testid={`combat-readiness-value-${playerId}`}>\n {statusText}\n </span>\n </div>\n\n {/* 10-segment combat readiness bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`combat-readiness-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(readinessColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.3s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(readinessColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n\n {/* Breakdown tooltip (hover) - optional enhancement */}\n <div\n data-testid={`combat-readiness-breakdown-${playerId}`}\n style={{\n display: \"none\", // Hidden by default, can be shown on hover\n fontSize: `${fontSize - 2}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n marginTop: \"4px\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {/* Detailed breakdown for debugging/advanced players */}\n <div>Body: {player.bodyPartHealth ? \"tracked\" : \"aggregate\"}</div>\n <div>Pain: {Math.round(player.pain)}%</div>\n <div>Consciousness: {Math.round(player.consciousness)}%</div>\n <div>Balance: {Math.round(player.balance)}%</div>\n </div>\n </div>\n );\n};\n\nexport default CombatReadinessBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,sBAAyD,EACpE,QACA,UACA,eACI;CACJ,MAAM,YAAY,cACV,yBAAyB,MAAM,GACrC,CAAC,MAAM,CACT;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,uBAAuB,WAAW,QAAQ;CACjE,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,cAAc,YAAY;CAEhC,MAAM,WAAW,WAAW,MAAM;CAClC,MAAM,YAAY,WAAW,KAAK;CAClC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,aAAa;CAExC,MAAM,aAAa,GAAG,UAAU,IAAI,eAAe;CAEnD,OACE,qBAAC,OAAD;EACE,eAAa,wBAAwB;EACrC,MAAK;EACL,cAAW;EACX,iBAAe;EACf,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,UAAU,uBAAuB,eAAe;EACnE,OAAO;GACL,OAAO,GAAG,SAAS;GACnB;GACA,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,CAAC;GAClE,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;EACxE;
|
|
1
|
+
{"version":3,"file":"CombatReadinessBar.js","names":[],"sources":["../../../../../src/components/shared/three/ui/CombatReadinessBar.tsx"],"sourcesContent":["/**\n * CombatReadinessBar Component - 10-bar segmented combat readiness display\n * \n * Displays comprehensive combat readiness with:\n * - 10 segmented bars representing 10% each\n * - Color transitions: Green (>80%), Yellow (60-79%), Orange (40-59%), Red (20-39%), Dark Red (<20%)\n * - Smooth 0.3s transition animations\n * - Korean/English bilingual labels\n * - Numeric percentage display\n * - Responsive sizing for mobile/tablet/desktop\n * - Real-time calculation from body health, pain, consciousness, and balance\n */\n\nimport React, { useMemo } from \"react\";\nimport { KOREAN_COLORS, FONT_FAMILY } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { \n calculateCombatReadiness, \n getCombatReadinessColor, \n getCombatReadinessLabel,\n getCombatReadinessBars\n} from \"../../../../utils/combatReadiness\";\nimport type { PlayerState } from \"../../../../systems/player\";\n\nexport interface CombatReadinessBarProps {\n /** Player state containing all combat factors */\n readonly player: PlayerState;\n /** Player identifier for test ID */\n readonly playerId: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n}\n\n/**\n * CombatReadinessBar - 10-bar segmented combat readiness display with Korean theming\n * \n * Calculates and displays overall combat readiness from multiple factors:\n * - Body part health (40% weight)\n * - Pain level (20% weight)\n * - Consciousness (20% weight)\n * - Balance state (20% weight)\n * \n * @example\n * ```tsx\n * <CombatReadinessBar \n * player={playerState} \n * playerId=\"player-1\"\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatReadinessBar: React.FC<CombatReadinessBarProps> = ({\n player,\n playerId,\n isMobile,\n}) => {\n const readiness = useMemo(\n () => calculateCombatReadiness(player),\n [player]\n );\n\n const segments = 10;\n const filledSegments = getCombatReadinessBars(readiness, segments);\n const readinessColor = getCombatReadinessColor(readiness);\n const readinessLabel = getCombatReadinessLabel(readiness);\n const shouldPulse = readiness < 20;\n\n const barWidth = isMobile ? 180 : 250;\n const barHeight = isMobile ? 16 : 20;\n const fontSize = isMobile ? 11 : 13;\n const padding = isMobile ? \"8px 12px\" : \"12px 16px\";\n\n const statusText = `${readiness}% ${readinessLabel.korean}`;\n\n return (\n <div\n data-testid={`combat-readiness-bar-${playerId}`}\n role=\"progressbar\"\n aria-label=\"전투 준비도 | Combat Readiness\"\n aria-valuenow={readiness}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuetext={`${readiness}% Combat Readiness - ${readinessLabel.english}`}\n style={{\n width: `${barWidth}px`,\n padding,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 1),\n borderRadius: \"8px\",\n border: `2px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1)}`,\n boxShadow: `0 0 10px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n }}\n >\n {/* Label and numeric/status display */}\n <div\n style={{\n fontSize: `${fontSize}px`,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n fontFamily: FONT_FAMILY.KOREAN,\n marginBottom: \"4px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n fontWeight: \"bold\",\n }}\n >\n <span>전투 준비도 | Combat Readiness</span>\n <span data-testid={`combat-readiness-value-${playerId}`}>\n {statusText}\n </span>\n </div>\n\n {/* 10-segment combat readiness bar */}\n <div\n style={{\n display: \"flex\",\n gap: \"3px\",\n height: `${barHeight}px`,\n animation: shouldPulse ? \"healthPulse 0.8s infinite\" : \"none\",\n }}\n >\n {Array.from({ length: segments }).map((_, index) => (\n <div\n key={index}\n data-testid={`combat-readiness-segment-${playerId}-${index}`}\n style={{\n flex: 1,\n backgroundColor:\n index < filledSegments\n ? hexToRgbaString(readinessColor, 1)\n : hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_MEDIUM, 1),\n borderRadius: \"2px\",\n transition: \"background-color 0.3s ease-in-out\",\n boxShadow:\n index < filledSegments\n ? `0 0 8px ${hexToRgbaString(readinessColor, 0.4)}`\n : \"none\",\n }}\n />\n ))}\n </div>\n\n {/* Breakdown tooltip (hover) - optional enhancement */}\n <div\n data-testid={`combat-readiness-breakdown-${playerId}`}\n style={{\n display: \"none\", // Hidden by default, can be shown on hover\n fontSize: `${fontSize - 2}px`,\n color: hexToRgbaString(KOREAN_COLORS.TEXT_SECONDARY, 1),\n marginTop: \"4px\",\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n >\n {/* Detailed breakdown for debugging/advanced players */}\n <div>Body: {player.bodyPartHealth ? \"tracked\" : \"aggregate\"}</div>\n <div>Pain: {Math.round(player.pain)}%</div>\n <div>Consciousness: {Math.round(player.consciousness)}%</div>\n <div>Balance: {Math.round(player.balance)}%</div>\n </div>\n </div>\n );\n};\n\nexport default CombatReadinessBar;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,sBAAyD,EACpE,QACA,UACA,eACI;CACJ,MAAM,YAAY,cACV,yBAAyB,MAAM,GACrC,CAAC,MAAM,CACT;CAEA,MAAM,WAAW;CACjB,MAAM,iBAAiB,uBAAuB,WAAW,QAAQ;CACjE,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,iBAAiB,wBAAwB,SAAS;CACxD,MAAM,cAAc,YAAY;CAEhC,MAAM,WAAW,WAAW,MAAM;CAClC,MAAM,YAAY,WAAW,KAAK;CAClC,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,UAAU,WAAW,aAAa;CAExC,MAAM,aAAa,GAAG,UAAU,IAAI,eAAe;CAEnD,OACE,qBAAC,OAAD;EACE,eAAa,wBAAwB;EACrC,MAAK;EACL,cAAW;EACX,iBAAe;EACf,iBAAe;EACf,iBAAe;EACf,kBAAgB,GAAG,UAAU,uBAAuB,eAAe;EACnE,OAAO;GACL,OAAO,GAAG,SAAS;GACnB;GACA,iBAAiB,gBAAgB,cAAc,oBAAoB,CAAC;GACpE,cAAc;GACd,QAAQ,aAAa,gBAAgB,cAAc,cAAc,CAAC;GAClE,WAAW,YAAY,gBAAgB,cAAc,cAAc,EAAG;EACxE;EAfF,UAAA;GAkBE,qBAAC,OAAD;IACE,OAAO;KACL,UAAU,GAAG,SAAS;KACtB,OAAO,gBAAgB,cAAc,cAAc,CAAC;KACpD,YAAY,YAAY;KACxB,cAAc;KACd,SAAS;KACT,gBAAgB;KAChB,YAAY;IACd;IATF,UAAA,CAWE,oBAAC,QAAD,EAAA,UAAM,4BAA+B,CAAA,GACrC,oBAAC,QAAD;KAAM,eAAa,0BAA0B;KAC1C,UAAA;IACG,CAAA,CACH;;GAGL,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,KAAK;KACL,QAAQ,GAAG,UAAU;KACrB,WAAW,cAAc,8BAA8B;IACzD;IAEC,UAAA,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC,CAAC,CAAC,KAAK,GAAG,UACxC,oBAAC,OAAD;KAEE,eAAa,4BAA4B,SAAS,GAAG;KACrD,OAAO;MACL,MAAM;MACN,iBACE,QAAQ,iBACJ,gBAAgB,gBAAgB,CAAC,IACjC,gBAAgB,cAAc,sBAAsB,CAAC;MAC3D,cAAc;MACd,YAAY;MACZ,WACE,QAAQ,iBACJ,WAAW,gBAAgB,gBAAgB,EAAG,MAC9C;KACR;IACD,GAfM,KAeN,CACF;GACE,CAAA;GAGL,qBAAC,OAAD;IACE,eAAa,8BAA8B;IAC3C,OAAO;KACL,SAAS;KACT,UAAU,GAAG,WAAW,EAAE;KAC1B,OAAO,gBAAgB,cAAc,gBAAgB,CAAC;KACtD,WAAW;KACX,YAAY,YAAY;IAC1B;IARF,UAAA;KAWE,qBAAC,OAAD,EAAA,UAAA,CAAK,UAAO,OAAO,iBAAiB,YAAY,WAAiB,EAAA,CAAA;KACjE,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAO,KAAK,MAAM,OAAO,IAAI;MAAE;KAAM,EAAA,CAAA;KAC1C,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAgB,KAAK,MAAM,OAAO,aAAa;MAAE;KAAM,EAAA,CAAA;KAC5D,qBAAC,OAAD,EAAA,UAAA;MAAK;MAAU,KAAK,MAAM,OAAO,OAAO;MAAE;KAAM,EAAA,CAAA;IAC7C;;EACF;;AAET"}
|