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":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n setOpacity(1);\n setScale(1);\n } else {\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ActionFeedback.js","names":[],"sources":["../../../../../src/components/shared/three/effects/ActionFeedback.tsx"],"sourcesContent":["/**\n * ActionFeedback - Combat action feedback display component\n *\n * Displays action indicators like \"Perfect!\", \"Critical!\", \"Blocked\", \"Dodged\",\n * and technique names with Korean-English bilingual text.\n *\n * Uses Html overlay from @react-three/drei for rendering within 3D scenes.\n *\n * @module components/shared/three/effects/ActionFeedback\n * @category Shared Effects\n * @korean 액션피드백\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport {\n ActionFeedback as ActionFeedbackData,\n ActionFeedbackType,\n} from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/** Fade in completes at 20% of total duration */\nconst FADE_IN_THRESHOLD = 0.2;\n/** Fade out begins at 80% of total duration */\nconst FADE_OUT_THRESHOLD = 0.8;\n\n/**\n * Props for the ActionFeedback component\n */\nexport interface ActionFeedbackProps {\n /** Array of action feedbacks to display */\n readonly feedbacks: readonly ActionFeedbackData[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1200) */\n readonly animationDuration?: number;\n}\n\n/**\n * Props for technique name display\n */\nexport interface TechniqueNameProps {\n /** Korean technique name */\n readonly korean: string;\n /** English technique name */\n readonly english: string;\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Animation duration in ms */\n readonly duration?: number;\n /** Callback when animation completes */\n readonly onComplete?: () => void;\n}\n\n/**\n * Get color based on feedback type\n */\nfunction getFeedbackColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"blocked\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN);\n case \"dodged\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GREEN);\n case \"technique\":\n return hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n case \"combo_milestone\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n default:\n return hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY);\n }\n}\n\n/**\n * Get glow color based on feedback type\n */\nfunction getGlowColor(type: ActionFeedbackType): string {\n switch (type) {\n case \"perfect\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"blocked\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_CYAN, 0.6);\n case \"dodged\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GREEN, 0.6);\n case \"technique\":\n return hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n case \"combo_milestone\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n default:\n return hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.4);\n }\n}\n\n/**\n * Individual action feedback display\n */\ninterface SingleFeedbackProps {\n readonly feedback: ActionFeedbackData;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleFeedback: React.FC<SingleFeedbackProps> = ({\n feedback,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(feedback.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, feedback.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, feedback.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2.5 + progress * 1.5; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + (progress < 0.2 ? progress * 2 : (1 - progress) * 0.5);\n const fontSize = isMobile ? 18 : 24;\n const color = getFeedbackColor(feedback.type);\n const glowColor = getGlowColor(feedback.type);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`action-feedback-${feedback.id}`}\n style={{\n fontSize: `${fontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${glowColor},\n 0 0 20px ${glowColor},\n 2px 2px 4px rgba(0, 0, 0, 0.9)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n textAlign: \"center\",\n }}\n >\n {feedback.textKorean} | {feedback.text}\n </div>\n </Html>\n );\n};\n\n/**\n * ActionFeedback Component\n *\n * Renders multiple action feedback indicators in the 3D scene.\n * Each indicator floats upward and fades out over time.\n *\n * @example\n * ```tsx\n * <ActionFeedback\n * feedbacks={actionFeedbacks}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nexport const ActionFeedback: React.FC<ActionFeedbackProps> = ({\n feedbacks,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1200,\n}) => {\n const visibleFeedbacks = useMemo(() => [...feedbacks], [feedbacks]);\n\n return (\n <group data-testid=\"action-feedback-container\">\n {visibleFeedbacks.map((feedback) => (\n <SingleFeedback\n key={feedback.id}\n feedback={feedback}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * TechniqueName Component\n *\n * Displays the current technique name in Korean and English.\n * Appears at the center of the screen with a dramatic animation.\n *\n * @example\n * ```tsx\n * <TechniqueName\n * korean=\"천둥벽력\"\n * english=\"Thunder Strike\"\n * isMobile={isMobile}\n * duration={2000}\n * />\n * ```\n */\nexport const TechniqueName: React.FC<TechniqueNameProps> = ({\n korean,\n english,\n isMobile = false,\n duration = 2000,\n onComplete,\n}) => {\n const [opacity, setOpacity] = useState(0);\n const [scale, setScale] = useState(0.5);\n const [startTime] = useState(() => Date.now());\n const startTimeRef = useRef(startTime);\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1);\n\n if (progress < FADE_IN_THRESHOLD) {\n const fadeInProgress = progress / FADE_IN_THRESHOLD;\n setOpacity(fadeInProgress);\n setScale(0.5 + fadeInProgress * 0.5);\n } else if (progress < FADE_OUT_THRESHOLD) {\n setOpacity(1);\n setScale(1);\n } else {\n const fadeOutProgress =\n (progress - FADE_OUT_THRESHOLD) / (1 - FADE_OUT_THRESHOLD);\n setOpacity(1 - fadeOutProgress);\n setScale(1 + fadeOutProgress * 0.2);\n }\n\n if (progress >= 1 && onComplete) {\n onComplete();\n }\n });\n\n const position3D: [number, number, number] = [0, 3.5, 0];\n\n const mainFontSize = isMobile ? 28 : 42;\n const subFontSize = isMobile ? 16 : 24;\n const color = hexColorToCSS(KOREAN_COLORS.SECONDARY_MAGENTA);\n const glowColor = hexToRgbaString(KOREAN_COLORS.SECONDARY_MAGENTA, 0.8);\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid=\"technique-name\"\n style={{\n textAlign: \"center\",\n opacity,\n transform: `scale(${scale})`,\n transition: \"transform 0.1s ease-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: `${mainFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color,\n textShadow: `\n 0 0 15px ${glowColor},\n 0 0 30px ${glowColor},\n 3px 3px 6px rgba(0, 0, 0, 0.9)\n `,\n letterSpacing: \"4px\",\n }}\n >\n {korean}\n </div>\n\n {/* Divider */}\n <div\n style={{\n width: \"60px\",\n height: \"2px\",\n background: `linear-gradient(90deg, transparent, ${color}, transparent)`,\n margin: \"8px auto\",\n }}\n />\n\n {/* English name */}\n <div\n style={{\n fontSize: `${subFontSize}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n textShadow: \"2px 2px 4px rgba(0, 0, 0, 0.8)\",\n letterSpacing: \"2px\",\n textTransform: \"uppercase\",\n }}\n >\n {english}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default ActionFeedback;\n"],"mappings":"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"}
|
|
@@ -121,9 +121,10 @@ SingleDamageNumber.displayName = "SingleDamageNumber";
|
|
|
121
121
|
* ```
|
|
122
122
|
*/
|
|
123
123
|
var DamageNumbersComponent = ({ damages, isMobile = false, arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS, animationDuration = 1500 }) => {
|
|
124
|
+
const visibleDamages = useMemo(() => [...damages], [damages]);
|
|
124
125
|
return /* @__PURE__ */ jsx("group", {
|
|
125
126
|
"data-testid": "damage-numbers-container",
|
|
126
|
-
children:
|
|
127
|
+
children: visibleDamages.map((damage) => /* @__PURE__ */ jsx(SingleDamageNumber, {
|
|
127
128
|
damage,
|
|
128
129
|
isMobile,
|
|
129
130
|
arenaBounds,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DamageNumbers.js","names":[],"sources":["../../../../../src/components/shared/three/effects/DamageNumbers.tsx"],"sourcesContent":["/**\n * DamageNumbers - Floating damage number display component\n *\n * Displays floating damage numbers that animate upward and fade out.\n * Color-coded based on damage type: normal (cyan), critical (gold), vital (red).\n *\n * Uses Html overlays from @react-three/drei for rendering within 3D scenes.\n * Performance optimized with React.memo to reduce unnecessary re-renders.\n *\n * @module components/shared/three/effects/DamageNumbers\n * @category Shared Effects\n * @korean 피해숫자\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { DamageNumber, DamageType } from \"../../../../hooks/useActionFeedback\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\nimport { hexColorToCSS, hexToRgbaString } from \"../../../../utils/colorUtils\";\nimport { withGPUAcceleration } from \"../../../../utils/performanceOptimization\";\n\n/**\n * Props for the DamageNumbers component\n */\nexport interface DamageNumbersProps {\n /** Array of damage numbers to display */\n readonly damages: readonly DamageNumber[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile?: boolean;\n /** Arena bounds for 3D positioning (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n /** Duration of animation in ms (default: 1500) */\n readonly animationDuration?: number;\n}\n\n/**\n * Get color based on damage type\n */\nfunction getDamageColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case \"vital\":\n return hexColorToCSS(KOREAN_COLORS.ACCENT_RED);\n case \"normal\":\n default:\n return hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN);\n }\n}\n\n/**\n * Get glow color based on damage type\n */\nfunction getGlowColor(type: DamageType): string {\n switch (type) {\n case \"critical\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8);\n case \"vital\":\n return hexToRgbaString(KOREAN_COLORS.ACCENT_RED, 0.8);\n case \"normal\":\n default:\n return hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.8);\n }\n}\n\n/**\n * Individual damage number display\n * Memoized to prevent unnecessary re-renders\n */\ninterface SingleDamageNumberProps {\n readonly damage: DamageNumber;\n readonly isMobile: boolean;\n readonly arenaBounds: PhysicsArenaBounds;\n readonly animationDuration: number;\n}\n\nconst SingleDamageNumber = React.memo<SingleDamageNumberProps>(({\n damage,\n isMobile,\n arenaBounds,\n animationDuration,\n}) => {\n const [progress, setProgress] = useState(0);\n const startTimeRef = useRef(damage.timestamp);\n\n const halfWidth = arenaBounds.worldWidthMeters / 2;\n const halfDepth = arenaBounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, damage.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, damage.position.y));\n \n const x = clampedX; // Meter position X\n const y = 2 + progress * 2; // Float upward\n const z = clampedZ; // Meter position Z (depth)\n const position3D: [number, number, number] = [x, y, z];\n\n useFrame(() => {\n const elapsed = Date.now() - startTimeRef.current;\n const newProgress = Math.min(elapsed / animationDuration, 1);\n setProgress(newProgress);\n });\n\n if (progress >= 1) return null;\n\n const opacity = 1 - progress;\n const scale = 1 + progress * 0.3; // Slight scale up during animation\n const fontSize = isMobile ? 20 : 28;\n const getCriticalBonus = (): number => {\n if (damage.type === \"critical\") return 8;\n if (damage.type === \"vital\") return 4;\n return 0;\n };\n const criticalBonus = getCriticalBonus();\n\n return (\n <Html\n position={position3D}\n center\n distanceFactor={10}\n style={{ pointerEvents: \"none\" }}\n >\n <div\n data-testid={`damage-${damage.id}`}\n style={withGPUAcceleration({\n fontSize: `${fontSize + criticalBonus}px`,\n fontWeight: \"bold\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: getDamageColor(damage.type),\n opacity,\n transform: `scale(${scale})`,\n textShadow: `\n 0 0 10px ${getGlowColor(damage.type)},\n 0 0 20px ${getGlowColor(damage.type)},\n 2px 2px 4px rgba(0, 0, 0, 0.8)\n `,\n whiteSpace: \"nowrap\",\n userSelect: \"none\",\n })}\n >\n {damage.damage}\n {damage.type === \"critical\" && \"!\"}\n {damage.type === \"vital\" && \"!!\"}\n </div>\n </Html>\n );\n}, (prevProps, nextProps) => {\n const prevArena = prevProps.arenaBounds;\n const nextArena = nextProps.arenaBounds;\n\n const sameArenaBounds =\n prevArena?.x === nextArena?.x &&\n prevArena?.y === nextArena?.y &&\n prevArena?.width === nextArena?.width &&\n prevArena?.height === nextArena?.height &&\n prevArena?.worldWidthMeters === nextArena?.worldWidthMeters &&\n prevArena?.worldDepthMeters === nextArena?.worldDepthMeters;\n\n return (\n prevProps.damage.id === nextProps.damage.id &&\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n sameArenaBounds\n );\n});\n\nSingleDamageNumber.displayName = \"SingleDamageNumber\";\n\n/**\n * DamageNumbers Component\n *\n * Renders multiple floating damage numbers in the 3D scene.\n * Each number floats upward and fades out over time.\n * Performance optimized with React.memo.\n *\n * @example\n * ```tsx\n * <DamageNumbers\n * damages={damageNumbers}\n * isMobile={isMobile}\n * arenaBounds={arenaBounds}\n * />\n * ```\n */\nconst DamageNumbersComponent: React.FC<DamageNumbersProps> = ({\n damages,\n isMobile = false,\n arenaBounds = DEFAULT_PHYSICS_ARENA_BOUNDS,\n animationDuration = 1500,\n}) => {\n const visibleDamages = useMemo(() => [...damages], [damages]);\n\n return (\n <group data-testid=\"damage-numbers-container\">\n {visibleDamages.map((damage) => (\n <SingleDamageNumber\n key={damage.id}\n damage={damage}\n isMobile={isMobile}\n arenaBounds={arenaBounds}\n animationDuration={animationDuration}\n />\n ))}\n </group>\n );\n};\n\n/**\n * Memoized DamageNumbers with custom comparison\n * Only re-renders when damage array changes\n */\nexport const DamageNumbers = React.memo(\n DamageNumbersComponent,\n (prevProps, nextProps) => {\n if (prevProps.damages.length !== nextProps.damages.length) {\n return false;\n }\n \n for (let i = 0; i < prevProps.damages.length; i++) {\n if (prevProps.damages[i].id !== nextProps.damages[i].id) {\n return false;\n }\n }\n \n return (\n prevProps.isMobile === nextProps.isMobile &&\n prevProps.animationDuration === nextProps.animationDuration &&\n prevProps.arenaBounds?.x === nextProps.arenaBounds?.x &&\n prevProps.arenaBounds?.y === nextProps.arenaBounds?.y &&\n prevProps.arenaBounds?.width === nextProps.arenaBounds?.width &&\n prevProps.arenaBounds?.height === nextProps.arenaBounds?.height\n );\n }\n);\n\nDamageNumbers.displayName = \"DamageNumbers\";\n\nexport default DamageNumbers;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,eAAe,MAA0B;CAChD,QAAQ,MAAR;EACE,KAAK,YACH,OAAO,cAAc,cAAc,WAAW;EAChD,KAAK,SACH,OAAO,cAAc,cAAc,UAAU;EAE/C,SACE,OAAO,cAAc,cAAc,YAAY;CACnD;AACF;;;;AAKA,SAAS,aAAa,MAA0B;CAC9C,QAAQ,MAAR;EACE,KAAK,YACH,OAAO,gBAAgB,cAAc,aAAa,EAAG;EACvD,KAAK,SACH,OAAO,gBAAgB,cAAc,YAAY,EAAG;EAEtD,SACE,OAAO,gBAAgB,cAAc,cAAc,EAAG;CAC1D;AACF;AAaA,IAAM,qBAAqB,MAAM,MAA+B,EAC9D,QACA,UACA,aACA,wBACI;CACJ,MAAM,CAAC,UAAU,eAAe,SAAS,CAAC;CAC1C,MAAM,eAAe,OAAO,OAAO,SAAS;CAE5C,MAAM,YAAY,YAAY,mBAAmB;CACjD,MAAM,YAAY,YAAY,mBAAmB;CAEjD,MAAM,WAAW,KAAK,IAAI,WAAW,KAAK,IAAI,CAAC,WAAW,OAAO,SAAS,CAAC,CAAC;CAC5E,MAAM,WAAW,KAAK,IAAI,WAAW,KAAK,IAAI,CAAC,WAAW,OAAO,SAAS,CAAC,CAAC;CAK5E,MAAM,aAAuC;EAAC;EAFpC,IAAI,WAAW;EAE2B;CAAC;CAErD,eAAe;EACb,MAAM,UAAU,KAAK,IAAI,IAAI,aAAa;EAC1C,MAAM,cAAc,KAAK,IAAI,UAAU,mBAAmB,CAAC;EAC3D,YAAY,WAAW;CACzB,CAAC;CAED,IAAI,YAAY,GAAG,OAAO;CAE1B,MAAM,UAAU,IAAI;CACpB,MAAM,QAAQ,IAAI,WAAW;CAC7B,MAAM,WAAW,WAAW,KAAK;CACjC,MAAM,yBAAiC;EACrC,IAAI,OAAO,SAAS,YAAY,OAAO;EACvC,IAAI,OAAO,SAAS,SAAS,OAAO;EACpC,OAAO;CACT;CACA,MAAM,gBAAgB,iBAAiB;CAEvC,OACE,oBAAC,MAAD;EACE,UAAU;EACV,QAAA;EACA,gBAAgB;EAChB,OAAO,EAAE,eAAe,OAAO;EAE/B,UAAA,qBAAC,OAAD;GACE,eAAa,UAAU,OAAO;GAC9B,OAAO,oBAAoB;IACzB,UAAU,GAAG,WAAW,cAAc;IACtC,YAAY;IACZ,YAAY,YAAY;IACxB,OAAO,eAAe,OAAO,IAAI;IACjC;IACA,WAAW,SAAS,MAAM;IAC1B,YAAY;uBACC,aAAa,OAAO,IAAI,EAAE;uBAC1B,aAAa,OAAO,IAAI,EAAE;;;IAGvC,YAAY;IACZ,YAAY;GACd,CAAC;GAhBH,UAAA;IAkBG,OAAO;IACP,OAAO,SAAS,cAAc;IAC9B,OAAO,SAAS,WAAW;GACzB;;CACD,CAAA;AAEV,IAAI,WAAW,cAAc;CAC3B,MAAM,YAAY,UAAU;CAC5B,MAAM,YAAY,UAAU;CAE5B,MAAM,kBACJ,WAAW,MAAM,WAAW,KAC5B,WAAW,MAAM,WAAW,KAC5B,WAAW,UAAU,WAAW,SAChC,WAAW,WAAW,WAAW,UACjC,WAAW,qBAAqB,WAAW,oBAC3C,WAAW,qBAAqB,WAAW;CAE7C,OACE,UAAU,OAAO,OAAO,UAAU,OAAO,MACzC,UAAU,aAAa,UAAU,YACjC,UAAU,sBAAsB,UAAU,qBAC1C;AAEJ,CAAC;AAED,mBAAmB,cAAc;;;;;;;;;;;;;;;;;AAkBjC,IAAM,0BAAwD,EAC5D,SACA,WAAW,OACX,cAAc,8BACd,oBAAoB,WAChB;CACJ,MAAM,iBAAiB,cAAc,CAAC,GAAG,OAAO,GAAG,CAAC,OAAO,CAAC;CAE5D,OACE,oBAAC,SAAD;EAAO,eAAY;EAChB,UAAA,eAAe,KAAK,WACnB,oBAAC,oBAAD;GAEU;GACE;GACG;GACM;EACpB,GALM,OAAO,EAKb,CACF;CACI,CAAA;AAEX;;;;;AAMA,IAAa,gBAAgB,MAAM,KACjC,yBACC,WAAW,cAAc;CACxB,IAAI,UAAU,QAAQ,WAAW,UAAU,QAAQ,QACjD,OAAO;CAGT,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,QAAQ,KAC5C,IAAI,UAAU,QAAQ,EAAE,CAAC,OAAO,UAAU,QAAQ,EAAE,CAAC,IACnD,OAAO;CAIX,OACE,UAAU,aAAa,UAAU,YACjC,UAAU,sBAAsB,UAAU,qBAC1C,UAAU,aAAa,MAAM,UAAU,aAAa,KACpD,UAAU,aAAa,MAAM,UAAU,aAAa,KACpD,UAAU,aAAa,UAAU,UAAU,aAAa,SACxD,UAAU,aAAa,WAAW,UAAU,aAAa;AAE7D,CACF;AAEA,cAAc,cAAc"}
|
|
@@ -406,14 +406,15 @@ var HitEffects3D = ({ effects, onEffectComplete, arenaBounds }) => {
|
|
|
406
406
|
}
|
|
407
407
|
});
|
|
408
408
|
});
|
|
409
|
-
|
|
409
|
+
const effectsToRender = useMemo(() => {
|
|
410
410
|
return effects.map((effect) => {
|
|
411
411
|
return {
|
|
412
412
|
effect,
|
|
413
413
|
effectRef: effectRefsSnapshot.get(effect.id)
|
|
414
414
|
};
|
|
415
415
|
}).filter((item) => item.effectRef !== void 0);
|
|
416
|
-
}, [effects, effectRefsSnapshot])
|
|
416
|
+
}, [effects, effectRefsSnapshot]);
|
|
417
|
+
return /* @__PURE__ */ jsx("group", { children: effectsToRender.map(({ effect, effectRef }) => {
|
|
417
418
|
return /* @__PURE__ */ jsx(HitEffectVisual, {
|
|
418
419
|
effect,
|
|
419
420
|
effectRef,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HitEffects3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/HitEffects3D.tsx"],"sourcesContent":["/**\n * HitEffects3D - Three.js particle effects for combat\n *\n * Maintains Korean theming and visual feedback for combat actions\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { HitEffect } from \"../../../../systems\";\nimport { HitEffectType } from \"../../../../systems/effects\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\n\n/**\n * Props for the HitEffects3D component.\n * Controls which effects are displayed and callbacks for effect lifecycle.\n */\nexport interface HitEffects3DProps {\n /** Array of active hit effects to render in the scene */\n readonly effects: HitEffect[];\n /** Callback invoked when an effect completes its duration */\n readonly onEffectComplete?: (effectId: string) => void;\n /** Arena bounds for accurate coordinate conversion (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n}\n\ninterface ActiveEffect extends HitEffect {\n progress: number;\n}\n\n/**\n * Individual Hit Effect Component\n * Renders a single effect with Three.js primitives\n */\nconst HitEffectVisual: React.FC<{\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n arenaBounds?: PhysicsArenaBounds;\n}> = ({ effect, effectRef, arenaBounds }) => {\n const groupRef = useRef<THREE.Group>(null);\n const alphaRef = useRef(1);\n\n const position3D: [number, number, number] = useMemo(() => {\n if (!effect.position) return [0, 1, 0];\n\n const bounds = arenaBounds ?? DEFAULT_PHYSICS_ARENA_BOUNDS;\n \n const halfWidth = bounds.worldWidthMeters / 2;\n const halfDepth = bounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, effect.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, effect.position.y));\n \n const x = clampedX; // Meter position X\n const y = 1.5; // Mid-height for effects\n const z = clampedZ; // Meter position Z (depth)\n\n return [x, y, z];\n }, [effect.position, arenaBounds]);\n\n useFrame(() => {\n if (!groupRef.current || !effectRef.current) return;\n\n const progress = effectRef.current.progress;\n alphaRef.current = 1 - progress;\n\n let sparkIndex = 0;\n groupRef.current.traverse((object) => {\n if (object instanceof THREE.Mesh && object.material instanceof THREE.MeshBasicMaterial) {\n const baseOpacity = object.material.userData.baseOpacity ?? 1;\n object.material.opacity = alphaRef.current * baseOpacity;\n \n if (effect.type === HitEffectType.BLOCK && object.geometry instanceof THREE.SphereGeometry) {\n if (object.geometry.parameters?.radius === 0.05) { // Spark particles have radius 0.05\n const i = sparkIndex++;\n if (i < 3) { // Only update the 3 spark particles\n object.position.y = Math.sin((1 - alphaRef.current) * Math.PI) * 0.3;\n }\n }\n }\n }\n });\n\n if (\n effect.type === HitEffectType.COUNTER ||\n effect.type === HitEffectType.VITAL_POINT_STRIKE\n ) {\n groupRef.current.rotation.y += 0.1;\n }\n\n if (effect.type === HitEffectType.CRITICAL_HIT) {\n const pulse =\n 1 + Math.sin(effectRef.current.progress * Math.PI * 4) * 0.2;\n groupRef.current.scale.set(pulse, pulse, pulse);\n }\n });\n\n switch (effect.type) {\n case HitEffectType.HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Impact flash sphere */}\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Expanding ring */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry\n args={[0.3 * effect.intensity, 0.35 * effect.intensity, 32]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.CRITICAL_HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Large impact sphere */}\n <mesh>\n <sphereGeometry args={[0.5 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.7}\n userData={{ baseOpacity: 0.7 }}\n />\n </mesh>\n {/* Star burst lines */}\n {[0, 1, 2, 3].map((i) => {\n const angle = (i * Math.PI) / 2;\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.3, 0, Math.sin(angle) * 0.3]}\n rotation={[0, angle, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.BLOCK:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Shield arc */}\n <mesh rotation={[0, 0, Math.PI / 2]}>\n <torusGeometry\n args={[0.4 * effect.intensity, 0.05, 8, 16, Math.PI]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Spark particles - positions will be updated in useFrame */}\n {[0, 1, 2].map((i) => (\n <mesh\n key={i}\n position={[\n (i - 1) * 0.2,\n 0,\n 0,\n ]}\n >\n <sphereGeometry args={[0.05, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.MISS:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Swish trail lines */}\n {[0, 1].map((i) => (\n <mesh\n key={i}\n position={[(i - 0.5) * 0.2, i * 0.1, 0]}\n rotation={[0, 0, (i - 0.5) * 0.3]}\n >\n <boxGeometry args={[0.6, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.TEXT_TERTIARY}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.VITAL_POINT_STRIKE:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Pulsing sphere */}\n <mesh>\n <sphereGeometry args={[0.35 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Concentric rings */}\n {[0.2, 0.3, 0.4].map((radius, i) => (\n <mesh key={i} rotation={[-Math.PI / 2, 0, 0]}>\n <ringGeometry args={[radius, radius + 0.02, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n {/* Crosshair */}\n <mesh position={[0, 0, 0]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n <mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.PARRY:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Deflection arc */}\n <mesh>\n <torusGeometry\n args={[0.35 * effect.intensity, 0.05, 8, 16, Math.PI / 2]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Sparks */}\n {[0, 1, 2].map((i) => {\n const angle = (Math.PI / 6) * (i - 1);\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.4, Math.sin(angle) * 0.4, 0]}\n >\n <sphereGeometry args={[0.04, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.COUNTER:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Spinning energy blades */}\n {[0, 1, 2, 3].map((i) => (\n <mesh\n key={i}\n rotation={[0, (i * Math.PI) / 2, 0]}\n position={[0, 0, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.GENERAL_DAMAGE:\n case HitEffectType.STATUS_EFFECT:\n default:\n return (\n <group ref={groupRef} position={position3D}>\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GREEN}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n </group>\n );\n }\n};\n\n/**\n * HitEffects3D Component\n * Manages all active hit effects in the combat scene\n * Uses refs to avoid triggering React re-renders at 60fps\n */\nexport const HitEffects3D: React.FC<HitEffects3DProps> = ({\n effects,\n onEffectComplete,\n arenaBounds,\n}) => {\n const effectRefsMap = useRef<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const [effectRefsSnapshot, setEffectRefsSnapshot] = useState<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n\n useEffect(() => {\n const currentIdSet = new Set(effects.map((e) => e.id));\n effectRefsMap.current.forEach((_ref, id) => {\n if (!currentIdSet.has(id)) {\n effectRefsMap.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n\n effects.forEach((effect) => {\n if (!effectRefsMap.current.has(effect.id)) {\n effectRefsMap.current.set(effect.id, {\n current: { ...effect, progress: 0 },\n });\n }\n });\n\n setEffectRefsSnapshot(new Map(effectRefsMap.current));\n }, [effects]);\n\n useFrame(() => {\n const now = Date.now();\n\n effectRefsMap.current.forEach((ref, id) => {\n if (!ref.current) return;\n\n const progress = Math.min(\n (now - ref.current.startTime) / ref.current.duration,\n 1\n );\n ref.current.progress = progress;\n\n const isExpired = progress >= 1;\n if (\n isExpired &&\n onEffectComplete &&\n !completedEffectsRef.current.has(id)\n ) {\n completedEffectsRef.current.add(id);\n onEffectComplete(id);\n }\n });\n });\n\n const effectsToRender = useMemo(() => {\n return effects\n .map((effect) => {\n const effectRef = effectRefsSnapshot.get(effect.id);\n return { effect, effectRef };\n })\n .filter(\n (\n item\n ): item is {\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n } => item.effectRef !== undefined\n );\n }, [effects, effectRefsSnapshot]);\n\n return (\n <group>\n {effectsToRender.map(({ effect, effectRef }) => {\n return (\n <HitEffectVisual\n key={effect.id}\n effect={effect}\n effectRef={effectRef}\n arenaBounds={arenaBounds}\n />\n );\n })}\n </group>\n );\n};\n\nexport default HitEffects3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HitEffects3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/HitEffects3D.tsx"],"sourcesContent":["/**\n * HitEffects3D - Three.js particle effects for combat\n *\n * Maintains Korean theming and visual feedback for combat actions\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { HitEffect } from \"../../../../systems\";\nimport { HitEffectType } from \"../../../../systems/effects\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { DEFAULT_PHYSICS_ARENA_BOUNDS, type PhysicsArenaBounds } from \"../../../../types/PhysicsTypes\";\n\n/**\n * Props for the HitEffects3D component.\n * Controls which effects are displayed and callbacks for effect lifecycle.\n */\nexport interface HitEffects3DProps {\n /** Array of active hit effects to render in the scene */\n readonly effects: HitEffect[];\n /** Callback invoked when an effect completes its duration */\n readonly onEffectComplete?: (effectId: string) => void;\n /** Arena bounds for accurate coordinate conversion (physics-first with meter dimensions) */\n readonly arenaBounds?: PhysicsArenaBounds;\n}\n\ninterface ActiveEffect extends HitEffect {\n progress: number;\n}\n\n/**\n * Individual Hit Effect Component\n * Renders a single effect with Three.js primitives\n */\nconst HitEffectVisual: React.FC<{\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n arenaBounds?: PhysicsArenaBounds;\n}> = ({ effect, effectRef, arenaBounds }) => {\n const groupRef = useRef<THREE.Group>(null);\n const alphaRef = useRef(1);\n\n const position3D: [number, number, number] = useMemo(() => {\n if (!effect.position) return [0, 1, 0];\n\n const bounds = arenaBounds ?? DEFAULT_PHYSICS_ARENA_BOUNDS;\n \n const halfWidth = bounds.worldWidthMeters / 2;\n const halfDepth = bounds.worldDepthMeters / 2;\n \n const clampedX = Math.min(halfWidth, Math.max(-halfWidth, effect.position.x));\n const clampedZ = Math.min(halfDepth, Math.max(-halfDepth, effect.position.y));\n \n const x = clampedX; // Meter position X\n const y = 1.5; // Mid-height for effects\n const z = clampedZ; // Meter position Z (depth)\n\n return [x, y, z];\n }, [effect.position, arenaBounds]);\n\n useFrame(() => {\n if (!groupRef.current || !effectRef.current) return;\n\n const progress = effectRef.current.progress;\n alphaRef.current = 1 - progress;\n\n let sparkIndex = 0;\n groupRef.current.traverse((object) => {\n if (object instanceof THREE.Mesh && object.material instanceof THREE.MeshBasicMaterial) {\n const baseOpacity = object.material.userData.baseOpacity ?? 1;\n object.material.opacity = alphaRef.current * baseOpacity;\n \n if (effect.type === HitEffectType.BLOCK && object.geometry instanceof THREE.SphereGeometry) {\n if (object.geometry.parameters?.radius === 0.05) { // Spark particles have radius 0.05\n const i = sparkIndex++;\n if (i < 3) { // Only update the 3 spark particles\n object.position.y = Math.sin((1 - alphaRef.current) * Math.PI) * 0.3;\n }\n }\n }\n }\n });\n\n if (\n effect.type === HitEffectType.COUNTER ||\n effect.type === HitEffectType.VITAL_POINT_STRIKE\n ) {\n groupRef.current.rotation.y += 0.1;\n }\n\n if (effect.type === HitEffectType.CRITICAL_HIT) {\n const pulse =\n 1 + Math.sin(effectRef.current.progress * Math.PI * 4) * 0.2;\n groupRef.current.scale.set(pulse, pulse, pulse);\n }\n });\n\n switch (effect.type) {\n case HitEffectType.HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Impact flash sphere */}\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Expanding ring */}\n <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry\n args={[0.3 * effect.intensity, 0.35 * effect.intensity, 32]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.CRITICAL_HIT:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Large impact sphere */}\n <mesh>\n <sphereGeometry args={[0.5 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.7}\n userData={{ baseOpacity: 0.7 }}\n />\n </mesh>\n {/* Star burst lines */}\n {[0, 1, 2, 3].map((i) => {\n const angle = (i * Math.PI) / 2;\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.3, 0, Math.sin(angle) * 0.3]}\n rotation={[0, angle, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_RED}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.BLOCK:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Shield arc */}\n <mesh rotation={[0, 0, Math.PI / 2]}>\n <torusGeometry\n args={[0.4 * effect.intensity, 0.05, 8, 16, Math.PI]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Spark particles - positions will be updated in useFrame */}\n {[0, 1, 2].map((i) => (\n <mesh\n key={i}\n position={[\n (i - 1) * 0.2,\n 0,\n 0,\n ]}\n >\n <sphereGeometry args={[0.05, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_CYAN}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.MISS:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Swish trail lines */}\n {[0, 1].map((i) => (\n <mesh\n key={i}\n position={[(i - 0.5) * 0.2, i * 0.1, 0]}\n rotation={[0, 0, (i - 0.5) * 0.3]}\n >\n <boxGeometry args={[0.6, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.TEXT_TERTIARY}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.VITAL_POINT_STRIKE:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Pulsing sphere */}\n <mesh>\n <sphereGeometry args={[0.35 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n {/* Concentric rings */}\n {[0.2, 0.3, 0.4].map((radius, i) => (\n <mesh key={i} rotation={[-Math.PI / 2, 0, 0]}>\n <ringGeometry args={[radius, radius + 0.02, 32]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={1}\n side={THREE.DoubleSide}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n {/* Crosshair */}\n <mesh position={[0, 0, 0]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n <mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]}>\n <boxGeometry args={[0.8, 0.02, 0.02]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.SECONDARY_MAGENTA}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n </group>\n );\n\n case HitEffectType.PARRY:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Deflection arc */}\n <mesh>\n <torusGeometry\n args={[0.35 * effect.intensity, 0.05, 8, 16, Math.PI / 2]}\n />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n {/* Sparks */}\n {[0, 1, 2].map((i) => {\n const angle = (Math.PI / 6) * (i - 1);\n return (\n <mesh\n key={i}\n position={[Math.cos(angle) * 0.4, Math.sin(angle) * 0.4, 0]}\n >\n <sphereGeometry args={[0.04, 8, 8]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GOLD}\n transparent\n opacity={0.8}\n userData={{ baseOpacity: 0.8 }}\n />\n </mesh>\n );\n })}\n </group>\n );\n\n case HitEffectType.COUNTER:\n return (\n <group ref={groupRef} position={position3D}>\n {/* Spinning energy blades */}\n {[0, 1, 2, 3].map((i) => (\n <mesh\n key={i}\n rotation={[0, (i * Math.PI) / 2, 0]}\n position={[0, 0, 0]}\n >\n <boxGeometry args={[0.6, 0.05, 0.05]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.PRIMARY_CYAN}\n transparent\n opacity={1}\n userData={{ baseOpacity: 1.0 }}\n />\n </mesh>\n ))}\n </group>\n );\n\n case HitEffectType.GENERAL_DAMAGE:\n case HitEffectType.STATUS_EFFECT:\n default:\n return (\n <group ref={groupRef} position={position3D}>\n <mesh>\n <sphereGeometry args={[0.3 * effect.intensity, 16, 16]} />\n <meshBasicMaterial\n color={KOREAN_COLORS.ACCENT_GREEN}\n transparent\n opacity={0.5}\n userData={{ baseOpacity: 0.5 }}\n />\n </mesh>\n </group>\n );\n }\n};\n\n/**\n * HitEffects3D Component\n * Manages all active hit effects in the combat scene\n * Uses refs to avoid triggering React re-renders at 60fps\n */\nexport const HitEffects3D: React.FC<HitEffects3DProps> = ({\n effects,\n onEffectComplete,\n arenaBounds,\n}) => {\n const effectRefsMap = useRef<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const [effectRefsSnapshot, setEffectRefsSnapshot] = useState<\n Map<string, React.MutableRefObject<ActiveEffect | null>>\n >(new Map());\n\n useEffect(() => {\n const currentIdSet = new Set(effects.map((e) => e.id));\n effectRefsMap.current.forEach((_ref, id) => {\n if (!currentIdSet.has(id)) {\n effectRefsMap.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n\n effects.forEach((effect) => {\n if (!effectRefsMap.current.has(effect.id)) {\n effectRefsMap.current.set(effect.id, {\n current: { ...effect, progress: 0 },\n });\n }\n });\n\n setEffectRefsSnapshot(new Map(effectRefsMap.current));\n }, [effects]);\n\n useFrame(() => {\n const now = Date.now();\n\n effectRefsMap.current.forEach((ref, id) => {\n if (!ref.current) return;\n\n const progress = Math.min(\n (now - ref.current.startTime) / ref.current.duration,\n 1\n );\n ref.current.progress = progress;\n\n const isExpired = progress >= 1;\n if (\n isExpired &&\n onEffectComplete &&\n !completedEffectsRef.current.has(id)\n ) {\n completedEffectsRef.current.add(id);\n onEffectComplete(id);\n }\n });\n });\n\n const effectsToRender = useMemo(() => {\n return effects\n .map((effect) => {\n const effectRef = effectRefsSnapshot.get(effect.id);\n return { effect, effectRef };\n })\n .filter(\n (\n item\n ): item is {\n effect: HitEffect;\n effectRef: React.MutableRefObject<ActiveEffect | null>;\n } => item.effectRef !== undefined\n );\n }, [effects, effectRefsSnapshot]);\n\n return (\n <group>\n {effectsToRender.map(({ effect, effectRef }) => {\n return (\n <HitEffectVisual\n key={effect.id}\n effect={effect}\n effectRef={effectRef}\n arenaBounds={arenaBounds}\n />\n );\n })}\n </group>\n );\n};\n\nexport default HitEffects3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayerStateIndicators.js","names":[],"sources":["../../../../../src/components/shared/three/effects/PlayerStateIndicators.tsx"],"sourcesContent":["/**\n * PlayerStateIndicators - Html overlay for player stats\n * \n * Displays health, stamina, Ki, balance state, and other combat metrics\n * as an Html overlay above the 3D player model.\n * \n * @module components/three/PlayerStateIndicators\n * @category 3D Components\n * @korean 플레이어상태표시기\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport type { PlayerStateIndicatorsProps, BalanceState } from \"../../../../types/player-visual\";\nimport { toHexColor } from \"../../../../utils/colorHelpers\";\n\n/**\n * Get color for balance state\n * \n * @param balance - Current balance state\n * @returns CSS color string\n * @korean 균형색상가져오기\n */\nconst getBalanceColor = (balance: BalanceState): string => {\n switch (balance) {\n case \"READY\":\n return \"#00cc44\"; // Green - ready for combat\n case \"SHAKEN\":\n return \"#ffcc00\"; // Yellow - slightly compromised\n case \"VULNERABLE\":\n return \"#ff8800\"; // Orange - significantly exposed\n case \"HELPLESS\":\n return \"#cc0000\"; // Red - complete vulnerability\n default:\n return \"#00cc44\";\n }\n};\n\n/**\n * Get Korean text for balance state\n * \n * @param balance - Current balance state\n * @returns Korean text\n * @korean 균형한글가져오기\n */\nconst getBalanceText = (balance: BalanceState): string => {\n switch (balance) {\n case \"READY\":\n return \"준비완료\";\n case \"SHAKEN\":\n return \"동요상태\";\n case \"VULNERABLE\":\n return \"취약상태\";\n case \"HELPLESS\":\n return \"무력상태\";\n default:\n return \"준비완료\";\n }\n};\n\n/**\n * PlayerStateIndicators Component\n * \n * Renders an Html overlay with health bar, stamina bar, Ki indicator,\n * balance state, and consciousness level.\n * \n * @example\n * ```tsx\n * <PlayerStateIndicators\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * balance=\"READY\"\n * consciousness={100}\n * isMobile={false}\n * />\n * ```\n * \n * @korean 플레이어상태표시기컴포넌트\n */\nexport const PlayerStateIndicators: React.FC<PlayerStateIndicatorsProps> = ({\n health,\n maxHealth,\n stamina,\n ki,\n balance,\n consciousness,\n pain = 0,\n bloodLoss = 0,\n isMobile,\n}) => {\n const healthPercent = useMemo(\n () => Math.max(0, Math.min(100, (health / maxHealth) * 100)),\n [health, maxHealth]\n );\n\n const staminaPercent = useMemo(\n () => Math.max(0, Math.min(100, stamina)),\n [stamina]\n );\n\n const kiPercent = useMemo(() => Math.max(0, Math.min(100, ki)), [ki]);\n\n const consciousnessPercent = useMemo(\n () => Math.max(0, Math.min(100, consciousness)),\n [consciousness]\n );\n\n const sizing = useMemo(\n () => ({\n width: isMobile ? \"60px\" : \"80px\",\n barHeight: isMobile ? \"4px\" : \"6px\",\n thinBarHeight: isMobile ? \"3px\" : \"4px\",\n fontSize: isMobile ? \"8px\" : \"10px\",\n gap: isMobile ? \"2px\" : \"4px\",\n }),\n [isMobile]\n );\n\n const healthColor = useMemo(() => {\n if (healthPercent > 50) return \"#00ff00\"; // Green\n if (healthPercent > 25) return \"#ffff00\"; // Yellow\n return \"#ff0000\"; // Red\n }, [healthPercent]);\n\n const balanceColor = useMemo(() => getBalanceColor(balance), [balance]);\n const balanceTextKorean = useMemo(() => getBalanceText(balance), [balance]);\n\n return (\n <Html\n position={[0, 2.5, 0]}\n center\n distanceFactor={isMobile ? 15 : 10}\n occlude={false}\n style={{ pointerEvents: \"none\", userSelect: \"none\" }}\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: sizing.gap,\n minWidth: sizing.width,\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n data-testid=\"player-state-indicators\"\n >\n {/* Health bar */}\n <div\n style={{\n height: sizing.barHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }}\n data-testid=\"health-bar\"\n title={`Health: ${health}/${maxHealth}`}\n >\n <div\n style={{\n width: `${healthPercent}%`,\n height: \"100%\",\n background: healthColor,\n transition: \"width 0.3s ease, background-color 0.3s ease\",\n }}\n />\n </div>\n\n {/* Stamina bar */}\n <div\n style={{\n height: sizing.thinBarHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.ACCENT_GOLD)}`,\n }}\n data-testid=\"stamina-bar\"\n title={`Stamina: ${stamina}%`}\n >\n <div\n style={{\n width: `${staminaPercent}%`,\n height: \"100%\",\n background: toHexColor(KOREAN_COLORS.ACCENT_GOLD),\n transition: \"width 0.3s ease\",\n }}\n />\n </div>\n\n {/* Ki bar */}\n <div\n style={{\n height: sizing.thinBarHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }}\n data-testid=\"ki-bar\"\n title={`Ki: ${ki}%`}\n >\n <div\n style={{\n width: `${kiPercent}%`,\n height: \"100%\",\n background: toHexColor(KOREAN_COLORS.PRIMARY_CYAN),\n transition: \"width 0.3s ease\",\n boxShadow:\n kiPercent > 80\n ? `0 0 ${isMobile ? \"4px\" : \"6px\"} ${toHexColor(KOREAN_COLORS.PRIMARY_CYAN)}`\n : \"none\",\n }}\n />\n </div>\n\n {/* Balance state indicator */}\n <div\n style={{\n fontSize: sizing.fontSize,\n color: balanceColor,\n textAlign: \"center\",\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n padding: \"2px 4px\",\n background: \"rgba(0,0,0,0.4)\",\n borderRadius: \"2px\",\n }}\n data-testid=\"balance-indicator\"\n title={`Balance: ${balance}`}\n >\n {balanceTextKorean}\n </div>\n\n {/* Consciousness indicator (if below 100%) */}\n {consciousnessPercent < 100 && (\n <div\n style={{\n height: sizing.thinBarHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.CONSCIOUSNESS_PURPLE)}`,\n }}\n data-testid=\"consciousness-bar\"\n title={`Consciousness: ${consciousness}%`}\n >\n <div\n style={{\n width: `${consciousnessPercent}%`,\n height: \"100%\",\n background: toHexColor(KOREAN_COLORS.CONSCIOUSNESS_PURPLE),\n transition: \"width 0.3s ease\",\n }}\n />\n </div>\n )}\n\n {/* Pain indicator (if above 20%) */}\n {pain > 20 && (\n <div\n style={{\n fontSize: sizing.fontSize,\n color: toHexColor(KOREAN_COLORS.PAIN_INDICATOR),\n textAlign: \"center\",\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n data-testid=\"pain-indicator\"\n title={`Pain: ${pain}%`}\n >\n 통증 {Math.round(pain)}%\n </div>\n )}\n\n {/* Blood loss indicator (if above 10%) */}\n {bloodLoss && bloodLoss > 10 && (\n <div\n style={{\n fontSize: sizing.fontSize,\n color: toHexColor(KOREAN_COLORS.BLOODLOSS_INDICATOR),\n textAlign: \"center\",\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n data-testid=\"bloodloss-indicator\"\n title={`Blood Loss: ${bloodLoss}%`}\n >\n 출혈 {Math.round(bloodLoss)}%\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nexport default PlayerStateIndicators;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlayerStateIndicators.js","names":[],"sources":["../../../../../src/components/shared/three/effects/PlayerStateIndicators.tsx"],"sourcesContent":["/**\n * PlayerStateIndicators - Html overlay for player stats\n * \n * Displays health, stamina, Ki, balance state, and other combat metrics\n * as an Html overlay above the 3D player model.\n * \n * @module components/three/PlayerStateIndicators\n * @category 3D Components\n * @korean 플레이어상태표시기\n */\n\nimport { Html } from \"@react-three/drei\";\nimport React, { useMemo } from \"react\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\nimport type { PlayerStateIndicatorsProps, BalanceState } from \"../../../../types/player-visual\";\nimport { toHexColor } from \"../../../../utils/colorHelpers\";\n\n/**\n * Get color for balance state\n * \n * @param balance - Current balance state\n * @returns CSS color string\n * @korean 균형색상가져오기\n */\nconst getBalanceColor = (balance: BalanceState): string => {\n switch (balance) {\n case \"READY\":\n return \"#00cc44\"; // Green - ready for combat\n case \"SHAKEN\":\n return \"#ffcc00\"; // Yellow - slightly compromised\n case \"VULNERABLE\":\n return \"#ff8800\"; // Orange - significantly exposed\n case \"HELPLESS\":\n return \"#cc0000\"; // Red - complete vulnerability\n default:\n return \"#00cc44\";\n }\n};\n\n/**\n * Get Korean text for balance state\n * \n * @param balance - Current balance state\n * @returns Korean text\n * @korean 균형한글가져오기\n */\nconst getBalanceText = (balance: BalanceState): string => {\n switch (balance) {\n case \"READY\":\n return \"준비완료\";\n case \"SHAKEN\":\n return \"동요상태\";\n case \"VULNERABLE\":\n return \"취약상태\";\n case \"HELPLESS\":\n return \"무력상태\";\n default:\n return \"준비완료\";\n }\n};\n\n/**\n * PlayerStateIndicators Component\n * \n * Renders an Html overlay with health bar, stamina bar, Ki indicator,\n * balance state, and consciousness level.\n * \n * @example\n * ```tsx\n * <PlayerStateIndicators\n * health={85}\n * maxHealth={100}\n * stamina={60}\n * ki={40}\n * balance=\"READY\"\n * consciousness={100}\n * isMobile={false}\n * />\n * ```\n * \n * @korean 플레이어상태표시기컴포넌트\n */\nexport const PlayerStateIndicators: React.FC<PlayerStateIndicatorsProps> = ({\n health,\n maxHealth,\n stamina,\n ki,\n balance,\n consciousness,\n pain = 0,\n bloodLoss = 0,\n isMobile,\n}) => {\n const healthPercent = useMemo(\n () => Math.max(0, Math.min(100, (health / maxHealth) * 100)),\n [health, maxHealth]\n );\n\n const staminaPercent = useMemo(\n () => Math.max(0, Math.min(100, stamina)),\n [stamina]\n );\n\n const kiPercent = useMemo(() => Math.max(0, Math.min(100, ki)), [ki]);\n\n const consciousnessPercent = useMemo(\n () => Math.max(0, Math.min(100, consciousness)),\n [consciousness]\n );\n\n const sizing = useMemo(\n () => ({\n width: isMobile ? \"60px\" : \"80px\",\n barHeight: isMobile ? \"4px\" : \"6px\",\n thinBarHeight: isMobile ? \"3px\" : \"4px\",\n fontSize: isMobile ? \"8px\" : \"10px\",\n gap: isMobile ? \"2px\" : \"4px\",\n }),\n [isMobile]\n );\n\n const healthColor = useMemo(() => {\n if (healthPercent > 50) return \"#00ff00\"; // Green\n if (healthPercent > 25) return \"#ffff00\"; // Yellow\n return \"#ff0000\"; // Red\n }, [healthPercent]);\n\n const balanceColor = useMemo(() => getBalanceColor(balance), [balance]);\n const balanceTextKorean = useMemo(() => getBalanceText(balance), [balance]);\n\n return (\n <Html\n position={[0, 2.5, 0]}\n center\n distanceFactor={isMobile ? 15 : 10}\n occlude={false}\n style={{ pointerEvents: \"none\", userSelect: \"none\" }}\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: sizing.gap,\n minWidth: sizing.width,\n fontFamily: FONT_FAMILY.KOREAN,\n }}\n data-testid=\"player-state-indicators\"\n >\n {/* Health bar */}\n <div\n style={{\n height: sizing.barHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }}\n data-testid=\"health-bar\"\n title={`Health: ${health}/${maxHealth}`}\n >\n <div\n style={{\n width: `${healthPercent}%`,\n height: \"100%\",\n background: healthColor,\n transition: \"width 0.3s ease, background-color 0.3s ease\",\n }}\n />\n </div>\n\n {/* Stamina bar */}\n <div\n style={{\n height: sizing.thinBarHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.ACCENT_GOLD)}`,\n }}\n data-testid=\"stamina-bar\"\n title={`Stamina: ${stamina}%`}\n >\n <div\n style={{\n width: `${staminaPercent}%`,\n height: \"100%\",\n background: toHexColor(KOREAN_COLORS.ACCENT_GOLD),\n transition: \"width 0.3s ease\",\n }}\n />\n </div>\n\n {/* Ki bar */}\n <div\n style={{\n height: sizing.thinBarHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }}\n data-testid=\"ki-bar\"\n title={`Ki: ${ki}%`}\n >\n <div\n style={{\n width: `${kiPercent}%`,\n height: \"100%\",\n background: toHexColor(KOREAN_COLORS.PRIMARY_CYAN),\n transition: \"width 0.3s ease\",\n boxShadow:\n kiPercent > 80\n ? `0 0 ${isMobile ? \"4px\" : \"6px\"} ${toHexColor(KOREAN_COLORS.PRIMARY_CYAN)}`\n : \"none\",\n }}\n />\n </div>\n\n {/* Balance state indicator */}\n <div\n style={{\n fontSize: sizing.fontSize,\n color: balanceColor,\n textAlign: \"center\",\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n padding: \"2px 4px\",\n background: \"rgba(0,0,0,0.4)\",\n borderRadius: \"2px\",\n }}\n data-testid=\"balance-indicator\"\n title={`Balance: ${balance}`}\n >\n {balanceTextKorean}\n </div>\n\n {/* Consciousness indicator (if below 100%) */}\n {consciousnessPercent < 100 && (\n <div\n style={{\n height: sizing.thinBarHeight,\n background: \"rgba(0,0,0,0.6)\",\n borderRadius: \"2px\",\n overflow: \"hidden\",\n border: `1px solid ${toHexColor(KOREAN_COLORS.CONSCIOUSNESS_PURPLE)}`,\n }}\n data-testid=\"consciousness-bar\"\n title={`Consciousness: ${consciousness}%`}\n >\n <div\n style={{\n width: `${consciousnessPercent}%`,\n height: \"100%\",\n background: toHexColor(KOREAN_COLORS.CONSCIOUSNESS_PURPLE),\n transition: \"width 0.3s ease\",\n }}\n />\n </div>\n )}\n\n {/* Pain indicator (if above 20%) */}\n {pain > 20 && (\n <div\n style={{\n fontSize: sizing.fontSize,\n color: toHexColor(KOREAN_COLORS.PAIN_INDICATOR),\n textAlign: \"center\",\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n data-testid=\"pain-indicator\"\n title={`Pain: ${pain}%`}\n >\n 통증 {Math.round(pain)}%\n </div>\n )}\n\n {/* Blood loss indicator (if above 10%) */}\n {bloodLoss && bloodLoss > 10 && (\n <div\n style={{\n fontSize: sizing.fontSize,\n color: toHexColor(KOREAN_COLORS.BLOODLOSS_INDICATOR),\n textAlign: \"center\",\n fontWeight: \"bold\",\n textShadow: \"0 0 4px rgba(0,0,0,0.8)\",\n }}\n data-testid=\"bloodloss-indicator\"\n title={`Blood Loss: ${bloodLoss}%`}\n >\n 출혈 {Math.round(bloodLoss)}%\n </div>\n )}\n </div>\n </Html>\n );\n};\n\nexport default PlayerStateIndicators;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StanceSymbol3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/StanceSymbol3D.tsx"],"sourcesContent":["/**\n * StanceSymbol3D - Floating trigram symbol above player\n *\n * Displays the Unicode trigram symbol (☰☱☲☳☴☵☶☷) floating above the player's head,\n * with rotation animation and pulsing glow effect. Provides immediate visual feedback\n * of the current stance to the player and observers.\n *\n * @module components/three/StanceSymbol3D\n * @category 3D Components\n * @korean 자세기호3D\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { FONT_FAMILY } from \"../../../../types/constants\";\nimport {\n getStanceColorHex,\n getStanceKoreanName,\n getTrigramSymbol,\n} from \"../../../../utils/stanceHelpers\";\n\n/**\n * Props for StanceSymbol3D component\n */\nexport interface StanceSymbol3DProps {\n /** Current trigram stance to display */\n readonly stance: TrigramStance;\n /** Height offset above player (default: 2.5) */\n readonly heightOffset?: number;\n /** Whether symbol should rotate */\n readonly animated?: boolean;\n /** Symbol scale multiplier */\n readonly scale?: number;\n /** Show Korean name below symbol */\n readonly showName?: boolean;\n}\n\n/**\n * Animation constants for stance symbol\n */\nconst ANIMATION_CONSTANTS = {\n ROTATION_SPEED: 0.5,\n BOB_AMPLITUDE: 0.1,\n BOB_FREQUENCY: 2,\n} as const;\n\n/**\n * StanceSymbol3D Component\n *\n * Renders a floating trigram symbol above the player with:\n * - Rotation animation\n * - Pulsing glow effect\n * - Stance-specific coloring\n * - Optional Korean name display\n *\n * Uses Html from @react-three/drei for crisp text rendering that always faces camera.\n *\n * @example\n * ```tsx\n * <StanceSymbol3D\n * stance={TrigramStance.GEON}\n * heightOffset={2.5}\n * animated={true}\n * showName={true}\n * />\n * ```\n */\nexport const StanceSymbol3D: React.FC<StanceSymbol3DProps> = ({\n stance,\n heightOffset = 2.5,\n animated = true,\n scale = 1.0,\n showName = true,\n}) => {\n const groupRef = useRef<THREE.Group>(null);\n\n const symbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n const koreanName = useMemo(() => getStanceKoreanName(stance), [stance]);\n const colorHex = useMemo(() => getStanceColorHex(stance), [stance]);\n\n useFrame((state) => {\n if (!animated || !groupRef.current) return;\n\n const time = state.clock.elapsedTime;\n\n groupRef.current.rotation.y = time * ANIMATION_CONSTANTS.ROTATION_SPEED;\n\n groupRef.current.position.y =\n heightOffset +\n Math.sin(time * ANIMATION_CONSTANTS.BOB_FREQUENCY) *\n ANIMATION_CONSTANTS.BOB_AMPLITUDE;\n });\n\n return (\n <group\n ref={groupRef}\n position={[0, heightOffset, 0]}\n name=\"stance-symbol-3d\"\n >\n {/* Interactive Light Source - illuminates player head/shoulders */}\n <pointLight color={colorHex} intensity={2.5} distance={4} decay={2} />\n\n {/* Trigram symbol with glow effect */}\n <Html\n center\n distanceFactor={10}\n zIndexRange={[100, 0]}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n }}\n >\n {/* Main trigram symbol */}\n <div\n style={{\n fontSize: `${48 * scale}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: colorHex,\n textShadow: `\n 0 0 10px ${colorHex},\n 0 0 20px ${colorHex},\n 0 0 30px ${colorHex}\n `,\n fontWeight: \"bold\",\n lineHeight: \"1\",\n animation: \"pulse 2s ease-in-out infinite\",\n }}\n data-testid=\"trigram-symbol\"\n >\n {symbol}\n </div>\n\n {/* Korean name below symbol */}\n {showName && (\n <div\n style={{\n fontSize: `${16 * scale}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: colorHex,\n textShadow: `0 0 5px ${colorHex}`,\n fontWeight: \"bold\",\n letterSpacing: \"2px\",\n }}\n data-testid=\"stance-korean-name\"\n >\n {koreanName}\n </div>\n )}\n </div>\n\n {/* CSS animation for pulse effect */}\n <style>\n {`\n @keyframes pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.8; transform: scale(1.1); }\n }\n `}\n </style>\n </Html>\n </group>\n );\n};\n\nexport default StanceSymbol3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA2CA,IAAM,sBAAsB;CAC1B,gBAAgB;CAChB,eAAe;CACf,eAAe;AACjB;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,kBAAiD,EAC5D,QACA,eAAe,KACf,WAAW,MACX,QAAQ,GACR,WAAW,WACP;CACJ,MAAM,WAAW,OAAoB,IAAI;CAEzC,MAAM,SAAS,cAAc,iBAAiB,MAAM,GAAG,CAAC,MAAM,CAAC;CAC/D,MAAM,aAAa,cAAc,oBAAoB,MAAM,GAAG,CAAC,MAAM,CAAC;CACtE,MAAM,WAAW,cAAc,kBAAkB,MAAM,GAAG,CAAC,MAAM,CAAC;CAElE,UAAU,UAAU;EAClB,IAAI,CAAC,YAAY,CAAC,SAAS,SAAS;EAEpC,MAAM,OAAO,MAAM,MAAM;EAEzB,SAAS,QAAQ,SAAS,IAAI,OAAO,oBAAoB;EAEzD,SAAS,QAAQ,SAAS,IACxB,eACA,KAAK,IAAI,OAAO,oBAAoB,aAAa,IAC/C,oBAAoB;CAC1B,CAAC;CAED,OACE,qBAAC,SAAD;EACE,KAAK;EACL,UAAU;GAAC;GAAG;GAAc;EAAC;EAC7B,MAAK;
|
|
1
|
+
{"version":3,"file":"StanceSymbol3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/StanceSymbol3D.tsx"],"sourcesContent":["/**\n * StanceSymbol3D - Floating trigram symbol above player\n *\n * Displays the Unicode trigram symbol (☰☱☲☳☴☵☶☷) floating above the player's head,\n * with rotation animation and pulsing glow effect. Provides immediate visual feedback\n * of the current stance to the player and observers.\n *\n * @module components/three/StanceSymbol3D\n * @category 3D Components\n * @korean 자세기호3D\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { FONT_FAMILY } from \"../../../../types/constants\";\nimport {\n getStanceColorHex,\n getStanceKoreanName,\n getTrigramSymbol,\n} from \"../../../../utils/stanceHelpers\";\n\n/**\n * Props for StanceSymbol3D component\n */\nexport interface StanceSymbol3DProps {\n /** Current trigram stance to display */\n readonly stance: TrigramStance;\n /** Height offset above player (default: 2.5) */\n readonly heightOffset?: number;\n /** Whether symbol should rotate */\n readonly animated?: boolean;\n /** Symbol scale multiplier */\n readonly scale?: number;\n /** Show Korean name below symbol */\n readonly showName?: boolean;\n}\n\n/**\n * Animation constants for stance symbol\n */\nconst ANIMATION_CONSTANTS = {\n ROTATION_SPEED: 0.5,\n BOB_AMPLITUDE: 0.1,\n BOB_FREQUENCY: 2,\n} as const;\n\n/**\n * StanceSymbol3D Component\n *\n * Renders a floating trigram symbol above the player with:\n * - Rotation animation\n * - Pulsing glow effect\n * - Stance-specific coloring\n * - Optional Korean name display\n *\n * Uses Html from @react-three/drei for crisp text rendering that always faces camera.\n *\n * @example\n * ```tsx\n * <StanceSymbol3D\n * stance={TrigramStance.GEON}\n * heightOffset={2.5}\n * animated={true}\n * showName={true}\n * />\n * ```\n */\nexport const StanceSymbol3D: React.FC<StanceSymbol3DProps> = ({\n stance,\n heightOffset = 2.5,\n animated = true,\n scale = 1.0,\n showName = true,\n}) => {\n const groupRef = useRef<THREE.Group>(null);\n\n const symbol = useMemo(() => getTrigramSymbol(stance), [stance]);\n const koreanName = useMemo(() => getStanceKoreanName(stance), [stance]);\n const colorHex = useMemo(() => getStanceColorHex(stance), [stance]);\n\n useFrame((state) => {\n if (!animated || !groupRef.current) return;\n\n const time = state.clock.elapsedTime;\n\n groupRef.current.rotation.y = time * ANIMATION_CONSTANTS.ROTATION_SPEED;\n\n groupRef.current.position.y =\n heightOffset +\n Math.sin(time * ANIMATION_CONSTANTS.BOB_FREQUENCY) *\n ANIMATION_CONSTANTS.BOB_AMPLITUDE;\n });\n\n return (\n <group\n ref={groupRef}\n position={[0, heightOffset, 0]}\n name=\"stance-symbol-3d\"\n >\n {/* Interactive Light Source - illuminates player head/shoulders */}\n <pointLight color={colorHex} intensity={2.5} distance={4} decay={2} />\n\n {/* Trigram symbol with glow effect */}\n <Html\n center\n distanceFactor={10}\n zIndexRange={[100, 0]}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n }}\n >\n {/* Main trigram symbol */}\n <div\n style={{\n fontSize: `${48 * scale}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: colorHex,\n textShadow: `\n 0 0 10px ${colorHex},\n 0 0 20px ${colorHex},\n 0 0 30px ${colorHex}\n `,\n fontWeight: \"bold\",\n lineHeight: \"1\",\n animation: \"pulse 2s ease-in-out infinite\",\n }}\n data-testid=\"trigram-symbol\"\n >\n {symbol}\n </div>\n\n {/* Korean name below symbol */}\n {showName && (\n <div\n style={{\n fontSize: `${16 * scale}px`,\n fontFamily: FONT_FAMILY.KOREAN,\n color: colorHex,\n textShadow: `0 0 5px ${colorHex}`,\n fontWeight: \"bold\",\n letterSpacing: \"2px\",\n }}\n data-testid=\"stance-korean-name\"\n >\n {koreanName}\n </div>\n )}\n </div>\n\n {/* CSS animation for pulse effect */}\n <style>\n {`\n @keyframes pulse {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.8; transform: scale(1.1); }\n }\n `}\n </style>\n </Html>\n </group>\n );\n};\n\nexport default StanceSymbol3D;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA2CA,IAAM,sBAAsB;CAC1B,gBAAgB;CAChB,eAAe;CACf,eAAe;AACjB;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,kBAAiD,EAC5D,QACA,eAAe,KACf,WAAW,MACX,QAAQ,GACR,WAAW,WACP;CACJ,MAAM,WAAW,OAAoB,IAAI;CAEzC,MAAM,SAAS,cAAc,iBAAiB,MAAM,GAAG,CAAC,MAAM,CAAC;CAC/D,MAAM,aAAa,cAAc,oBAAoB,MAAM,GAAG,CAAC,MAAM,CAAC;CACtE,MAAM,WAAW,cAAc,kBAAkB,MAAM,GAAG,CAAC,MAAM,CAAC;CAElE,UAAU,UAAU;EAClB,IAAI,CAAC,YAAY,CAAC,SAAS,SAAS;EAEpC,MAAM,OAAO,MAAM,MAAM;EAEzB,SAAS,QAAQ,SAAS,IAAI,OAAO,oBAAoB;EAEzD,SAAS,QAAQ,SAAS,IACxB,eACA,KAAK,IAAI,OAAO,oBAAoB,aAAa,IAC/C,oBAAoB;CAC1B,CAAC;CAED,OACE,qBAAC,SAAD;EACE,KAAK;EACL,UAAU;GAAC;GAAG;GAAc;EAAC;EAC7B,MAAK;EAHP,UAAA,CAME,oBAAC,cAAD;GAAY,OAAO;GAAU,WAAW;GAAK,UAAU;GAAG,OAAO;EAAI,CAAA,GAGrE,qBAAC,MAAD;GACE,QAAA;GACA,gBAAgB;GAChB,aAAa,CAAC,KAAK,CAAC;GACpB,OAAO;IACL,eAAe;IACf,YAAY;GACd;GAPF,UAAA,CASE,qBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,eAAe;KACf,YAAY;KACZ,KAAK;IACP;IANF,UAAA,CASE,oBAAC,OAAD;KACE,OAAO;MACL,UAAU,GAAG,KAAK,MAAM;MACxB,YAAY,YAAY;MACxB,OAAO;MACP,YAAY;2BACC,SAAS;2BACT,SAAS;2BACT,SAAS;;MAEtB,YAAY;MACZ,YAAY;MACZ,WAAW;KACb;KACA,eAAY;KAEX,UAAA;IACE,CAAA,GAGJ,YACC,oBAAC,OAAD;KACE,OAAO;MACL,UAAU,GAAG,KAAK,MAAM;MACxB,YAAY,YAAY;MACxB,OAAO;MACP,YAAY,WAAW;MACvB,YAAY;MACZ,eAAe;KACjB;KACA,eAAY;KAEX,UAAA;IACE,CAAA,CAEJ;GAGL,CAAA,GAAA,oBAAC,SAAD,EAAA,UACG;;;;;YAMI,CAAA,CACH;EACD,CAAA,CAAA;;AAEX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StanceTransitionEffect.js","names":[],"sources":["../../../../../src/components/shared/three/effects/StanceTransitionEffect.tsx"],"sourcesContent":["/**\n * StanceTransitionEffect - Smooth visual transition between trigram stances\n *\n * Manages the visual transition when a player changes stance, providing:\n * - 0.5s smooth color fade between old and new stance colors\n * - Expanding energy ring effect\n * - Bilingual stance name display (Korean + English) for 1s\n * - Audio synchronization for stance change SFX\n *\n * @module components/three/StanceTransitionEffect\n * @category 3D Components\n * @korean 자세전환효과\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 { TrigramStance } from \"../../../../types/common\";\nimport { FONT_FAMILY } from \"../../../../types/constants\";\nimport { colorUtils } from \"../../../../types/constants/colors\";\nimport {\n getStanceColor,\n getStanceNames,\n} from \"../../../../utils/stanceHelpers\";\n\n/**\n * Props for StanceTransitionEffect component\n */\nexport interface StanceTransitionEffectProps {\n /** Previous stance (for color interpolation) */\n readonly fromStance: TrigramStance | null;\n /** New stance being transitioned to */\n readonly toStance: TrigramStance;\n /** Callback when transition completes */\n readonly onTransitionComplete?: () => void;\n /** Transition duration in seconds (default: 0.5) */\n readonly duration?: number;\n /** Show stance name overlay (default: true) */\n readonly showNameOverlay?: boolean;\n}\n\n/**\n * StanceTransitionEffect Component\n *\n * Provides smooth visual feedback during stance changes:\n * 1. Expanding energy ring effect from player center\n * 2. Color interpolation from old to new stance\n * 3. Bilingual stance name overlay (1 second display)\n *\n * Performance optimized:\n * - Single animation frame callback\n * - Auto-cleanup after transition completes\n * - Reuses Three.js materials and geometries\n *\n * @example\n * ```tsx\n * <StanceTransitionEffect\n * fromStance={TrigramStance.GEON}\n * toStance={TrigramStance.TAE}\n * onTransitionComplete={() => console.log('Transition done')}\n * duration={0.5}\n * />\n * ```\n */\nexport const StanceTransitionEffect: React.FC<StanceTransitionEffectProps> = ({\n fromStance,\n toStance,\n onTransitionComplete,\n duration = 0.5,\n showNameOverlay = true,\n}) => {\n const ringRef = useRef<THREE.Mesh>(null);\n const startTimeRef = useRef<number>(0);\n const isInitializedRef = useRef(false);\n const [isTransitioning, setIsTransitioning] = useState(true);\n const [showName, setShowName] = useState(showNameOverlay);\n\n const fromColor = useMemo(\n () => (fromStance ? getStanceColor(fromStance) : getStanceColor(toStance)),\n [fromStance, toStance]\n );\n const toColor = useMemo(() => getStanceColor(toStance), [toStance]);\n const stanceNames = useMemo(() => getStanceNames(toStance), [toStance]);\n\n\n useEffect(() => {\n isInitializedRef.current = false;\n startTimeRef.current = 0;\n setIsTransitioning(true);\n setShowName(showNameOverlay);\n\n if (showNameOverlay) {\n const nameTimer = setTimeout(() => {\n setShowName(false);\n }, 1000);\n\n return () => clearTimeout(nameTimer);\n }\n }, [toStance, showNameOverlay]);\n\n useFrame((state) => {\n if (!isTransitioning || !ringRef.current) return;\n\n if (!isInitializedRef.current) {\n startTimeRef.current = state.clock.elapsedTime;\n isInitializedRef.current = true;\n }\n\n const elapsed = state.clock.elapsedTime - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1.0);\n\n const currentColor = colorUtils.blend(fromColor, toColor, progress);\n (ringRef.current.material as THREE.MeshBasicMaterial).color.setHex(\n currentColor\n );\n\n const scale = 0.5 + progress * 2.5; // From 0.5 to 3.0\n ringRef.current.scale.setScalar(scale);\n\n const opacity = 1.0 - progress * 0.7; // From 1.0 to 0.3\n (ringRef.current.material as THREE.MeshBasicMaterial).opacity = opacity;\n\n if (progress >= 1.0) {\n setIsTransitioning(false);\n onTransitionComplete?.();\n }\n });\n\n const toColorHex = `#${toColor.toString(16).padStart(6, \"0\")}`;\n\n return (\n <group name=\"stance-transition-effect\">\n {/* Expanding energy ring */}\n <mesh\n ref={ringRef}\n position={[0, 0.05, 0]}\n rotation={[-Math.PI / 2, 0, 0]}\n name=\"transition-ring\"\n >\n <ringGeometry args={[0.8, 1.0, 32]} />\n <meshBasicMaterial\n color={fromColor}\n transparent\n opacity={1.0}\n side={THREE.DoubleSide}\n depthWrite={false}\n blending={THREE.AdditiveBlending}\n />\n </mesh>\n\n {/* Stance name overlay (Korean + English) */}\n {showName && (\n <Html\n position={[0, 2.0, 0]}\n center\n distanceFactor={10}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n data-testid=\"stance-name-overlay\"\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n padding: \"8px 16px\",\n backgroundColor: \"rgba(0, 0, 0, 0.7)\",\n borderRadius: \"8px\",\n border: `2px solid ${toColorHex}`,\n boxShadow: `0 0 20px ${toColorHex}`,\n animation: \"fadeInOut 1s ease-in-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: \"24px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"bold\",\n textShadow: `0 0 10px ${toColorHex}`,\n }}\n >\n {stanceNames.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: \"14px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"normal\",\n opacity: 0.8,\n }}\n >\n {stanceNames.english}\n </div>\n </div>\n\n {/* CSS animation */}\n <style>\n {`\n @keyframes fadeInOut {\n 0% { opacity: 0; transform: translateY(10px); }\n 20% { opacity: 1; transform: translateY(0); }\n 80% { opacity: 1; transform: translateY(0); }\n 100% { opacity: 0; transform: translateY(-10px); }\n }\n `}\n </style>\n </Html>\n )}\n </group>\n );\n};\n\nexport default StanceTransitionEffect;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA,IAAa,0BAAiE,EAC5E,YACA,UACA,sBACA,WAAW,IACX,kBAAkB,WACd;CACJ,MAAM,UAAU,OAAmB,IAAI;CACvC,MAAM,eAAe,OAAe,CAAC;CACrC,MAAM,mBAAmB,OAAO,KAAK;CACrC,MAAM,CAAC,iBAAiB,sBAAsB,SAAS,IAAI;CAC3D,MAAM,CAAC,UAAU,eAAe,SAAS,eAAe;CAExD,MAAM,YAAY,cACT,aAAa,eAAe,UAAU,IAAI,eAAe,QAAQ,GACxE,CAAC,YAAY,QAAQ,CACvB;CACA,MAAM,UAAU,cAAc,eAAe,QAAQ,GAAG,CAAC,QAAQ,CAAC;CAClE,MAAM,cAAc,cAAc,eAAe,QAAQ,GAAG,CAAC,QAAQ,CAAC;CAGtE,gBAAgB;EACd,iBAAiB,UAAU;EAC3B,aAAa,UAAU;EACvB,mBAAmB,IAAI;EACvB,YAAY,eAAe;EAE3B,IAAI,iBAAiB;GACnB,MAAM,YAAY,iBAAiB;IACjC,YAAY,KAAK;GACnB,GAAG,GAAI;GAEP,aAAa,aAAa,SAAS;EACrC;CACF,GAAG,CAAC,UAAU,eAAe,CAAC;CAE9B,UAAU,UAAU;EAClB,IAAI,CAAC,mBAAmB,CAAC,QAAQ,SAAS;EAE1C,IAAI,CAAC,iBAAiB,SAAS;GAC7B,aAAa,UAAU,MAAM,MAAM;GACnC,iBAAiB,UAAU;EAC7B;EAEA,MAAM,UAAU,MAAM,MAAM,cAAc,aAAa;EACvD,MAAM,WAAW,KAAK,IAAI,UAAU,UAAU,CAAG;EAEjD,MAAM,eAAe,WAAW,MAAM,WAAW,SAAS,QAAQ;EAClE,QAAS,QAAQ,SAAqC,MAAM,OAC1D,YACF;EAEA,MAAM,QAAQ,KAAM,WAAW;EAC/B,QAAQ,QAAQ,MAAM,UAAU,KAAK;EAErC,MAAM,UAAU,IAAM,WAAW;EACjC,QAAS,QAAQ,SAAqC,UAAU;EAEhE,IAAI,YAAY,GAAK;GACnB,mBAAmB,KAAK;GACxB,uBAAuB;EACzB;CACF,CAAC;CAED,MAAM,aAAa,IAAI,QAAQ,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CAE3D,OACE,qBAAC,SAAD;EAAO,MAAK;
|
|
1
|
+
{"version":3,"file":"StanceTransitionEffect.js","names":[],"sources":["../../../../../src/components/shared/three/effects/StanceTransitionEffect.tsx"],"sourcesContent":["/**\n * StanceTransitionEffect - Smooth visual transition between trigram stances\n *\n * Manages the visual transition when a player changes stance, providing:\n * - 0.5s smooth color fade between old and new stance colors\n * - Expanding energy ring effect\n * - Bilingual stance name display (Korean + English) for 1s\n * - Audio synchronization for stance change SFX\n *\n * @module components/three/StanceTransitionEffect\n * @category 3D Components\n * @korean 자세전환효과\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 { TrigramStance } from \"../../../../types/common\";\nimport { FONT_FAMILY } from \"../../../../types/constants\";\nimport { colorUtils } from \"../../../../types/constants/colors\";\nimport {\n getStanceColor,\n getStanceNames,\n} from \"../../../../utils/stanceHelpers\";\n\n/**\n * Props for StanceTransitionEffect component\n */\nexport interface StanceTransitionEffectProps {\n /** Previous stance (for color interpolation) */\n readonly fromStance: TrigramStance | null;\n /** New stance being transitioned to */\n readonly toStance: TrigramStance;\n /** Callback when transition completes */\n readonly onTransitionComplete?: () => void;\n /** Transition duration in seconds (default: 0.5) */\n readonly duration?: number;\n /** Show stance name overlay (default: true) */\n readonly showNameOverlay?: boolean;\n}\n\n/**\n * StanceTransitionEffect Component\n *\n * Provides smooth visual feedback during stance changes:\n * 1. Expanding energy ring effect from player center\n * 2. Color interpolation from old to new stance\n * 3. Bilingual stance name overlay (1 second display)\n *\n * Performance optimized:\n * - Single animation frame callback\n * - Auto-cleanup after transition completes\n * - Reuses Three.js materials and geometries\n *\n * @example\n * ```tsx\n * <StanceTransitionEffect\n * fromStance={TrigramStance.GEON}\n * toStance={TrigramStance.TAE}\n * onTransitionComplete={() => console.log('Transition done')}\n * duration={0.5}\n * />\n * ```\n */\nexport const StanceTransitionEffect: React.FC<StanceTransitionEffectProps> = ({\n fromStance,\n toStance,\n onTransitionComplete,\n duration = 0.5,\n showNameOverlay = true,\n}) => {\n const ringRef = useRef<THREE.Mesh>(null);\n const startTimeRef = useRef<number>(0);\n const isInitializedRef = useRef(false);\n const [isTransitioning, setIsTransitioning] = useState(true);\n const [showName, setShowName] = useState(showNameOverlay);\n\n const fromColor = useMemo(\n () => (fromStance ? getStanceColor(fromStance) : getStanceColor(toStance)),\n [fromStance, toStance]\n );\n const toColor = useMemo(() => getStanceColor(toStance), [toStance]);\n const stanceNames = useMemo(() => getStanceNames(toStance), [toStance]);\n\n\n useEffect(() => {\n isInitializedRef.current = false;\n startTimeRef.current = 0;\n setIsTransitioning(true);\n setShowName(showNameOverlay);\n\n if (showNameOverlay) {\n const nameTimer = setTimeout(() => {\n setShowName(false);\n }, 1000);\n\n return () => clearTimeout(nameTimer);\n }\n }, [toStance, showNameOverlay]);\n\n useFrame((state) => {\n if (!isTransitioning || !ringRef.current) return;\n\n if (!isInitializedRef.current) {\n startTimeRef.current = state.clock.elapsedTime;\n isInitializedRef.current = true;\n }\n\n const elapsed = state.clock.elapsedTime - startTimeRef.current;\n const progress = Math.min(elapsed / duration, 1.0);\n\n const currentColor = colorUtils.blend(fromColor, toColor, progress);\n (ringRef.current.material as THREE.MeshBasicMaterial).color.setHex(\n currentColor\n );\n\n const scale = 0.5 + progress * 2.5; // From 0.5 to 3.0\n ringRef.current.scale.setScalar(scale);\n\n const opacity = 1.0 - progress * 0.7; // From 1.0 to 0.3\n (ringRef.current.material as THREE.MeshBasicMaterial).opacity = opacity;\n\n if (progress >= 1.0) {\n setIsTransitioning(false);\n onTransitionComplete?.();\n }\n });\n\n const toColorHex = `#${toColor.toString(16).padStart(6, \"0\")}`;\n\n return (\n <group name=\"stance-transition-effect\">\n {/* Expanding energy ring */}\n <mesh\n ref={ringRef}\n position={[0, 0.05, 0]}\n rotation={[-Math.PI / 2, 0, 0]}\n name=\"transition-ring\"\n >\n <ringGeometry args={[0.8, 1.0, 32]} />\n <meshBasicMaterial\n color={fromColor}\n transparent\n opacity={1.0}\n side={THREE.DoubleSide}\n depthWrite={false}\n blending={THREE.AdditiveBlending}\n />\n </mesh>\n\n {/* Stance name overlay (Korean + English) */}\n {showName && (\n <Html\n position={[0, 2.0, 0]}\n center\n distanceFactor={10}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n data-testid=\"stance-name-overlay\"\n >\n <div\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: \"4px\",\n padding: \"8px 16px\",\n backgroundColor: \"rgba(0, 0, 0, 0.7)\",\n borderRadius: \"8px\",\n border: `2px solid ${toColorHex}`,\n boxShadow: `0 0 20px ${toColorHex}`,\n animation: \"fadeInOut 1s ease-in-out\",\n }}\n >\n {/* Korean name */}\n <div\n style={{\n fontSize: \"24px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"bold\",\n textShadow: `0 0 10px ${toColorHex}`,\n }}\n >\n {stanceNames.korean}\n </div>\n\n {/* English name */}\n <div\n style={{\n fontSize: \"14px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: toColorHex,\n fontWeight: \"normal\",\n opacity: 0.8,\n }}\n >\n {stanceNames.english}\n </div>\n </div>\n\n {/* CSS animation */}\n <style>\n {`\n @keyframes fadeInOut {\n 0% { opacity: 0; transform: translateY(10px); }\n 20% { opacity: 1; transform: translateY(0); }\n 80% { opacity: 1; transform: translateY(0); }\n 100% { opacity: 0; transform: translateY(-10px); }\n }\n `}\n </style>\n </Html>\n )}\n </group>\n );\n};\n\nexport default StanceTransitionEffect;\n"],"mappings":"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"}
|
|
@@ -224,13 +224,14 @@ var VitalPointMarkers3D = ({ position = [
|
|
|
224
224
|
return /* @__PURE__ */ jsx("group", {
|
|
225
225
|
position,
|
|
226
226
|
children: visiblePoints.map((vitalPoint) => {
|
|
227
|
+
const position3D = convert2DTo3D(vitalPoint.position, [
|
|
228
|
+
0,
|
|
229
|
+
0,
|
|
230
|
+
0
|
|
231
|
+
]);
|
|
227
232
|
return /* @__PURE__ */ jsx(VitalPointMarker, {
|
|
228
233
|
vitalPoint,
|
|
229
|
-
position3D
|
|
230
|
-
0,
|
|
231
|
-
0,
|
|
232
|
-
0
|
|
233
|
-
]),
|
|
234
|
+
position3D,
|
|
234
235
|
selected: selectedPoint === vitalPoint.id,
|
|
235
236
|
hovered: hoveredPoint === vitalPoint.id,
|
|
236
237
|
showLabel: showLabels,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VitalPointMarkers3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/VitalPointMarkers3D.tsx"],"sourcesContent":["/**\n * VitalPointMarkers3D - 3D vital point visualization\n *\n * Renders anatomical vital points (급소) in 3D space around character models\n * Provides Korean martial arts targeting system visualization\n * Note: Currently displays points from KOREAN_VITAL_POINTS data (expandable to 70 points)\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { Position, VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Body region filter options\n */\nexport type BodyRegionFilter = \"all\" | \"head\" | \"torso\" | \"arms\" | \"legs\";\n\n/**\n * Props for the VitalPointMarkers3D component.\n * Controls visibility and interaction with anatomical targeting points.\n */\nexport interface VitalPointMarkers3DProps {\n /** 3D world position of the character [x, y, z]. Defaults to [0, 0, 0] */\n readonly position?: [number, number, number];\n /** Whether markers are visible. Defaults to true */\n readonly visible?: boolean;\n /** Selected vital point ID for highlighting */\n readonly selectedPoint?: string | null;\n /** Callback when a vital point is clicked */\n readonly onPointClick?: (vitalPointId: string) => void;\n /** Callback when a vital point is hovered */\n readonly onPointHover?: (vitalPointId: string | null) => void;\n /** Filter points by severity level */\n readonly severityFilter?: VitalPointSeverity[];\n /** Filter points by body region */\n readonly regionFilter?: BodyRegionFilter;\n /** Search query to filter points by name */\n readonly searchQuery?: string;\n /** Whether to show point labels with Korean names */\n readonly showLabels?: boolean;\n /** Scale multiplier for marker size. Defaults to 1.0 */\n readonly scale?: number;\n /** Whether to enable pulsing animation. Defaults to true */\n readonly animated?: boolean;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.SECONDARY_MAGENTA;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.SECONDARY_YELLOW;\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.ACCENT_CYAN;\n default:\n return KOREAN_COLORS.PRIMARY_CYAN;\n }\n};\n\nconst CHARACTER_CENTER_X = 100; // Pixel center of character sprite\nconst CHARACTER_SPRITE_HEIGHT = 200; // Pixel height of character sprite\nconst CHARACTER_3D_HEIGHT = 2.8; // 3D model height in world units\nconst CHARACTER_3D_WIDTH = 0.8; // Approximate 3D model width\nconst SPRITE_WIDTH = 100; // Half-width of sprite (total ~200 pixels)\n\nconst LABEL_STYLES = {\n padding: \"4px 8px\",\n borderRadius: \"4px\",\n fontSize: \"10px\",\n subtextSize: \"8px\",\n subtextOpacity: 0.8,\n borderWidth: \"1px\",\n} as const;\n\n/**\n * Convert color number to RGBA hex string\n * @param color - Color as number (e.g., 0xFF0000)\n * @param alpha - Alpha channel as hex string (e.g., \"dd\" for semi-transparent)\n * @returns RGBA hex string (e.g., \"#ff0000dd\")\n */\nconst colorToRgbaHex = (color: number, alpha: string = \"ff\"): string => {\n return `#${color.toString(16).padStart(6, \"0\")}${alpha}`;\n};\n\n/**\n * Convert 2D sprite coordinates to 3D body position\n * Maps vital point pixel coordinates to 3D character model space\n *\n * Input: 2D pixel coordinates where:\n * - X: ~50-150 (100 = center), positive = right side of character\n * - Y: 0 = top of head, 200 = feet (y increases downward)\n *\n * Output: 3D world coordinates relative to character position where:\n * - X: horizontal offset from character center\n * - Y: height from ground (0 = feet, 2.8 = top)\n * - Z: depth offset (positive = in front of character)\n */\nconst convert2DTo3D = (\n pos2D: Position,\n basePosition: [number, number, number]\n): [number, number, number] => {\n const normalizedX = (pos2D.x - CHARACTER_CENTER_X) / SPRITE_WIDTH;\n const x3D = normalizedX * CHARACTER_3D_WIDTH;\n\n const normalizedY = 1 - pos2D.y / CHARACTER_SPRITE_HEIGHT;\n const y3D = normalizedY * CHARACTER_3D_HEIGHT;\n\n const z3D = 0.15;\n\n return [basePosition[0] + x3D, basePosition[1] + y3D, basePosition[2] + z3D];\n};\n\n/**\n * Individual Vital Point Marker Component\n */\ninterface VitalPointMarkerProps {\n readonly vitalPoint: VitalPoint;\n readonly position3D: [number, number, number];\n readonly selected: boolean;\n readonly hovered: boolean;\n readonly showLabel: boolean;\n readonly scale: number;\n readonly animated: boolean;\n readonly onHover: (hovered: boolean) => void;\n readonly onClick: () => void;\n}\n\nconst VitalPointMarker: React.FC<VitalPointMarkerProps> = ({\n vitalPoint,\n position3D,\n selected,\n hovered,\n showLabel,\n scale,\n animated,\n onHover,\n onClick,\n}) => {\n const sphereRef = useRef<THREE.Mesh>(null);\n const ringRef = useRef<THREE.Mesh>(null);\n\n useFrame((state) => {\n if (!sphereRef.current || !animated) return;\n\n const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.1 + 1;\n sphereRef.current.scale.setScalar(pulse * scale);\n\n if (ringRef.current && (selected || hovered)) {\n ringRef.current.rotation.z += 0.05;\n }\n });\n\n const color = useMemo(() => {\n if (selected) return KOREAN_COLORS.ACCENT_GOLD;\n if (hovered) return KOREAN_COLORS.PRIMARY_CYAN;\n return getSeverityColor(vitalPoint.severity);\n }, [selected, hovered, vitalPoint.severity]);\n\n const markerSize = useMemo(() => {\n const DEFAULT_MARKER_SIZE = 0.08;\n\n switch (vitalPoint.severity) {\n case VitalPointSeverity.LETHAL:\n case VitalPointSeverity.CRITICAL:\n return DEFAULT_MARKER_SIZE * 1.6 * scale; // 0.128\n case VitalPointSeverity.MAJOR:\n return DEFAULT_MARKER_SIZE * 1.3 * scale; // 0.104\n case VitalPointSeverity.MODERATE:\n return DEFAULT_MARKER_SIZE * 1.0 * scale; // 0.08\n case VitalPointSeverity.MINOR:\n return DEFAULT_MARKER_SIZE * 0.8 * scale; // 0.064\n default:\n return DEFAULT_MARKER_SIZE * scale;\n }\n }, [vitalPoint.severity, scale]);\n\n return (\n <group position={position3D}>\n {/* Main sphere marker */}\n <mesh\n ref={sphereRef}\n onPointerOver={() => onHover(true)}\n onPointerOut={() => onHover(false)}\n onClick={(e) => {\n e.stopPropagation();\n onClick();\n }}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <meshStandardMaterial\n color={color}\n emissive={color}\n emissiveIntensity={hovered || selected ? 0.8 : 0.4}\n transparent\n opacity={hovered || selected ? 1.0 : 0.85}\n />\n </mesh>\n\n {/* Outer ring for selected/hovered */}\n {(selected || hovered) && (\n <mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry args={[markerSize * 1.5, markerSize * 1.8, 32]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={0.5}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Label overlay */}\n {showLabel && (hovered || selected) && (\n <Html\n position={[0, markerSize * 2, 0]}\n center\n distanceFactor={5}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n background: colorToRgbaHex(color, \"dd\"),\n color: \"#ffffff\",\n padding: LABEL_STYLES.padding,\n borderRadius: LABEL_STYLES.borderRadius,\n fontSize: LABEL_STYLES.fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n textAlign: \"center\",\n border: `${LABEL_STYLES.borderWidth} solid ${colorToRgbaHex(\n color\n )}`,\n }}\n >\n <div>{vitalPoint.names.korean}</div>\n <div\n style={{\n fontSize: LABEL_STYLES.subtextSize,\n opacity: LABEL_STYLES.subtextOpacity,\n }}\n >\n {vitalPoint.names.english}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\n/**\n * VitalPointMarkers3D Component\n * Renders all vital points as 3D markers around a character\n */\nexport const VitalPointMarkers3D: React.FC<VitalPointMarkers3DProps> = ({\n position = [0, 0, 0],\n visible = true,\n selectedPoint = null,\n onPointClick,\n onPointHover,\n severityFilter,\n regionFilter = \"all\",\n searchQuery = \"\",\n showLabels = true,\n scale = 1.0,\n animated = true,\n}) => {\n const [hoveredPoint, setHoveredPoint] = useState<string | null>(null);\n\n const visiblePoints = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilter && severityFilter.length > 0) {\n points = points.filter((vp) => severityFilter.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points;\n }, [severityFilter, regionFilter, searchQuery]);\n\n const handlePointHover = (vitalPointId: string, hovered: boolean) => {\n const newHovered = hovered ? vitalPointId : null;\n setHoveredPoint(newHovered);\n onPointHover?.(newHovered);\n };\n\n const handlePointClick = (vitalPointId: string) => {\n onPointClick?.(vitalPointId);\n };\n\n if (!visible) return null;\n\n return (\n <group position={position}>\n {visiblePoints.map((vitalPoint) => {\n const position3D = convert2DTo3D(vitalPoint.position, [0, 0, 0]);\n\n return (\n <VitalPointMarker\n key={vitalPoint.id}\n vitalPoint={vitalPoint}\n position3D={position3D}\n selected={selectedPoint === vitalPoint.id}\n hovered={hoveredPoint === vitalPoint.id}\n showLabel={showLabels}\n scale={scale}\n animated={animated}\n onHover={(hovered) => handlePointHover(vitalPoint.id, hovered)}\n onClick={() => handlePointClick(vitalPoint.id)}\n />\n );\n })}\n </group>\n );\n};\n\nexport default VitalPointMarkers3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VitalPointMarkers3D.js","names":[],"sources":["../../../../../src/components/shared/three/effects/VitalPointMarkers3D.tsx"],"sourcesContent":["/**\n * VitalPointMarkers3D - 3D vital point visualization\n *\n * Renders anatomical vital points (급소) in 3D space around character models\n * Provides Korean martial arts targeting system visualization\n * Note: Currently displays points from KOREAN_VITAL_POINTS data (expandable to 70 points)\n */\n\nimport { Html } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_VITAL_POINTS } from \"../../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPoint } from \"../../../../systems/vitalpoint/types\";\nimport { Position, VitalPointSeverity } from \"../../../../types/common\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"../../../../types/constants\";\n\n/**\n * Body region filter options\n */\nexport type BodyRegionFilter = \"all\" | \"head\" | \"torso\" | \"arms\" | \"legs\";\n\n/**\n * Props for the VitalPointMarkers3D component.\n * Controls visibility and interaction with anatomical targeting points.\n */\nexport interface VitalPointMarkers3DProps {\n /** 3D world position of the character [x, y, z]. Defaults to [0, 0, 0] */\n readonly position?: [number, number, number];\n /** Whether markers are visible. Defaults to true */\n readonly visible?: boolean;\n /** Selected vital point ID for highlighting */\n readonly selectedPoint?: string | null;\n /** Callback when a vital point is clicked */\n readonly onPointClick?: (vitalPointId: string) => void;\n /** Callback when a vital point is hovered */\n readonly onPointHover?: (vitalPointId: string | null) => void;\n /** Filter points by severity level */\n readonly severityFilter?: VitalPointSeverity[];\n /** Filter points by body region */\n readonly regionFilter?: BodyRegionFilter;\n /** Search query to filter points by name */\n readonly searchQuery?: string;\n /** Whether to show point labels with Korean names */\n readonly showLabels?: boolean;\n /** Scale multiplier for marker size. Defaults to 1.0 */\n readonly scale?: number;\n /** Whether to enable pulsing animation. Defaults to true */\n readonly animated?: boolean;\n}\n\n/**\n * Get color based on vital point severity\n */\nconst getSeverityColor = (severity: VitalPointSeverity): number => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return KOREAN_COLORS.ACCENT_RED;\n case VitalPointSeverity.CRITICAL:\n return KOREAN_COLORS.SECONDARY_MAGENTA;\n case VitalPointSeverity.MAJOR:\n return KOREAN_COLORS.ACCENT_GOLD;\n case VitalPointSeverity.MODERATE:\n return KOREAN_COLORS.SECONDARY_YELLOW;\n case VitalPointSeverity.MINOR:\n return KOREAN_COLORS.ACCENT_CYAN;\n default:\n return KOREAN_COLORS.PRIMARY_CYAN;\n }\n};\n\nconst CHARACTER_CENTER_X = 100; // Pixel center of character sprite\nconst CHARACTER_SPRITE_HEIGHT = 200; // Pixel height of character sprite\nconst CHARACTER_3D_HEIGHT = 2.8; // 3D model height in world units\nconst CHARACTER_3D_WIDTH = 0.8; // Approximate 3D model width\nconst SPRITE_WIDTH = 100; // Half-width of sprite (total ~200 pixels)\n\nconst LABEL_STYLES = {\n padding: \"4px 8px\",\n borderRadius: \"4px\",\n fontSize: \"10px\",\n subtextSize: \"8px\",\n subtextOpacity: 0.8,\n borderWidth: \"1px\",\n} as const;\n\n/**\n * Convert color number to RGBA hex string\n * @param color - Color as number (e.g., 0xFF0000)\n * @param alpha - Alpha channel as hex string (e.g., \"dd\" for semi-transparent)\n * @returns RGBA hex string (e.g., \"#ff0000dd\")\n */\nconst colorToRgbaHex = (color: number, alpha: string = \"ff\"): string => {\n return `#${color.toString(16).padStart(6, \"0\")}${alpha}`;\n};\n\n/**\n * Convert 2D sprite coordinates to 3D body position\n * Maps vital point pixel coordinates to 3D character model space\n *\n * Input: 2D pixel coordinates where:\n * - X: ~50-150 (100 = center), positive = right side of character\n * - Y: 0 = top of head, 200 = feet (y increases downward)\n *\n * Output: 3D world coordinates relative to character position where:\n * - X: horizontal offset from character center\n * - Y: height from ground (0 = feet, 2.8 = top)\n * - Z: depth offset (positive = in front of character)\n */\nconst convert2DTo3D = (\n pos2D: Position,\n basePosition: [number, number, number]\n): [number, number, number] => {\n const normalizedX = (pos2D.x - CHARACTER_CENTER_X) / SPRITE_WIDTH;\n const x3D = normalizedX * CHARACTER_3D_WIDTH;\n\n const normalizedY = 1 - pos2D.y / CHARACTER_SPRITE_HEIGHT;\n const y3D = normalizedY * CHARACTER_3D_HEIGHT;\n\n const z3D = 0.15;\n\n return [basePosition[0] + x3D, basePosition[1] + y3D, basePosition[2] + z3D];\n};\n\n/**\n * Individual Vital Point Marker Component\n */\ninterface VitalPointMarkerProps {\n readonly vitalPoint: VitalPoint;\n readonly position3D: [number, number, number];\n readonly selected: boolean;\n readonly hovered: boolean;\n readonly showLabel: boolean;\n readonly scale: number;\n readonly animated: boolean;\n readonly onHover: (hovered: boolean) => void;\n readonly onClick: () => void;\n}\n\nconst VitalPointMarker: React.FC<VitalPointMarkerProps> = ({\n vitalPoint,\n position3D,\n selected,\n hovered,\n showLabel,\n scale,\n animated,\n onHover,\n onClick,\n}) => {\n const sphereRef = useRef<THREE.Mesh>(null);\n const ringRef = useRef<THREE.Mesh>(null);\n\n useFrame((state) => {\n if (!sphereRef.current || !animated) return;\n\n const pulse = Math.sin(state.clock.elapsedTime * 2) * 0.1 + 1;\n sphereRef.current.scale.setScalar(pulse * scale);\n\n if (ringRef.current && (selected || hovered)) {\n ringRef.current.rotation.z += 0.05;\n }\n });\n\n const color = useMemo(() => {\n if (selected) return KOREAN_COLORS.ACCENT_GOLD;\n if (hovered) return KOREAN_COLORS.PRIMARY_CYAN;\n return getSeverityColor(vitalPoint.severity);\n }, [selected, hovered, vitalPoint.severity]);\n\n const markerSize = useMemo(() => {\n const DEFAULT_MARKER_SIZE = 0.08;\n\n switch (vitalPoint.severity) {\n case VitalPointSeverity.LETHAL:\n case VitalPointSeverity.CRITICAL:\n return DEFAULT_MARKER_SIZE * 1.6 * scale; // 0.128\n case VitalPointSeverity.MAJOR:\n return DEFAULT_MARKER_SIZE * 1.3 * scale; // 0.104\n case VitalPointSeverity.MODERATE:\n return DEFAULT_MARKER_SIZE * 1.0 * scale; // 0.08\n case VitalPointSeverity.MINOR:\n return DEFAULT_MARKER_SIZE * 0.8 * scale; // 0.064\n default:\n return DEFAULT_MARKER_SIZE * scale;\n }\n }, [vitalPoint.severity, scale]);\n\n return (\n <group position={position3D}>\n {/* Main sphere marker */}\n <mesh\n ref={sphereRef}\n onPointerOver={() => onHover(true)}\n onPointerOut={() => onHover(false)}\n onClick={(e) => {\n e.stopPropagation();\n onClick();\n }}\n >\n <sphereGeometry args={[markerSize, 16, 16]} />\n <meshStandardMaterial\n color={color}\n emissive={color}\n emissiveIntensity={hovered || selected ? 0.8 : 0.4}\n transparent\n opacity={hovered || selected ? 1.0 : 0.85}\n />\n </mesh>\n\n {/* Outer ring for selected/hovered */}\n {(selected || hovered) && (\n <mesh ref={ringRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0]}>\n <ringGeometry args={[markerSize * 1.5, markerSize * 1.8, 32]} />\n <meshBasicMaterial\n color={color}\n transparent\n opacity={0.5}\n side={THREE.DoubleSide}\n />\n </mesh>\n )}\n\n {/* Label overlay */}\n {showLabel && (hovered || selected) && (\n <Html\n position={[0, markerSize * 2, 0]}\n center\n distanceFactor={5}\n style={{\n pointerEvents: \"none\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n background: colorToRgbaHex(color, \"dd\"),\n color: \"#ffffff\",\n padding: LABEL_STYLES.padding,\n borderRadius: LABEL_STYLES.borderRadius,\n fontSize: LABEL_STYLES.fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n whiteSpace: \"nowrap\",\n textAlign: \"center\",\n border: `${LABEL_STYLES.borderWidth} solid ${colorToRgbaHex(\n color\n )}`,\n }}\n >\n <div>{vitalPoint.names.korean}</div>\n <div\n style={{\n fontSize: LABEL_STYLES.subtextSize,\n opacity: LABEL_STYLES.subtextOpacity,\n }}\n >\n {vitalPoint.names.english}\n </div>\n </div>\n </Html>\n )}\n </group>\n );\n};\n\n/**\n * VitalPointMarkers3D Component\n * Renders all vital points as 3D markers around a character\n */\nexport const VitalPointMarkers3D: React.FC<VitalPointMarkers3DProps> = ({\n position = [0, 0, 0],\n visible = true,\n selectedPoint = null,\n onPointClick,\n onPointHover,\n severityFilter,\n regionFilter = \"all\",\n searchQuery = \"\",\n showLabels = true,\n scale = 1.0,\n animated = true,\n}) => {\n const [hoveredPoint, setHoveredPoint] = useState<string | null>(null);\n\n const visiblePoints = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n if (severityFilter && severityFilter.length > 0) {\n points = points.filter((vp) => severityFilter.includes(vp.severity));\n }\n\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\")\n );\n } else if (regionFilter === \"legs\") {\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\")\n );\n } else {\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query)\n );\n }\n\n return points;\n }, [severityFilter, regionFilter, searchQuery]);\n\n const handlePointHover = (vitalPointId: string, hovered: boolean) => {\n const newHovered = hovered ? vitalPointId : null;\n setHoveredPoint(newHovered);\n onPointHover?.(newHovered);\n };\n\n const handlePointClick = (vitalPointId: string) => {\n onPointClick?.(vitalPointId);\n };\n\n if (!visible) return null;\n\n return (\n <group position={position}>\n {visiblePoints.map((vitalPoint) => {\n const position3D = convert2DTo3D(vitalPoint.position, [0, 0, 0]);\n\n return (\n <VitalPointMarker\n key={vitalPoint.id}\n vitalPoint={vitalPoint}\n position3D={position3D}\n selected={selectedPoint === vitalPoint.id}\n hovered={hoveredPoint === vitalPoint.id}\n showLabel={showLabels}\n scale={scale}\n animated={animated}\n onHover={(hovered) => handlePointHover(vitalPoint.id, hovered)}\n onClick={() => handlePointClick(vitalPoint.id)}\n />\n );\n })}\n </group>\n );\n};\n\nexport default VitalPointMarkers3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GuardIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/GuardIndicator.tsx"],"sourcesContent":["/**\n * GuardIndicator - Visual indicator showing current fighting stance guard position\n * Displays Korean traditional stance name, English translation, and guard characteristics\n *\n * @module components/shared/three/indicators/GuardIndicator\n * @category Combat UI\n * @korean 방어자세표시기\n */\n\nimport React, { useMemo } from \"react\";\nimport { STANCE_GUARD_CONFIGS } from \"../../../../systems/animation\";\nimport { TRIGRAM_DATA } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Props for GuardIndicator component\n */\nexport interface GuardIndicatorProps {\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Whether player is in guard animation state */\n readonly isInGuard: boolean;\n /** Player 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 * Get guard height label based on arm positions\n *\n * Guard heights based on shoulder.x (vertical angle in radians):\n * - High guard (고위): shoulder at chin level (x <= -0.8)\n * GEON (-1.0), LI (-1.6), GAN (-1.0)\n * - Mid guard (중위): shoulder at chest level (-0.8 < x < -0.4)\n * TAE (-0.7), JIN (-0.6), SON (-0.8), GAM (-0.8)\n * - Low guard (저위): shoulder low (x >= -0.4)\n * GON (-0.4)\n */\nfunction getGuardHeight(stance: TrigramStance): {\n korean: string;\n english: string;\n} {\n const config = STANCE_GUARD_CONFIGS[stance];\n const shoulderY = config.guardPose.leftArm.shoulder.x; // x is vertical in Euler\n\n if (shoulderY <= -0.8) {\n return { korean: \"고위\", english: \"High\" };\n } else if (shoulderY < -0.4) {\n return { korean: \"중위\", english: \"Mid\" };\n } else {\n return { korean: \"저위\", english: \"Low\" };\n }\n}\n\n/**\n * Get traditional Korean stance name from guard config\n */\nfunction getTraditionalStanceName(stance: TrigramStance): {\n korean: string;\n romanized: string;\n} {\n const stanceNames: Record<\n TrigramStance,\n { korean: string; romanized: string }\n > = {\n [TrigramStance.GEON]: { korean: \"앞서기\", romanized: \"Ap Seogi\" },\n [TrigramStance.TAE]: { korean: \"범서기\", romanized: \"Beom Seogi\" },\n [TrigramStance.LI]: { korean: \"겨루기 준비\", romanized: \"Gyeorugi Junbi\" },\n [TrigramStance.JIN]: { korean: \"주춤서기\", romanized: \"Juchum Seogi\" },\n [TrigramStance.SON]: { korean: \"학다리서기\", romanized: \"Hakdari Seogi\" },\n [TrigramStance.GAM]: { korean: \"뒷발서기\", romanized: \"Dwitbal Seogi\" },\n [TrigramStance.GAN]: { korean: \"모아서기\", romanized: \"Moa Seogi\" },\n [TrigramStance.GON]: { korean: \"중하\", romanized: \"Joong Ha Seogi\" },\n };\n return stanceNames[stance];\n}\n\n/**\n * Get weight distribution icon\n */\nfunction getWeightIcon(weight: \"forward\" | \"neutral\" | \"back\"): string {\n switch (weight) {\n case \"forward\":\n return \"▲\"; // Forward lean\n case \"back\":\n return \"▼\"; // Back lean\n case \"neutral\":\n return \"●\"; // Centered\n }\n}\n\n/**\n * GuardIndicator Component\n *\n * Displays current guard position information with Korean martial arts terminology.\n * Shows only when player is in a stance guard animation state.\n *\n * Features:\n * - Traditional Korean stance name (앞서기, 앞굽이, etc.)\n * - Romanized pronunciation\n * - Guard height indicator (High/Mid/Low)\n * - Weight distribution visualization\n * - Korean cyberpunk styling with glow effects\n * - Responsive mobile layout\n * - Accessible with proper ARIA labels\n *\n * @example\n * ```tsx\n * <GuardIndicator\n * currentStance={TrigramStance.GEON}\n * isInGuard={true}\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n *\n * @korean 방어자세표시기\n */\nexport const GuardIndicator: React.FC<GuardIndicatorProps> = ({\n currentStance,\n isInGuard,\n position: _position, // Currently unused - indicator always positioned bottom-right\n isMobile = false,\n}) => {\n const config = useMemo(\n () => STANCE_GUARD_CONFIGS[currentStance],\n [currentStance],\n );\n\n const trigramData = useMemo(\n () => TRIGRAM_DATA[currentStance],\n [currentStance],\n );\n\n const stanceName = useMemo(\n () => getTraditionalStanceName(currentStance),\n [currentStance],\n );\n\n const guardHeight = useMemo(\n () => getGuardHeight(currentStance),\n [currentStance],\n );\n\n const weightIcon = useMemo(\n () => getWeightIcon(config.guardPose.weight),\n [config.guardPose.weight],\n );\n\n const layout = useMemo(\n () => ({\n fontSize: isMobile ? 10 : 12,\n titleSize: isMobile ? 13 : 16,\n iconSize: isMobile ? 14 : 18,\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n gap: isMobile ? \"3px\" : \"4px\",\n }),\n [isMobile],\n );\n\n const containerStyle = useMemo(\n () => ({\n position: \"relative\" as const,\n display: \"flex\",\n flexDirection: \"column\" as const,\n gap: layout.gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n padding: layout.padding,\n pointerEvents: \"none\" as const,\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [layout],\n );\n\n if (!isInGuard) return null;\n\n return (\n <div\n data-testid=\"guard-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Guard position: ${stanceName.romanized}, ${guardHeight.english} guard, ${config.guardPose.weight} weight`}\n style={containerStyle}\n >\n {/* Title: GUARD with trigram symbol */}\n <div\n style={{\n fontSize: layout.titleSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(\n KOREAN_COLORS.ACCENT_GOLD,\n 0.6,\n )}`,\n borderBottom: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.4,\n )}`,\n paddingBottom: layout.gap,\n }}\n >\n {trigramData.symbol} GUARD\n </div>\n\n {/* Traditional stance name (Korean) */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: \"center\",\n fontWeight: \"600\",\n }}\n >\n {stanceName.korean}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8),\n textAlign: \"center\",\n fontStyle: \"italic\",\n }}\n >\n {stanceName.romanized}\n </div>\n\n {/* Guard characteristics row */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-around\",\n marginTop: layout.gap,\n paddingTop: layout.gap,\n borderTop: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n }}\n >\n {/* Guard height */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n ⚔️\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {guardHeight.korean}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n }}\n >\n {guardHeight.english}\n </div>\n </div>\n\n {/* Weight distribution */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n {weightIcon}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {config.guardPose.weight === \"forward\" && \"전방\"}\n {config.guardPose.weight === \"neutral\" && \"중립\"}\n {config.guardPose.weight === \"back\" && \"후방\"}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n textTransform: \"capitalize\" as const,\n }}\n >\n {config.guardPose.weight}\n </div>\n </div>\n </div>\n </div>\n );\n};\n\n/**\n * Memoized GuardIndicator to prevent unnecessary re-renders\n */\nexport default React.memo(GuardIndicator);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"GuardIndicator.js","names":[],"sources":["../../../../../src/components/shared/three/indicators/GuardIndicator.tsx"],"sourcesContent":["/**\n * GuardIndicator - Visual indicator showing current fighting stance guard position\n * Displays Korean traditional stance name, English translation, and guard characteristics\n *\n * @module components/shared/three/indicators/GuardIndicator\n * @category Combat UI\n * @korean 방어자세표시기\n */\n\nimport React, { useMemo } from \"react\";\nimport { STANCE_GUARD_CONFIGS } from \"../../../../systems/animation\";\nimport { TRIGRAM_DATA } from \"../../../../systems/trigram/types\";\nimport { TrigramStance } from \"../../../../types/common\";\nimport { KOREAN_COLORS } from \"../../../../types/constants\";\nimport { hexToRgbaString } from \"../../../../utils/colorUtils\";\n\n/**\n * Props for GuardIndicator component\n */\nexport interface GuardIndicatorProps {\n /** Current trigram stance */\n readonly currentStance: TrigramStance;\n /** Whether player is in guard animation state */\n readonly isInGuard: boolean;\n /** Player 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 * Get guard height label based on arm positions\n *\n * Guard heights based on shoulder.x (vertical angle in radians):\n * - High guard (고위): shoulder at chin level (x <= -0.8)\n * GEON (-1.0), LI (-1.6), GAN (-1.0)\n * - Mid guard (중위): shoulder at chest level (-0.8 < x < -0.4)\n * TAE (-0.7), JIN (-0.6), SON (-0.8), GAM (-0.8)\n * - Low guard (저위): shoulder low (x >= -0.4)\n * GON (-0.4)\n */\nfunction getGuardHeight(stance: TrigramStance): {\n korean: string;\n english: string;\n} {\n const config = STANCE_GUARD_CONFIGS[stance];\n const shoulderY = config.guardPose.leftArm.shoulder.x; // x is vertical in Euler\n\n if (shoulderY <= -0.8) {\n return { korean: \"고위\", english: \"High\" };\n } else if (shoulderY < -0.4) {\n return { korean: \"중위\", english: \"Mid\" };\n } else {\n return { korean: \"저위\", english: \"Low\" };\n }\n}\n\n/**\n * Get traditional Korean stance name from guard config\n */\nfunction getTraditionalStanceName(stance: TrigramStance): {\n korean: string;\n romanized: string;\n} {\n const stanceNames: Record<\n TrigramStance,\n { korean: string; romanized: string }\n > = {\n [TrigramStance.GEON]: { korean: \"앞서기\", romanized: \"Ap Seogi\" },\n [TrigramStance.TAE]: { korean: \"범서기\", romanized: \"Beom Seogi\" },\n [TrigramStance.LI]: { korean: \"겨루기 준비\", romanized: \"Gyeorugi Junbi\" },\n [TrigramStance.JIN]: { korean: \"주춤서기\", romanized: \"Juchum Seogi\" },\n [TrigramStance.SON]: { korean: \"학다리서기\", romanized: \"Hakdari Seogi\" },\n [TrigramStance.GAM]: { korean: \"뒷발서기\", romanized: \"Dwitbal Seogi\" },\n [TrigramStance.GAN]: { korean: \"모아서기\", romanized: \"Moa Seogi\" },\n [TrigramStance.GON]: { korean: \"중하\", romanized: \"Joong Ha Seogi\" },\n };\n return stanceNames[stance];\n}\n\n/**\n * Get weight distribution icon\n */\nfunction getWeightIcon(weight: \"forward\" | \"neutral\" | \"back\"): string {\n switch (weight) {\n case \"forward\":\n return \"▲\"; // Forward lean\n case \"back\":\n return \"▼\"; // Back lean\n case \"neutral\":\n return \"●\"; // Centered\n }\n}\n\n/**\n * GuardIndicator Component\n *\n * Displays current guard position information with Korean martial arts terminology.\n * Shows only when player is in a stance guard animation state.\n *\n * Features:\n * - Traditional Korean stance name (앞서기, 앞굽이, etc.)\n * - Romanized pronunciation\n * - Guard height indicator (High/Mid/Low)\n * - Weight distribution visualization\n * - Korean cyberpunk styling with glow effects\n * - Responsive mobile layout\n * - Accessible with proper ARIA labels\n *\n * @example\n * ```tsx\n * <GuardIndicator\n * currentStance={TrigramStance.GEON}\n * isInGuard={true}\n * position=\"left\"\n * isMobile={false}\n * />\n * ```\n *\n * @korean 방어자세표시기\n */\nexport const GuardIndicator: React.FC<GuardIndicatorProps> = ({\n currentStance,\n isInGuard,\n position: _position, // Currently unused - indicator always positioned bottom-right\n isMobile = false,\n}) => {\n const config = useMemo(\n () => STANCE_GUARD_CONFIGS[currentStance],\n [currentStance],\n );\n\n const trigramData = useMemo(\n () => TRIGRAM_DATA[currentStance],\n [currentStance],\n );\n\n const stanceName = useMemo(\n () => getTraditionalStanceName(currentStance),\n [currentStance],\n );\n\n const guardHeight = useMemo(\n () => getGuardHeight(currentStance),\n [currentStance],\n );\n\n const weightIcon = useMemo(\n () => getWeightIcon(config.guardPose.weight),\n [config.guardPose.weight],\n );\n\n const layout = useMemo(\n () => ({\n fontSize: isMobile ? 10 : 12,\n titleSize: isMobile ? 13 : 16,\n iconSize: isMobile ? 14 : 18,\n padding: isMobile ? \"6px 10px\" : \"8px 12px\",\n gap: isMobile ? \"3px\" : \"4px\",\n }),\n [isMobile],\n );\n\n const containerStyle = useMemo(\n () => ({\n position: \"relative\" as const,\n display: \"flex\",\n flexDirection: \"column\" as const,\n gap: layout.gap,\n backgroundColor: hexToRgbaString(KOREAN_COLORS.UI_BACKGROUND_DARK, 0.85),\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.6)}`,\n borderRadius: \"6px\",\n padding: layout.padding,\n pointerEvents: \"none\" as const,\n width: \"100%\",\n boxSizing: \"border-box\" as const,\n boxShadow: `0 0 15px ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.3)}`,\n }),\n [layout],\n );\n\n if (!isInGuard) return null;\n\n return (\n <div\n data-testid=\"guard-indicator\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={`Guard position: ${stanceName.romanized}, ${guardHeight.english} guard, ${config.guardPose.weight} weight`}\n style={containerStyle}\n >\n {/* Title: GUARD with trigram symbol */}\n <div\n style={{\n fontSize: layout.titleSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 1),\n fontWeight: \"bold\",\n textAlign: \"center\",\n textShadow: `0 0 10px ${hexToRgbaString(\n KOREAN_COLORS.ACCENT_GOLD,\n 0.6,\n )}`,\n borderBottom: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.4,\n )}`,\n paddingBottom: layout.gap,\n }}\n >\n {trigramData.symbol} GUARD\n </div>\n\n {/* Traditional stance name (Korean) */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n textAlign: \"center\",\n fontWeight: \"600\",\n }}\n >\n {stanceName.korean}\n </div>\n\n {/* Romanized name */}\n <div\n style={{\n fontSize: layout.fontSize,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.8),\n textAlign: \"center\",\n fontStyle: \"italic\",\n }}\n >\n {stanceName.romanized}\n </div>\n\n {/* Guard characteristics row */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-around\",\n marginTop: layout.gap,\n paddingTop: layout.gap,\n borderTop: `1px solid ${hexToRgbaString(\n KOREAN_COLORS.PRIMARY_CYAN,\n 0.3,\n )}`,\n }}\n >\n {/* Guard height */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n ⚔️\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {guardHeight.korean}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n }}\n >\n {guardHeight.english}\n </div>\n </div>\n\n {/* Weight distribution */}\n <div style={{ textAlign: \"center\" }}>\n <div\n style={{\n fontSize: layout.iconSize,\n color: hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 1),\n }}\n >\n {weightIcon}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 1,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.9),\n }}\n >\n {config.guardPose.weight === \"forward\" && \"전방\"}\n {config.guardPose.weight === \"neutral\" && \"중립\"}\n {config.guardPose.weight === \"back\" && \"후방\"}\n </div>\n <div\n style={{\n fontSize: layout.fontSize - 2,\n color: hexToRgbaString(KOREAN_COLORS.ACCENT_GOLD, 0.6),\n textTransform: \"capitalize\" as const,\n }}\n >\n {config.guardPose.weight}\n </div>\n </div>\n </div>\n </div>\n );\n};\n\n/**\n * Memoized GuardIndicator to prevent unnecessary re-renders\n */\nexport default React.memo(GuardIndicator);\n"],"mappings":"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"}
|