blacktrigram 0.7.68 → 0.7.69
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/assets/index.css +987 -987
- package/lib/audio/AudioAssetLoader.js.map +1 -1
- package/lib/audio/AudioAssetRegistry.js +2 -2
- package/lib/audio/AudioAssetRegistry.js.map +1 -1
- package/lib/audio/AudioCache.js.map +1 -1
- package/lib/audio/AudioManager.js +1 -1
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/audio/AudioMonitor.js.map +1 -1
- package/lib/audio/AudioPool.js.map +1 -1
- package/lib/audio/AudioProvider.js.map +1 -1
- package/lib/audio/AudioUtils.js.map +1 -1
- package/lib/audio/BoneImpactAudioMap.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +3 -3
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.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.map +1 -1
- package/lib/components/screens/combat/components/effects/CombatParticleEffects3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/BalanceIndicator.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- 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/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 +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.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/TrainingDummy3D.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.map +1 -1
- package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/HapticController.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.map +1 -1
- 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/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/ElementalColorSystem.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.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/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/data/techniques.js.map +1 -1
- package/lib/hooks/useCombatTimer.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +2 -2
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/hooks/useTechniqueSelection.js.map +1 -1
- package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
- package/lib/index.js +2 -2
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/EffectCalculator.js.map +1 -1
- package/lib/systems/ResponsiveScaling.js.map +1 -1
- package/lib/systems/TrigramSystem.js.map +1 -1
- package/lib/systems/ai/AIPersonality.js.map +1 -1
- package/lib/systems/ai/ArchetypeEnforcer.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeConfig.js.map +1 -1
- package/lib/systems/animation/builders/KeyframeInterpolation.js.map +1 -1
- package/lib/systems/animation/builders/MartialArtsAnimationBuilder.js.map +1 -1
- package/lib/systems/animation/builders/SkeletonRig.js +1 -1
- package/lib/systems/animation/builders/SkeletonRig.js.map +1 -1
- package/lib/systems/animation/catalogs/AttackAnimations.js +1 -1
- package/lib/systems/animation/catalogs/AttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/BasicAnimations.js +1 -1
- package/lib/systems/animation/catalogs/BasicAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ComboAnimations.js +1 -1
- package/lib/systems/animation/catalogs/ComboAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DarkOpsAnimations.js +1 -1
- package/lib/systems/animation/catalogs/DarkOpsAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/DefensiveAnimations.js +2 -11
- package/lib/systems/animation/catalogs/DefensiveAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/ElbowKneeAnimations.js +1 -1
- package/lib/systems/animation/catalogs/ElbowKneeAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js +1 -1
- package/lib/systems/animation/catalogs/FootworkSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamRedirectionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GamTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanStanceAnimations.js +1 -1
- package/lib/systems/animation/catalogs/GanStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GanTechniqueAnimations.js +1 -1
- package/lib/systems/animation/catalogs/GanTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GeonStanceAnimations.js +1 -1
- package/lib/systems/animation/catalogs/GeonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js +1 -1
- package/lib/systems/animation/catalogs/GonTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/GrapplingAnimations.js +1 -1
- package/lib/systems/animation/catalogs/GrapplingAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinStanceAnimations.js +1 -1
- package/lib/systems/animation/catalogs/JinStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/JinTechniqueAnimations.js +1 -1
- package/lib/systems/animation/catalogs/JinTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/KickAnimations.js +1 -1
- package/lib/systems/animation/catalogs/KickAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js +1 -1
- package/lib/systems/animation/catalogs/LiStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/LiTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/MovementAnimations.js +1 -1
- package/lib/systems/animation/catalogs/MovementAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js +1 -1
- package/lib/systems/animation/catalogs/PunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonStanceAnimations.js +1 -1
- package/lib/systems/animation/catalogs/SonStanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SonTechniqueAnimations.js +1 -1
- package/lib/systems/animation/catalogs/SonTechniqueAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js +1 -1
- package/lib/systems/animation/catalogs/SpecializedPunchAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAnimations.js +1 -1
- package/lib/systems/animation/catalogs/StanceAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceAttackAnimations.js +215 -272
- package/lib/systems/animation/catalogs/StanceAttackAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceIdleAnimations.js +2 -11
- package/lib/systems/animation/catalogs/StanceIdleAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js +2 -2
- package/lib/systems/animation/catalogs/StanceLocomotionAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/StepSkeletalAnimations.js +1 -1
- package/lib/systems/animation/catalogs/StepSkeletalAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeJointLockAnimations.js.map +1 -1
- package/lib/systems/animation/catalogs/TaeStanceAnimations.js +1 -1
- package/lib/systems/animation/catalogs/TaeStanceAnimations.js.map +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js +1 -1
- package/lib/systems/animation/core/AnimationOptimizations.js.map +1 -1
- package/lib/systems/animation/core/AnimationPriority.js.map +1 -1
- package/lib/systems/animation/core/AnimationRegistry.js +11 -209
- package/lib/systems/animation/core/AnimationRegistry.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js +2 -2
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapper.js +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapper.js.map +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapping.js +1 -1
- package/lib/systems/animation/core/TechniqueAnimationMapping.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/BodyPartDamageIntegration.js.map +1 -1
- package/lib/systems/bodypart/BodyPartHealthSystem.js.map +1 -1
- package/lib/systems/bodypart/BodyPartPositionMapping.js.map +1 -1
- package/lib/systems/bodypart/InjuryTracker.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/BreakingStatusEffects.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/painConsciousnessUtils.js.map +1 -1
- package/lib/systems/combat/typeGuards.js.map +1 -1
- package/lib/systems/index.js +2 -2
- package/lib/systems/movement/integration.js.map +1 -1
- package/lib/systems/physics/AttackMovementPhysics.js.map +1 -1
- package/lib/systems/physics/CollisionDetection.js +1 -1
- package/lib/systems/physics/CollisionDetection.js.map +1 -1
- package/lib/systems/physics/KnockbackPhysics.js.map +1 -1
- package/lib/systems/physics/MovementPhysics.js.map +1 -1
- package/lib/systems/physics/PhysicalReachCalculator.js.map +1 -1
- package/lib/systems/physics/SpeedModifierSystem.js.map +1 -1
- package/lib/systems/trigram/KoreanTechniques.js.map +1 -1
- package/lib/systems/trigram/StanceManager.js.map +1 -1
- package/lib/systems/trigram/TransitionCalculator.js.map +1 -1
- package/lib/systems/trigram/TrigramCalculator.js.map +1 -1
- package/lib/systems/trigram/techniques/TechniqueConfig.js.map +1 -1
- package/lib/systems/trigram/techniques/index.js.map +1 -1
- package/lib/systems/trigram/types.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanAnatomy.js.map +1 -1
- package/lib/systems/vitalpoint/KoreanVitalPoints.js.map +1 -1
- package/lib/systems/vitalpoint/MeridianVitalPointMapping.js.map +1 -1
- package/lib/types/constants/designSystem.js.map +1 -1
- package/lib/utils/colorHelpers.js.map +1 -1
- package/lib/utils/colorUtils.js.map +1 -1
- package/lib/utils/controlMapping.js.map +1 -1
- package/lib/utils/deviceDetection.js.map +1 -1
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/fabricTextures.js.map +1 -1
- package/lib/utils/htmlOverlayHelpers.js.map +1 -1
- package/lib/utils/performance/PerformanceMonitor.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/safeAreaUtils.js.map +1 -1
- package/lib/utils/stanceHelpers.js.map +1 -1
- package/lib/utils/visualEffects.js.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CombatControlsPanel.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/CombatControlsPanel.tsx"],"sourcesContent":["/**\n * CombatControlsPanel - Displays controls guide and combat messages log\n *\n * Shows keyboard/touch controls on the left and scrolling combat log on the right.\n * Positioned at the bottom of the combat screen above the back button.\n *\n * Refactored to use useKoreanTheme for consistent theming.\n *\n * @module components/combat/components/CombatControlsPanel\n * @category Combat UI\n * @korean 전투컨트롤패널\n */\n\nimport React from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\n/** Ratio of screen height used for message log max height */\nconst MESSAGE_LOG_HEIGHT_RATIO = 0.18;\n\nexport interface CombatControlsPanelProps {\n /** Combat message log (most recent messages) */\n readonly combatMessages: readonly string[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n /** Screen height for responsive sizing */\n readonly height?: number;\n}\n\n/**\n * CombatControlsPanel - Controls guide and combat log display\n * \n * Uses useKoreanTheme for consistent styling.\n *\n * @example\n * ```tsx\n * <CombatControlsPanel\n * combatMessages={combatState.combatMessages}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatControlsPanel: React.FC<CombatControlsPanelProps> = ({\n combatMessages,\n isMobile,\n height,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"sm\", isMobile });\n const panelBackground = hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.8);\n \n return (\n <div\n data-testid=\"combat-controls-panel\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"90px\" : \"100px\",\n left: isMobile ? \"5px\" : \"15px\",\n right: isMobile ? \"5px\" : \"15px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n pointerEvents: \"auto\",\n zIndex: 50,\n }}\n >\n {/* Controls Guide */}\n <div\n data-testid=\"combat-controls-guide\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n }}\n >\n <div style={{ fontSize: isMobile ? \"10px\" : \"12px\" }}>\n 조작법 | Controls: A/D - Attack/Defend | 1-8 - Stances\n </div>\n </div>\n\n {/* Combat Message Log */}\n <div\n data-testid=\"combat-message-log\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n maxHeight: height ? `${Math.round(height * MESSAGE_LOG_HEIGHT_RATIO)}px` : \"18vh\",\n overflow: \"auto\",\n }}\n >\n {combatMessages.slice(-5).map((msg, idx) => (\n <div\n key={`msg-${idx}`}\n style={{ fontSize: \"12px\", marginBottom: \"4px\" }}\n >\n {msg}\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nexport default CombatControlsPanel;\n"],"mappings":";;;;;;AAkBA,IAAM,2BAA2B;;;;;;;;;;;;;;AAwBjC,IAAa,uBAA2D,EACtE,gBACA,UACA,aACI;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;CAAS,CAAC;CACzE,MAAM,kBAAkB,gBAAgB,MAAM,OAAO,oBAAoB,EAAG;CAE5E,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,UAAU;GACV,QAAQ,WAAW,SAAS;GAC5B,MAAM,WAAW,QAAQ;GACzB,OAAO,WAAW,QAAQ;GAC1B,SAAS;GACT,gBAAgB;GAChB,eAAe;GACf,QAAQ;EACV;YAXF,CAcE,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACjE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACnD,YAAY,MAAM,WAAW;GAC/B;aAEA,oBAAC,OAAD;IAAK,OAAO,EAAE,UAAU,WAAW,SAAS,OAAO;cAAG;GAEjD,CAAA;EACF,CAAA,GAGL,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACjE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACnD,YAAY,MAAM,WAAW;IAC7B,WAAW,SAAS,GAAG,KAAK,MAAM,SAAS,wBAAwB,EAAE,MAAM;IAC3E,UAAU;GACZ;aAEC,eAAe,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"CombatControlsPanel.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/CombatControlsPanel.tsx"],"sourcesContent":["/**\n * CombatControlsPanel - Displays controls guide and combat messages log\n *\n * Shows keyboard/touch controls on the left and scrolling combat log on the right.\n * Positioned at the bottom of the combat screen above the back button.\n *\n * Refactored to use useKoreanTheme for consistent theming.\n *\n * @module components/combat/components/CombatControlsPanel\n * @category Combat UI\n * @korean 전투컨트롤패널\n */\n\nimport React from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\n\n/** Ratio of screen height used for message log max height */\nconst MESSAGE_LOG_HEIGHT_RATIO = 0.18;\n\nexport interface CombatControlsPanelProps {\n /** Combat message log (most recent messages) */\n readonly combatMessages: readonly string[];\n /** Whether to use mobile-optimized sizing */\n readonly isMobile: boolean;\n /** Screen height for responsive sizing */\n readonly height?: number;\n}\n\n/**\n * CombatControlsPanel - Controls guide and combat log display\n * \n * Uses useKoreanTheme for consistent styling.\n *\n * @example\n * ```tsx\n * <CombatControlsPanel\n * combatMessages={combatState.combatMessages}\n * isMobile={false}\n * />\n * ```\n */\nexport const CombatControlsPanel: React.FC<CombatControlsPanelProps> = ({\n combatMessages,\n isMobile,\n height,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"sm\", isMobile });\n const panelBackground = hexToRgbaString(theme.colors.UI_BACKGROUND_DARK, 0.8);\n \n return (\n <div\n data-testid=\"combat-controls-panel\"\n style={{\n position: \"absolute\",\n bottom: isMobile ? \"90px\" : \"100px\",\n left: isMobile ? \"5px\" : \"15px\",\n right: isMobile ? \"5px\" : \"15px\",\n display: \"flex\",\n justifyContent: \"space-between\",\n pointerEvents: \"auto\",\n zIndex: 50,\n }}\n >\n {/* Controls Guide */}\n <div\n data-testid=\"combat-controls-guide\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n }}\n >\n <div style={{ fontSize: isMobile ? \"10px\" : \"12px\" }}>\n 조작법 | Controls: A/D - Attack/Defend | 1-8 - Stances\n </div>\n </div>\n\n {/* Combat Message Log */}\n <div\n data-testid=\"combat-message-log\"\n style={{\n width: isMobile ? \"45%\" : \"400px\",\n background: panelBackground,\n border: `2px solid ${hexToRgbaString(theme.colors.PRIMARY_CYAN, 1)}`,\n borderRadius: \"8px\",\n padding: \"10px\",\n color: hexToRgbaString(theme.colors.PRIMARY_CYAN, 1),\n fontFamily: theme.fontFamily.KOREAN,\n maxHeight: height ? `${Math.round(height * MESSAGE_LOG_HEIGHT_RATIO)}px` : \"18vh\",\n overflow: \"auto\",\n }}\n >\n {combatMessages.slice(-5).map((msg, idx) => (\n <div\n key={`msg-${idx}`}\n style={{ fontSize: \"12px\", marginBottom: \"4px\" }}\n >\n {msg}\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nexport default CombatControlsPanel;\n"],"mappings":";;;;;;AAkBA,IAAM,2BAA2B;;;;;;;;;;;;;;AAwBjC,IAAa,uBAA2D,EACtE,gBACA,UACA,aACI;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;CAAS,CAAC;CACzE,MAAM,kBAAkB,gBAAgB,MAAM,OAAO,oBAAoB,EAAG;CAE5E,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,OAAO;GACL,UAAU;GACV,QAAQ,WAAW,SAAS;GAC5B,MAAM,WAAW,QAAQ;GACzB,OAAO,WAAW,QAAQ;GAC1B,SAAS;GACT,gBAAgB;GAChB,eAAe;GACf,QAAQ;EACV;YAXF,CAcE,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACjE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACnD,YAAY,MAAM,WAAW;GAC/B;aAEA,oBAAC,OAAD;IAAK,OAAO,EAAE,UAAU,WAAW,SAAS,OAAO;cAAG;GAEjD,CAAA;EACF,CAAA,GAGL,oBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,OAAO,WAAW,QAAQ;IAC1B,YAAY;IACZ,QAAQ,aAAa,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACjE,cAAc;IACd,SAAS;IACT,OAAO,gBAAgB,MAAM,OAAO,cAAc,CAAC;IACnD,YAAY,MAAM,WAAW;IAC7B,WAAW,SAAS,GAAG,KAAK,MAAM,SAAS,wBAAwB,EAAE,MAAM;IAC3E,UAAU;GACZ;aAEC,eAAe,MAAM,EAAE,CAAC,CAAC,KAAK,KAAK,QAClC,oBAAC,OAAD;IAEE,OAAO;KAAE,UAAU;KAAQ,cAAc;IAAM;cAE9C;GACE,GAJE,OAAO,KAIT,CACN;EACE,CAAA,CACF;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PauseMenu.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenu.tsx"],"sourcesContent":["/**\n * PauseMenu Component - Main pause menu overlay\n *\n * Features:\n * - Resume combat\n * - Restart match\n * - View controls\n * - Adjust settings\n * - Return to menu (with confirmation)\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - WCAG 2.1 Level AA compliant\n * - Keyboard navigation (Arrow keys, Enter, Escape)\n * - Focus indicators with high contrast\n * - ARIA labels for screen readers\n * \n * Refactored to use useKoreanTheme for consistent theming\n */\n\nimport React, { useCallback, useMemo, useState, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport ConfirmDialog from \"../../../../shared/ui/shared/ConfirmDialog\";\nimport ControlsGuide from \"./ControlsGuide\";\nimport QuickSettings from \"./QuickSettings\";\nimport { handleKeyboardNav } from \"../../../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../../../types/AccessibilityTypes\";\nimport { PauseMenuButton } from \"./PauseMenuButton\";\n\nexport interface PauseMenuProps {\n readonly onResume: () => void;\n readonly onRestart: () => void;\n readonly onReturnToMenu: () => void;\n readonly isMobile: boolean;\n}\n\ninterface MenuItem {\n readonly key: string;\n readonly labelKorean: string;\n readonly labelEnglish: string;\n readonly testId: string;\n readonly onClick: () => void;\n readonly icon?: string;\n}\n\n/**\n * PauseMenu - Main pause menu with options and submenus\n */\nexport const PauseMenu: React.FC<PauseMenuProps> = ({\n onResume,\n onRestart,\n onReturnToMenu,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n const themeColors = useMemo(() => ({\n overlayBg: hexToRgbaString(theme.colors.BLACK_SOLID, 0.85),\n accentGold: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n accentGoldGlow: hexToRgbaString(theme.colors.ACCENT_GOLD, 0.6),\n textSecondary: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.8),\n }), [theme.colors.BLACK_SOLID, theme.colors.ACCENT_GOLD, theme.colors.TEXT_SECONDARY]);\n const [activeSubmenu, setActiveSubmenu] = useState<\n \"controls\" | \"settings\" | null\n >(null);\n const [showConfirm, setShowConfirm] = useState<\n \"restart\" | \"menu\" | null\n >(null);\n const [focusedIndex, setFocusedIndex] = useState<number>(0);\n const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const menuItems: MenuItem[] = React.useMemo(\n () => [\n {\n key: \"resume\",\n labelKorean: \"계속\",\n labelEnglish: \"Resume\",\n testId: \"pause-resume-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n onResume();\n },\n icon: \"▶️\",\n },\n {\n key: \"restart\",\n labelKorean: \"재시작\",\n labelEnglish: \"Restart Match\",\n testId: \"pause-restart-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"restart\");\n },\n icon: \"🔄\",\n },\n {\n key: \"controls\",\n labelKorean: \"조작법\",\n labelEnglish: \"Controls\",\n testId: \"pause-controls-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"controls\");\n },\n icon: \"🎮\",\n },\n {\n key: \"settings\",\n labelKorean: \"설정\",\n labelEnglish: \"Settings\",\n testId: \"pause-settings-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"settings\");\n },\n icon: \"⚙️\",\n },\n {\n key: \"menu\",\n labelKorean: \"메인 메뉴\",\n labelEnglish: \"Return to Menu\",\n testId: \"pause-menu-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"menu\");\n },\n icon: \"🏠\",\n },\n ],\n [audio, onResume]\n );\n\n useEffect(() => {\n if (buttonRefs.current[0]) {\n buttonRefs.current[0].focus();\n }\n }, [menuItems]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent, index: number) => {\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n menuItems[index].onClick();\n },\n onCancel: () => {\n onResume();\n },\n onNavigate: (direction) => {\n if (direction === 'up') {\n const newIndex = (index - 1 + menuItems.length) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n } else if (direction === 'down') {\n const newIndex = (index + 1) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n }\n },\n });\n },\n [menuItems, onResume]\n );\n\n\n return (\n <>\n {/* Main Pause Menu */}\n <div\n data-testid=\"pause-menu\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pause-title\"\n aria-describedby=\"pause-hint\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: themeColors.overlayBg,\n backdropFilter: \"blur(8px)\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n }}\n >\n {/* Pause Title */}\n <h1\n id=\"pause-title\"\n data-testid=\"pause-title\"\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: themeColors.accentGold,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"40px\" : \"60px\"} 0`,\n textShadow: `0 0 30px ${themeColors.accentGoldGlow}`,\n textAlign: \"center\",\n }}\n >\n 일시정지 | Paused\n </h1>\n\n {/* Menu Buttons */}\n <div\n role=\"menu\"\n aria-label={createBilingualLabel('일시정지 메뉴', 'Pause Menu').label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n }}\n >\n {menuItems.map((item, index) => (\n <PauseMenuButton\n key={item.key}\n ref={(el) => { buttonRefs.current[index] = el; }}\n labelKorean={item.labelKorean}\n labelEnglish={item.labelEnglish}\n icon={item.icon}\n onClick={item.onClick}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n onKeyDown={(e) => handleKeyDown(e, index)}\n onFocus={() => setFocusedIndex(index)}\n isFocused={focusedIndex === index}\n isMobile={isMobile}\n testId={item.testId}\n />\n ))}\n </div>\n\n {/* ESC hint */}\n <div\n id=\"pause-hint\"\n data-testid=\"pause-hint\"\n style={{\n marginTop: isMobile ? \"40px\" : \"60px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: themeColors.textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n ESC 키를 눌러 계속 | Press ESC to resume\n <br />\n ↑↓ 키로 이동 | Use ↑↓ to navigate\n </div>\n </div>\n\n {/* Controls Guide Submenu */}\n {activeSubmenu === \"controls\" && (\n <ControlsGuide\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Settings Submenu */}\n {activeSubmenu === \"settings\" && (\n <QuickSettings\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Restart Confirmation Dialog */}\n {showConfirm === \"restart\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Restart Match?\"\n titleKorean=\"경기를 재시작하시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 초기화됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onRestart();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Return to Menu Confirmation Dialog */}\n {showConfirm === \"menu\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Return to Menu?\"\n titleKorean=\"메인 메뉴로 돌아가시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 손실됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onReturnToMenu();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n </>\n );\n};\n\nexport default PauseMenu;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PauseMenu.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/controls/PauseMenu.tsx"],"sourcesContent":["/**\n * PauseMenu Component - Main pause menu overlay\n *\n * Features:\n * - Resume combat\n * - Restart match\n * - View controls\n * - Adjust settings\n * - Return to menu (with confirmation)\n * - Korean/English bilingual text\n * - Cyberpunk Korean theming\n * - Backdrop blur effect\n * - WCAG 2.1 Level AA compliant\n * - Keyboard navigation (Arrow keys, Enter, Escape)\n * - Focus indicators with high contrast\n * - ARIA labels for screen readers\n * \n * Refactored to use useKoreanTheme for consistent theming\n */\n\nimport React, { useCallback, useMemo, useState, useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\nimport { hexToRgbaString } from \"../../../../../utils/colorUtils\";\nimport ConfirmDialog from \"../../../../shared/ui/shared/ConfirmDialog\";\nimport ControlsGuide from \"./ControlsGuide\";\nimport QuickSettings from \"./QuickSettings\";\nimport { handleKeyboardNav } from \"../../../../../utils/accessibility\";\nimport { createBilingualLabel } from \"../../../../../types/AccessibilityTypes\";\nimport { PauseMenuButton } from \"./PauseMenuButton\";\n\nexport interface PauseMenuProps {\n readonly onResume: () => void;\n readonly onRestart: () => void;\n readonly onReturnToMenu: () => void;\n readonly isMobile: boolean;\n}\n\ninterface MenuItem {\n readonly key: string;\n readonly labelKorean: string;\n readonly labelEnglish: string;\n readonly testId: string;\n readonly onClick: () => void;\n readonly icon?: string;\n}\n\n/**\n * PauseMenu - Main pause menu with options and submenus\n */\nexport const PauseMenu: React.FC<PauseMenuProps> = ({\n onResume,\n onRestart,\n onReturnToMenu,\n isMobile,\n}) => {\n const audio = useAudio();\n const theme = useKoreanTheme({ variant: \"primary\", size: \"lg\", isMobile });\n const themeColors = useMemo(() => ({\n overlayBg: hexToRgbaString(theme.colors.BLACK_SOLID, 0.85),\n accentGold: hexToRgbaString(theme.colors.ACCENT_GOLD, 1),\n accentGoldGlow: hexToRgbaString(theme.colors.ACCENT_GOLD, 0.6),\n textSecondary: hexToRgbaString(theme.colors.TEXT_SECONDARY, 0.8),\n }), [theme.colors.BLACK_SOLID, theme.colors.ACCENT_GOLD, theme.colors.TEXT_SECONDARY]);\n const [activeSubmenu, setActiveSubmenu] = useState<\n \"controls\" | \"settings\" | null\n >(null);\n const [showConfirm, setShowConfirm] = useState<\n \"restart\" | \"menu\" | null\n >(null);\n const [focusedIndex, setFocusedIndex] = useState<number>(0);\n const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const menuItems: MenuItem[] = React.useMemo(\n () => [\n {\n key: \"resume\",\n labelKorean: \"계속\",\n labelEnglish: \"Resume\",\n testId: \"pause-resume-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n onResume();\n },\n icon: \"▶️\",\n },\n {\n key: \"restart\",\n labelKorean: \"재시작\",\n labelEnglish: \"Restart Match\",\n testId: \"pause-restart-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"restart\");\n },\n icon: \"🔄\",\n },\n {\n key: \"controls\",\n labelKorean: \"조작법\",\n labelEnglish: \"Controls\",\n testId: \"pause-controls-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"controls\");\n },\n icon: \"🎮\",\n },\n {\n key: \"settings\",\n labelKorean: \"설정\",\n labelEnglish: \"Settings\",\n testId: \"pause-settings-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setActiveSubmenu(\"settings\");\n },\n icon: \"⚙️\",\n },\n {\n key: \"menu\",\n labelKorean: \"메인 메뉴\",\n labelEnglish: \"Return to Menu\",\n testId: \"pause-menu-button\",\n onClick: () => {\n audio.playSFX(\"menu_select\");\n setShowConfirm(\"menu\");\n },\n icon: \"🏠\",\n },\n ],\n [audio, onResume]\n );\n\n useEffect(() => {\n if (buttonRefs.current[0]) {\n buttonRefs.current[0].focus();\n }\n }, [menuItems]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent, index: number) => {\n handleKeyboardNav(e.nativeEvent, {\n onActivate: () => {\n menuItems[index].onClick();\n },\n onCancel: () => {\n onResume();\n },\n onNavigate: (direction) => {\n if (direction === 'up') {\n const newIndex = (index - 1 + menuItems.length) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n } else if (direction === 'down') {\n const newIndex = (index + 1) % menuItems.length;\n setFocusedIndex(newIndex);\n buttonRefs.current[newIndex]?.focus();\n }\n },\n });\n },\n [menuItems, onResume]\n );\n\n\n return (\n <>\n {/* Main Pause Menu */}\n <div\n data-testid=\"pause-menu\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pause-title\"\n aria-describedby=\"pause-hint\"\n style={{\n position: \"fixed\",\n top: 0,\n left: 0,\n width: \"100%\",\n height: \"100%\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: themeColors.overlayBg,\n backdropFilter: \"blur(8px)\",\n zIndex: 1000,\n pointerEvents: \"auto\",\n }}\n >\n {/* Pause Title */}\n <h1\n id=\"pause-title\"\n data-testid=\"pause-title\"\n style={{\n fontSize: isMobile ? \"48px\" : \"72px\",\n color: themeColors.accentGold,\n fontFamily: theme.fontFamily.KOREAN,\n fontWeight: \"bold\",\n margin: `0 0 ${isMobile ? \"40px\" : \"60px\"} 0`,\n textShadow: `0 0 30px ${themeColors.accentGoldGlow}`,\n textAlign: \"center\",\n }}\n >\n 일시정지 | Paused\n </h1>\n\n {/* Menu Buttons */}\n <div\n role=\"menu\"\n aria-label={createBilingualLabel('일시정지 메뉴', 'Pause Menu').label}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: isMobile ? \"12px\" : \"16px\",\n minWidth: isMobile ? \"280px\" : \"360px\",\n }}\n >\n {menuItems.map((item, index) => (\n <PauseMenuButton\n key={item.key}\n ref={(el) => { buttonRefs.current[index] = el; }}\n labelKorean={item.labelKorean}\n labelEnglish={item.labelEnglish}\n icon={item.icon}\n onClick={item.onClick}\n onMouseEnter={() => audio.playSFX(\"menu_hover\")}\n onKeyDown={(e) => handleKeyDown(e, index)}\n onFocus={() => setFocusedIndex(index)}\n isFocused={focusedIndex === index}\n isMobile={isMobile}\n testId={item.testId}\n />\n ))}\n </div>\n\n {/* ESC hint */}\n <div\n id=\"pause-hint\"\n data-testid=\"pause-hint\"\n style={{\n marginTop: isMobile ? \"40px\" : \"60px\",\n fontSize: isMobile ? \"12px\" : \"14px\",\n color: themeColors.textSecondary,\n fontFamily: theme.fontFamily.KOREAN,\n textAlign: \"center\",\n }}\n >\n ESC 키를 눌러 계속 | Press ESC to resume\n <br />\n ↑↓ 키로 이동 | Use ↑↓ to navigate\n </div>\n </div>\n\n {/* Controls Guide Submenu */}\n {activeSubmenu === \"controls\" && (\n <ControlsGuide\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Settings Submenu */}\n {activeSubmenu === \"settings\" && (\n <QuickSettings\n onClose={() => {\n setActiveSubmenu(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Restart Confirmation Dialog */}\n {showConfirm === \"restart\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Restart Match?\"\n titleKorean=\"경기를 재시작하시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 초기화됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onRestart();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n\n {/* Return to Menu Confirmation Dialog */}\n {showConfirm === \"menu\" && (\n <ConfirmDialog\n isOpen={true}\n title=\"Return to Menu?\"\n titleKorean=\"메인 메뉴로 돌아가시겠습니까?\"\n message=\"All progress in the current match will be lost.\"\n messageKorean=\"현재 경기의 모든 진행 상황이 손실됩니다.\"\n onConfirm={() => {\n setShowConfirm(null);\n onReturnToMenu();\n }}\n onCancel={() => {\n setShowConfirm(null);\n }}\n isMobile={isMobile}\n />\n )}\n </>\n );\n};\n\nexport default PauseMenu;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAa,aAAuC,EAClD,UACA,WACA,gBACA,eACI;CACJ,MAAM,QAAQ,SAAS;CACvB,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAM;CAAS,CAAC;CACzE,MAAM,cAAc,eAAe;EACjC,WAAW,gBAAgB,MAAM,OAAO,aAAa,GAAI;EACzD,YAAY,gBAAgB,MAAM,OAAO,aAAa,CAAC;EACvD,gBAAgB,gBAAgB,MAAM,OAAO,aAAa,EAAG;EAC7D,eAAe,gBAAgB,MAAM,OAAO,gBAAgB,EAAG;CACjE,IAAI;EAAC,MAAM,OAAO;EAAa,MAAM,OAAO;EAAa,MAAM,OAAO;CAAc,CAAC;CACrF,MAAM,CAAC,eAAe,oBAAoB,SAExC,IAAI;CACN,MAAM,CAAC,aAAa,kBAAkB,SAEpC,IAAI;CACN,MAAM,CAAC,cAAc,mBAAmB,SAAiB,CAAC;CAC1D,MAAM,aAAa,OAAqC,CAAC,CAAC;CAE1D,MAAM,YAAwB,MAAM,cAC5B;EACJ;GACE,KAAK;GACL,aAAa;GACb,cAAc;GACd,QAAQ;GACR,eAAe;IACb,MAAM,QAAQ,aAAa;IAC3B,SAAS;GACX;GACA,MAAM;EACR;EACA;GACE,KAAK;GACL,aAAa;GACb,cAAc;GACd,QAAQ;GACR,eAAe;IACb,MAAM,QAAQ,aAAa;IAC3B,eAAe,SAAS;GAC1B;GACA,MAAM;EACR;EACA;GACE,KAAK;GACL,aAAa;GACb,cAAc;GACd,QAAQ;GACR,eAAe;IACb,MAAM,QAAQ,aAAa;IAC3B,iBAAiB,UAAU;GAC7B;GACA,MAAM;EACR;EACA;GACE,KAAK;GACL,aAAa;GACb,cAAc;GACd,QAAQ;GACR,eAAe;IACb,MAAM,QAAQ,aAAa;IAC3B,iBAAiB,UAAU;GAC7B;GACA,MAAM;EACR;EACA;GACE,KAAK;GACL,aAAa;GACb,cAAc;GACd,QAAQ;GACR,eAAe;IACb,MAAM,QAAQ,aAAa;IAC3B,eAAe,MAAM;GACvB;GACA,MAAM;EACR;CACF,GACA,CAAC,OAAO,QAAQ,CAClB;CAEA,gBAAgB;EACd,IAAI,WAAW,QAAQ,IACrB,WAAW,QAAQ,EAAE,CAAC,MAAM;CAEhC,GAAG,CAAC,SAAS,CAAC;CAEd,MAAM,gBAAgB,aACnB,GAAwB,UAAkB;EACzC,kBAAkB,EAAE,aAAa;GAC/B,kBAAkB;IAChB,UAAU,MAAM,CAAC,QAAQ;GAC3B;GACA,gBAAgB;IACd,SAAS;GACX;GACA,aAAa,cAAc;IACzB,IAAI,cAAc,MAAM;KACtB,MAAM,YAAY,QAAQ,IAAI,UAAU,UAAU,UAAU;KAC5D,gBAAgB,QAAQ;KACxB,WAAW,QAAQ,SAAS,EAAE,MAAM;IACtC,OAAO,IAAI,cAAc,QAAQ;KAC/B,MAAM,YAAY,QAAQ,KAAK,UAAU;KACzC,gBAAgB,QAAQ;KACxB,WAAW,QAAQ,SAAS,EAAE,MAAM;IACtC;GACF;EACF,CAAC;CACH,GACA,CAAC,WAAW,QAAQ,CACtB;CAGA,OACE,qBAAA,UAAA,EAAA,UAAA;EAEE,qBAAC,OAAD;GACE,eAAY;GACZ,MAAK;GACL,cAAW;GACX,mBAAgB;GAChB,oBAAiB;GACjB,OAAO;IACL,UAAU;IACV,KAAK;IACL,MAAM;IACN,OAAO;IACP,QAAQ;IACR,SAAS;IACT,eAAe;IACf,YAAY;IACZ,gBAAgB;IAChB,iBAAiB,YAAY;IAC7B,gBAAgB;IAChB,QAAQ;IACR,eAAe;GACjB;aApBF;IAuBE,oBAAC,MAAD;KACE,IAAG;KACH,eAAY;KACZ,OAAO;MACL,UAAU,WAAW,SAAS;MAC9B,OAAO,YAAY;MACnB,YAAY,MAAM,WAAW;MAC7B,YAAY;MACZ,QAAQ,OAAO,WAAW,SAAS,OAAO;MAC1C,YAAY,YAAY,YAAY;MACpC,WAAW;KACb;eACD;IAEG,CAAA;IAGJ,oBAAC,OAAD;KACE,MAAK;KACL,cAAY,qBAAqB,WAAW,YAAY,CAAC,CAAC;KAC1D,OAAO;MACL,SAAS;MACT,eAAe;MACf,KAAK,WAAW,SAAS;MACzB,UAAU,WAAW,UAAU;KACjC;eAEC,UAAU,KAAK,MAAM,UACpB,oBAAC,iBAAD;MAEE,MAAM,OAAO;OAAE,WAAW,QAAQ,SAAS;MAAI;MAC/C,aAAa,KAAK;MAClB,cAAc,KAAK;MACnB,MAAM,KAAK;MACX,SAAS,KAAK;MACd,oBAAoB,MAAM,QAAQ,YAAY;MAC9C,YAAY,MAAM,cAAc,GAAG,KAAK;MACxC,eAAe,gBAAgB,KAAK;MACpC,WAAW,iBAAiB;MAClB;MACV,QAAQ,KAAK;KACd,GAZM,KAAK,GAYX,CACF;IACE,CAAA;IAGL,qBAAC,OAAD;KACE,IAAG;KACH,eAAY;KACZ,OAAO;MACL,WAAW,WAAW,SAAS;MAC/B,UAAU,WAAW,SAAS;MAC9B,OAAO,YAAY;MACnB,YAAY,MAAM,WAAW;MAC7B,WAAW;KACb;eATF;MAUC;MAEC,oBAAC,MAAD,CAAK,CAAA;MAAC;KAEH;;GACF;;EAGJ,kBAAkB,cACjB,oBAAC,eAAD;GACE,eAAe;IACb,iBAAiB,IAAI;GACvB;GACU;EACX,CAAA;EAIF,kBAAkB,cACjB,oBAAC,eAAD;GACE,eAAe;IACb,iBAAiB,IAAI;GACvB;GACU;EACX,CAAA;EAIF,gBAAgB,aACf,oBAAC,eAAD;GACE,QAAQ;GACR,OAAM;GACN,aAAY;GACZ,SAAQ;GACR,eAAc;GACd,iBAAiB;IACf,eAAe,IAAI;IACnB,UAAU;GACZ;GACA,gBAAgB;IACd,eAAe,IAAI;GACrB;GACU;EACX,CAAA;EAIF,gBAAgB,UACf,oBAAC,eAAD;GACE,QAAQ;GACR,OAAM;GACN,aAAY;GACZ,SAAQ;GACR,eAAc;GACd,iBAAiB;IACf,eAAe,IAAI;IACnB,eAAe;GACjB;GACA,gBAAgB;IACd,eAAe,IAAI;GACrB;GACU;EACX,CAAA;CAEH,EAAA,CAAA;AAEN"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BloodDecals3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodDecals3D.tsx"],"sourcesContent":["/**\n * BloodDecals3D - Blood accumulation decal system for character models\n *\n * Projects blood decals onto 3D character surfaces using decal geometry for\n * persistent blood visualization. Decals fade over time and track injury\n * persistence across combat rounds.\n *\n * Features:\n * - Decal projection onto character meshes\n * - Blood accumulation from hits and lacerations\n * - Blood trail visualization\n * - Cross-round injury persistence\n * - Korean-themed blood visualization\n *\n * @module components/combat/BloodDecals3D\n * @category Combat Effects\n * @korean 피흔적3D\n */\n\nimport { DecalGeometry } from \"three/examples/jsm/geometries/DecalGeometry.js\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo, useEffect } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\n/**\n * Blood decal configuration\n */\nexport interface BloodDecal {\n /** Unique identifier */\n readonly id: string;\n /** Position in world space */\n readonly position: [number, number, number];\n /** Normal vector for surface projection */\n readonly normal: [number, number, number];\n /** Size of decal */\n readonly size: [number, number, number];\n /** Rotation around normal (radians) */\n readonly rotation: number;\n /** Opacity (0.0 to 1.0) */\n readonly opacity: number;\n /** Creation timestamp */\n readonly timestamp: number;\n /** Whether decal is from a laceration (adds blood trail) */\n readonly isLaceration?: boolean;\n}\n\n/**\n * Props for BloodDecals3D component\n */\nexport interface BloodDecals3DProps {\n /** Active blood decals to render */\n readonly decals: readonly BloodDecal[];\n /** Character mesh reference for decal projection */\n readonly targetMeshRef?: React.RefObject<THREE.Mesh>;\n /** Whether decals are enabled (violence settings) */\n readonly enabled?: boolean;\n /** Mobile mode (simplified decals) */\n readonly isMobile?: boolean;\n /** Decal fade duration in seconds */\n readonly fadeDuration?: number;\n /** Callback when decal fully fades */\n readonly onDecalComplete?: (decalId: string) => void;\n}\n\n/**\n * Blood decal constants\n */\nconst DECAL_CONSTANTS = {\n /** Default fade duration (seconds) */\n FADE_DURATION: 15.0,\n /** Base decal size */\n BASE_SIZE: [0.15, 0.15, 0.05] as [number, number, number],\n /** Laceration trail size multiplier */\n TRAIL_MULTIPLIER: 3.0,\n /** Maximum concurrent decals for performance */\n MAX_DECALS: 20,\n /** Mobile decal limit */\n MAX_DECALS_MOBILE: 10,\n} as const;\n\n/**\n * Generate blood decal texture (procedural)\n */\n/**\n * Generate a procedural blood texture with irregular edges.\n * Note: Uses Math.random() for visual variety. Each texture will be slightly different,\n * providing natural variation in blood splatter appearance across different decals.\n * This is intentional for realism, though it does create unique textures per component.\n * For better memory efficiency in production, consider pre-generating 3-5 variations\n * and randomly selecting one.\n */\nconst createBloodTexture = (): THREE.Texture => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 256;\n canvas.height = 256;\n const ctx = canvas.getContext(\"2d\");\n \n if (!ctx) {\n console.warn(\"Blood decal texture generation failed: Could not get 2D context\");\n const fallbackCanvas = document.createElement(\"canvas\");\n fallbackCanvas.width = 256;\n fallbackCanvas.height = 256;\n return new THREE.CanvasTexture(fallbackCanvas);\n }\n\n ctx.clearRect(0, 0, 256, 256);\n\n const centerX = 128;\n const centerY = 128;\n\n const gradient = ctx.createRadialGradient(\n centerX,\n centerY,\n 0,\n centerX,\n centerY,\n 100\n );\n\n const bloodColor = new THREE.Color(KOREAN_COLORS.BLOODLOSS_INDICATOR);\n const r = Math.floor(bloodColor.r * 255);\n const g = Math.floor(bloodColor.g * 255);\n const b = Math.floor(bloodColor.b * 255);\n\n gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1.0)`);\n gradient.addColorStop(0.6, `rgba(${r * 0.8}, ${g * 0.6}, ${b * 0.6}, 0.8)`);\n gradient.addColorStop(1, `rgba(${r * 0.5}, ${g * 0.3}, ${b * 0.3}, 0)`);\n\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, 256, 256);\n\n for (let i = 0; i < 30; i++) {\n const angle = (i / 30) * Math.PI * 2;\n const radius = 80 + Math.random() * 20;\n const x = centerX + Math.cos(angle) * radius;\n const y = centerY + Math.sin(angle) * radius;\n const size = 5 + Math.random() * 10;\n\n ctx.fillStyle = `rgba(${r * 0.7}, ${g * 0.4}, ${b * 0.4}, ${0.5 + Math.random() * 0.3})`;\n ctx.beginPath();\n ctx.arc(x, y, size, 0, Math.PI * 2);\n ctx.fill();\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.needsUpdate = true;\n return texture;\n};\n\n/**\n * Individual decal component\n */\nconst DecalMesh: React.FC<{\n decal: BloodDecal;\n texture: THREE.Texture;\n targetMesh?: THREE.Mesh;\n fadeDuration: number;\n}> = ({ decal, texture, targetMesh, fadeDuration }) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const materialRef = useRef<THREE.MeshBasicMaterial | null>(null);\n const ageRef = useRef(0);\n\n useEffect(() => {\n if (!meshRef.current || !targetMesh) return;\n\n try {\n const position = new THREE.Vector3(...decal.position);\n const size = new THREE.Vector3(...decal.size);\n\n const orientation = new THREE.Euler();\n orientation.copy(targetMesh.rotation);\n \n const decalGeometry = new DecalGeometry(\n targetMesh,\n position,\n orientation,\n size\n );\n\n meshRef.current.geometry = decalGeometry;\n \n meshRef.current.rotation.set(0, 0, decal.rotation);\n } catch (error) {\n if (process.env.NODE_ENV === \"development\") {\n console.warn(\"BloodDecals3D: Failed to project blood decal onto target mesh.\", {\n decalId: decal.id,\n position: decal.position,\n size: decal.size,\n error,\n });\n }\n }\n }, [decal, targetMesh]);\n\n useFrame((_, delta) => {\n if (!materialRef.current) return;\n\n ageRef.current += delta;\n const fadeProgress = Math.min(ageRef.current / fadeDuration, 1);\n materialRef.current.opacity = decal.opacity * (1 - fadeProgress);\n });\n\n return (\n <mesh ref={meshRef} data-testid={`blood-decal-${decal.id}`}>\n <meshBasicMaterial\n ref={materialRef}\n map={texture}\n transparent\n opacity={decal.opacity}\n depthTest={true}\n depthWrite={false}\n polygonOffset\n polygonOffsetFactor={-4}\n />\n </mesh>\n );\n};\n\n/**\n * BloodDecals3D Component\n *\n * Renders persistent blood decals on 3D character models using decal projection.\n * Decals fade over time and can persist across combat rounds for injury tracking.\n *\n * Performance optimized:\n * - Limited concurrent decals (20 desktop, 10 mobile)\n * - Efficient decal geometry generation\n * - Texture reuse across all decals\n * - Automatic cleanup of faded decals\n *\n * @example\n * ```tsx\n * const [bloodDecals, setBloodDecals] = useState<BloodDecal[]>([]);\n * const characterMeshRef = useRef<THREE.Mesh>(null);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], normal: [number, number, number]) => {\n * setBloodDecals([...bloodDecals, {\n * id: generateId(),\n * position,\n * normal,\n * size: [0.15, 0.15, 0.05],\n * rotation: Math.random() * Math.PI * 2,\n * opacity: 0.8,\n * timestamp: Date.now(),\n * isLaceration: false,\n * }]);\n * };\n *\n * <mesh ref={characterMeshRef}>\n * <capsuleGeometry args={[0.5, 1.6, 16, 32]} />\n * <meshStandardMaterial color={0xcccccc} />\n * </mesh>\n *\n * <BloodDecals3D\n * decals={bloodDecals}\n * targetMeshRef={characterMeshRef}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onDecalComplete={(id) => {\n * setBloodDecals(prev => prev.filter(d => d.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodDecals3D: React.FC<BloodDecals3DProps> = ({\n decals,\n targetMeshRef,\n enabled = true,\n isMobile = false,\n fadeDuration = DECAL_CONSTANTS.FADE_DURATION,\n onDecalComplete,\n}) => {\n const completedDecalsRef = useRef<Set<string>>(new Set());\n\n const maxDecals = isMobile\n ? DECAL_CONSTANTS.MAX_DECALS_MOBILE\n : DECAL_CONSTANTS.MAX_DECALS;\n\n const bloodTexture = useMemo(() => createBloodTexture(), []);\n\n const activeDecals = useMemo(() => {\n const sorted = [...decals].sort((a, b) => b.timestamp - a.timestamp);\n return sorted.slice(0, maxDecals);\n }, [decals, maxDecals]);\n\n const decalAgesRef = useRef<Map<string, number>>(new Map());\n\n useEffect(() => {\n activeDecals.forEach((decal) => {\n if (!decalAgesRef.current.has(decal.id)) {\n decalAgesRef.current.set(decal.id, 0);\n }\n });\n }, [activeDecals]);\n\n useFrame((_, delta) => {\n activeDecals.forEach((decal) => {\n const currentAge = (decalAgesRef.current.get(decal.id) ?? 0) + delta;\n decalAgesRef.current.set(decal.id, currentAge);\n \n const isExpired = currentAge >= fadeDuration;\n\n if (\n isExpired &&\n onDecalComplete &&\n !completedDecalsRef.current.has(decal.id)\n ) {\n completedDecalsRef.current.add(decal.id);\n onDecalComplete(decal.id);\n }\n });\n });\n\n useEffect(() => {\n return () => {\n bloodTexture.dispose();\n };\n }, [bloodTexture]);\n\n const [targetMesh, setTargetMesh] = React.useState<THREE.Mesh | undefined>();\n\n React.useLayoutEffect(() => {\n setTargetMesh(targetMeshRef?.current ?? undefined);\n }, [targetMeshRef]);\n\n if (!enabled || activeDecals.length === 0) {\n return null;\n }\n\n return (\n <group data-testid=\"blood-decals-3d\">\n {activeDecals.map((decal) => (\n <DecalMesh\n key={decal.id}\n decal={decal}\n texture={bloodTexture}\n targetMesh={targetMesh}\n fadeDuration={fadeDuration}\n />\n ))}\n </group>\n );\n};\n\nexport default BloodDecals3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BloodDecals3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodDecals3D.tsx"],"sourcesContent":["/**\n * BloodDecals3D - Blood accumulation decal system for character models\n *\n * Projects blood decals onto 3D character surfaces using decal geometry for\n * persistent blood visualization. Decals fade over time and track injury\n * persistence across combat rounds.\n *\n * Features:\n * - Decal projection onto character meshes\n * - Blood accumulation from hits and lacerations\n * - Blood trail visualization\n * - Cross-round injury persistence\n * - Korean-themed blood visualization\n *\n * @module components/combat/BloodDecals3D\n * @category Combat Effects\n * @korean 피흔적3D\n */\n\nimport { DecalGeometry } from \"three/examples/jsm/geometries/DecalGeometry.js\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo, useEffect } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\n\n/**\n * Blood decal configuration\n */\nexport interface BloodDecal {\n /** Unique identifier */\n readonly id: string;\n /** Position in world space */\n readonly position: [number, number, number];\n /** Normal vector for surface projection */\n readonly normal: [number, number, number];\n /** Size of decal */\n readonly size: [number, number, number];\n /** Rotation around normal (radians) */\n readonly rotation: number;\n /** Opacity (0.0 to 1.0) */\n readonly opacity: number;\n /** Creation timestamp */\n readonly timestamp: number;\n /** Whether decal is from a laceration (adds blood trail) */\n readonly isLaceration?: boolean;\n}\n\n/**\n * Props for BloodDecals3D component\n */\nexport interface BloodDecals3DProps {\n /** Active blood decals to render */\n readonly decals: readonly BloodDecal[];\n /** Character mesh reference for decal projection */\n readonly targetMeshRef?: React.RefObject<THREE.Mesh>;\n /** Whether decals are enabled (violence settings) */\n readonly enabled?: boolean;\n /** Mobile mode (simplified decals) */\n readonly isMobile?: boolean;\n /** Decal fade duration in seconds */\n readonly fadeDuration?: number;\n /** Callback when decal fully fades */\n readonly onDecalComplete?: (decalId: string) => void;\n}\n\n/**\n * Blood decal constants\n */\nconst DECAL_CONSTANTS = {\n /** Default fade duration (seconds) */\n FADE_DURATION: 15.0,\n /** Base decal size */\n BASE_SIZE: [0.15, 0.15, 0.05] as [number, number, number],\n /** Laceration trail size multiplier */\n TRAIL_MULTIPLIER: 3.0,\n /** Maximum concurrent decals for performance */\n MAX_DECALS: 20,\n /** Mobile decal limit */\n MAX_DECALS_MOBILE: 10,\n} as const;\n\n/**\n * Generate blood decal texture (procedural)\n */\n/**\n * Generate a procedural blood texture with irregular edges.\n * Note: Uses Math.random() for visual variety. Each texture will be slightly different,\n * providing natural variation in blood splatter appearance across different decals.\n * This is intentional for realism, though it does create unique textures per component.\n * For better memory efficiency in production, consider pre-generating 3-5 variations\n * and randomly selecting one.\n */\nconst createBloodTexture = (): THREE.Texture => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 256;\n canvas.height = 256;\n const ctx = canvas.getContext(\"2d\");\n \n if (!ctx) {\n console.warn(\"Blood decal texture generation failed: Could not get 2D context\");\n const fallbackCanvas = document.createElement(\"canvas\");\n fallbackCanvas.width = 256;\n fallbackCanvas.height = 256;\n return new THREE.CanvasTexture(fallbackCanvas);\n }\n\n ctx.clearRect(0, 0, 256, 256);\n\n const centerX = 128;\n const centerY = 128;\n\n const gradient = ctx.createRadialGradient(\n centerX,\n centerY,\n 0,\n centerX,\n centerY,\n 100\n );\n\n const bloodColor = new THREE.Color(KOREAN_COLORS.BLOODLOSS_INDICATOR);\n const r = Math.floor(bloodColor.r * 255);\n const g = Math.floor(bloodColor.g * 255);\n const b = Math.floor(bloodColor.b * 255);\n\n gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1.0)`);\n gradient.addColorStop(0.6, `rgba(${r * 0.8}, ${g * 0.6}, ${b * 0.6}, 0.8)`);\n gradient.addColorStop(1, `rgba(${r * 0.5}, ${g * 0.3}, ${b * 0.3}, 0)`);\n\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, 256, 256);\n\n for (let i = 0; i < 30; i++) {\n const angle = (i / 30) * Math.PI * 2;\n const radius = 80 + Math.random() * 20;\n const x = centerX + Math.cos(angle) * radius;\n const y = centerY + Math.sin(angle) * radius;\n const size = 5 + Math.random() * 10;\n\n ctx.fillStyle = `rgba(${r * 0.7}, ${g * 0.4}, ${b * 0.4}, ${0.5 + Math.random() * 0.3})`;\n ctx.beginPath();\n ctx.arc(x, y, size, 0, Math.PI * 2);\n ctx.fill();\n }\n\n const texture = new THREE.CanvasTexture(canvas);\n texture.needsUpdate = true;\n return texture;\n};\n\n/**\n * Individual decal component\n */\nconst DecalMesh: React.FC<{\n decal: BloodDecal;\n texture: THREE.Texture;\n targetMesh?: THREE.Mesh;\n fadeDuration: number;\n}> = ({ decal, texture, targetMesh, fadeDuration }) => {\n const meshRef = useRef<THREE.Mesh>(null);\n const materialRef = useRef<THREE.MeshBasicMaterial | null>(null);\n const ageRef = useRef(0);\n\n useEffect(() => {\n if (!meshRef.current || !targetMesh) return;\n\n try {\n const position = new THREE.Vector3(...decal.position);\n const size = new THREE.Vector3(...decal.size);\n\n const orientation = new THREE.Euler();\n orientation.copy(targetMesh.rotation);\n \n const decalGeometry = new DecalGeometry(\n targetMesh,\n position,\n orientation,\n size\n );\n\n meshRef.current.geometry = decalGeometry;\n \n meshRef.current.rotation.set(0, 0, decal.rotation);\n } catch (error) {\n if (process.env.NODE_ENV === \"development\") {\n console.warn(\"BloodDecals3D: Failed to project blood decal onto target mesh.\", {\n decalId: decal.id,\n position: decal.position,\n size: decal.size,\n error,\n });\n }\n }\n }, [decal, targetMesh]);\n\n useFrame((_, delta) => {\n if (!materialRef.current) return;\n\n ageRef.current += delta;\n const fadeProgress = Math.min(ageRef.current / fadeDuration, 1);\n materialRef.current.opacity = decal.opacity * (1 - fadeProgress);\n });\n\n return (\n <mesh ref={meshRef} data-testid={`blood-decal-${decal.id}`}>\n <meshBasicMaterial\n ref={materialRef}\n map={texture}\n transparent\n opacity={decal.opacity}\n depthTest={true}\n depthWrite={false}\n polygonOffset\n polygonOffsetFactor={-4}\n />\n </mesh>\n );\n};\n\n/**\n * BloodDecals3D Component\n *\n * Renders persistent blood decals on 3D character models using decal projection.\n * Decals fade over time and can persist across combat rounds for injury tracking.\n *\n * Performance optimized:\n * - Limited concurrent decals (20 desktop, 10 mobile)\n * - Efficient decal geometry generation\n * - Texture reuse across all decals\n * - Automatic cleanup of faded decals\n *\n * @example\n * ```tsx\n * const [bloodDecals, setBloodDecals] = useState<BloodDecal[]>([]);\n * const characterMeshRef = useRef<THREE.Mesh>(null);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], normal: [number, number, number]) => {\n * setBloodDecals([...bloodDecals, {\n * id: generateId(),\n * position,\n * normal,\n * size: [0.15, 0.15, 0.05],\n * rotation: Math.random() * Math.PI * 2,\n * opacity: 0.8,\n * timestamp: Date.now(),\n * isLaceration: false,\n * }]);\n * };\n *\n * <mesh ref={characterMeshRef}>\n * <capsuleGeometry args={[0.5, 1.6, 16, 32]} />\n * <meshStandardMaterial color={0xcccccc} />\n * </mesh>\n *\n * <BloodDecals3D\n * decals={bloodDecals}\n * targetMeshRef={characterMeshRef}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onDecalComplete={(id) => {\n * setBloodDecals(prev => prev.filter(d => d.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodDecals3D: React.FC<BloodDecals3DProps> = ({\n decals,\n targetMeshRef,\n enabled = true,\n isMobile = false,\n fadeDuration = DECAL_CONSTANTS.FADE_DURATION,\n onDecalComplete,\n}) => {\n const completedDecalsRef = useRef<Set<string>>(new Set());\n\n const maxDecals = isMobile\n ? DECAL_CONSTANTS.MAX_DECALS_MOBILE\n : DECAL_CONSTANTS.MAX_DECALS;\n\n const bloodTexture = useMemo(() => createBloodTexture(), []);\n\n const activeDecals = useMemo(() => {\n const sorted = [...decals].sort((a, b) => b.timestamp - a.timestamp);\n return sorted.slice(0, maxDecals);\n }, [decals, maxDecals]);\n\n const decalAgesRef = useRef<Map<string, number>>(new Map());\n\n useEffect(() => {\n activeDecals.forEach((decal) => {\n if (!decalAgesRef.current.has(decal.id)) {\n decalAgesRef.current.set(decal.id, 0);\n }\n });\n }, [activeDecals]);\n\n useFrame((_, delta) => {\n activeDecals.forEach((decal) => {\n const currentAge = (decalAgesRef.current.get(decal.id) ?? 0) + delta;\n decalAgesRef.current.set(decal.id, currentAge);\n \n const isExpired = currentAge >= fadeDuration;\n\n if (\n isExpired &&\n onDecalComplete &&\n !completedDecalsRef.current.has(decal.id)\n ) {\n completedDecalsRef.current.add(decal.id);\n onDecalComplete(decal.id);\n }\n });\n });\n\n useEffect(() => {\n return () => {\n bloodTexture.dispose();\n };\n }, [bloodTexture]);\n\n const [targetMesh, setTargetMesh] = React.useState<THREE.Mesh | undefined>();\n\n React.useLayoutEffect(() => {\n setTargetMesh(targetMeshRef?.current ?? undefined);\n }, [targetMeshRef]);\n\n if (!enabled || activeDecals.length === 0) {\n return null;\n }\n\n return (\n <group data-testid=\"blood-decals-3d\">\n {activeDecals.map((decal) => (\n <DecalMesh\n key={decal.id}\n decal={decal}\n texture={bloodTexture}\n targetMesh={targetMesh}\n fadeDuration={fadeDuration}\n />\n ))}\n </group>\n );\n};\n\nexport default BloodDecals3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BloodParticles3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodParticles3D.tsx"],"sourcesContent":["/**\n * BloodParticles3D - Realistic blood splatter particle system\n *\n * Creates physics-based blood particles that spray from impact points with gravity,\n * creating pools on the arena floor. Optimized for 60fps with instanced rendering.\n *\n * Features:\n * - Gravity-based particle physics\n * - Blood pool accumulation on floor\n * - 10-second fade-out for pools\n * - Instanced rendering for performance\n * - Mobile-optimized particle counts\n *\n * @module components/combat/BloodParticles3D\n * @category Combat Effects\n * @korean 피입자3D\n */\n\nimport { Points, PointMaterial } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../../utils/threeObjectPool\";\n\n/**\n * Blood particle data structure for efficient simulation\n * \n * PERFORMANCE: position and velocity now use pooled Vector3 objects\n * that are acquired on particle creation and released on expiration\n */\ninterface BloodParticle {\n /** Current position [x, y, z] - POOLED Vector3 */\n position: THREE.Vector3;\n /** Current velocity [x, y, z] - POOLED Vector3 */\n velocity: THREE.Vector3;\n /** Particle lifetime in seconds */\n lifetime: number;\n /** Time elapsed since creation */\n age: number;\n /** Whether particle has settled on floor */\n settled: boolean;\n /** Flag to track if vectors are pooled and need release */\n isPooled: boolean;\n}\n\n/**\n * Blood splatter effect configuration\n */\nexport interface BloodSplatterEffect {\n /** Unique identifier */\n readonly id: string;\n /** Origin position in 3D world space */\n readonly position: [number, number, number];\n /** Impact direction for splatter */\n readonly direction: [number, number, number];\n /** Intensity of effect (0.0 to 1.0) */\n readonly intensity: number;\n /** Timestamp when effect was created */\n readonly startTime: number;\n}\n\n/**\n * Props for BloodParticles3D component\n */\nexport interface BloodParticles3DProps {\n /** Active blood splatter effects to render */\n readonly effects: readonly BloodSplatterEffect[];\n /** Whether to enable blood effects (violence settings) */\n readonly enabled?: boolean;\n /** Mobile device mode (reduced particle count) */\n readonly isMobile?: boolean;\n /** Callback when effect completes */\n readonly onEffectComplete?: (effectId: string) => void;\n}\n\n/**\n * Performance and physics constants\n */\nconst BLOOD_CONSTANTS = {\n /** Gravity acceleration (m/s²) */\n GRAVITY: -9.8,\n /** Maximum particles per splatter effect */\n MAX_PARTICLES_DESKTOP: 300,\n MAX_PARTICLES_MOBILE: 100,\n /** Particle lifetime in seconds */\n PARTICLE_LIFETIME: 2.0,\n /** Blood pool fade duration in seconds */\n POOL_FADE_DURATION: 10.0,\n /** Initial velocity range */\n VELOCITY_MIN: 2.0,\n VELOCITY_MAX: 5.0,\n /** Spread angle in radians */\n SPREAD_ANGLE: Math.PI / 3,\n /** Floor Y position */\n FLOOR_Y: 0.0,\n /** Particle size */\n PARTICLE_SIZE: 0.05,\n /**\n * Maximum per-frame delta time (seconds) used to clamp the blood physics update.\n * \n * We cap the simulation step at ~33ms (1/30) to avoid the classic\n * \"spiral of death\" when the frame rate drops: very large timesteps can\n * cause unstable motion, particles tunneling through the floor, or\n * visually exaggerated splatter. Treating anything below 30fps as\n * \"slow motion\" for this effect keeps the blood behavior stable even\n * on slower devices. If the engine's minimum target frame rate changes,\n * adjust this threshold accordingly.\n */\n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * Generate initial particles for a blood splatter effect\n * \n * PERFORMANCE OPTIMIZATION: Uses ThreeObjectPools for ALL Vector3 allocations\n * \n * Pooling Strategy (UPDATED):\n * - Acquire temp vectors for calculations (baseDir, origin, axis vectors) - RELEASED\n * - Acquire position/velocity from pool for particle ownership - MUST BE RELEASED on expiration\n * \n * Memory Impact:\n * - Before: 600 Vector3 allocations per splatter (2 per particle × 300)\n * - After: 5 temp vectors (reused) + particles use pooled vectors (released on expiration)\n * - Reduction: ~99.2% fewer allocations (600 → 5 temp + pooled reuse)\n * \n * @param effect - Blood splatter effect configuration\n * @param maxParticles - Maximum number of particles to generate\n * @returns Array of blood particles with ALL vectors from pool (must be released later)\n */\nconst generateBloodParticles = (\n effect: BloodSplatterEffect,\n maxParticles: number\n): BloodParticle[] => {\n const particles: BloodParticle[] = [];\n const particleCount = Math.floor(maxParticles * effect.intensity);\n\n const baseDir = ThreeObjectPools.vector3.acquire();\n const origin = ThreeObjectPools.vector3.acquire();\n const direction = ThreeObjectPools.vector3.acquire();\n const yAxis = ThreeObjectPools.vector3.acquire();\n const zAxis = ThreeObjectPools.vector3.acquire();\n\n baseDir.set(...effect.direction).normalize();\n origin.set(...effect.position);\n yAxis.set(0, 1, 0);\n zAxis.set(0, 0, 1);\n\n for (let i = 0; i < particleCount; i++) {\n const spreadAngle = BLOOD_CONSTANTS.SPREAD_ANGLE;\n const phi = (Math.random() - 0.5) * spreadAngle;\n const theta = Math.random() * Math.PI * 2;\n\n direction.copy(baseDir);\n direction.applyAxisAngle(yAxis, phi);\n direction.applyAxisAngle(zAxis, theta);\n\n const speed =\n BLOOD_CONSTANTS.VELOCITY_MIN +\n Math.random() * (BLOOD_CONSTANTS.VELOCITY_MAX - BLOOD_CONSTANTS.VELOCITY_MIN);\n\n const particlePosition = ThreeObjectPools.vector3.acquire();\n const particleVelocity = ThreeObjectPools.vector3.acquire();\n \n particlePosition.copy(origin);\n particleVelocity.copy(direction).multiplyScalar(speed);\n\n particles.push({\n position: particlePosition,\n velocity: particleVelocity,\n lifetime: BLOOD_CONSTANTS.PARTICLE_LIFETIME,\n age: 0,\n settled: false,\n isPooled: true, // Mark as pooled for cleanup\n });\n }\n\n ThreeObjectPools.vector3.release(baseDir);\n ThreeObjectPools.vector3.release(origin);\n ThreeObjectPools.vector3.release(direction);\n ThreeObjectPools.vector3.release(yAxis);\n ThreeObjectPools.vector3.release(zAxis);\n\n return particles;\n};\n\n/**\n * BloodParticles3D Component\n *\n * Renders realistic blood splatter effects using physics-based particle simulation.\n * Optimized for performance with instanced rendering and efficient physics updates.\n *\n * @example\n * ```tsx\n * const [bloodEffects, setBloodEffects] = useState<BloodSplatterEffect[]>([]);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], direction: [number, number, number]) => {\n * setBloodEffects([...bloodEffects, {\n * id: generateId(),\n * position,\n * direction,\n * intensity: 0.8,\n * startTime: Date.now(),\n * }]);\n * };\n *\n * <BloodParticles3D\n * effects={bloodEffects}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onEffectComplete={(id) => {\n * setBloodEffects(prev => prev.filter(e => e.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodParticles3D: React.FC<BloodParticles3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const particlesRef = useRef<Map<string, BloodParticle[]>>(new Map());\n const poolParticlesRef = useRef<BloodParticle[]>([]);\n const pointsRef = useRef<THREE.Points>(null);\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const maxParticles = isMobile\n ? BLOOD_CONSTANTS.MAX_PARTICLES_MOBILE\n : BLOOD_CONSTANTS.MAX_PARTICLES_DESKTOP;\n\n const bloodColor = useMemo(() => KOREAN_COLORS.BLOODLOSS_INDICATOR, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n effects.forEach((effect) => {\n if (!particlesRef.current.has(effect.id)) {\n const particles = generateBloodParticles(effect, maxParticles);\n particlesRef.current.set(effect.id, particles);\n }\n });\n\n const effectIds = new Set(effects.map((e) => e.id));\n particlesRef.current.forEach((particles, id) => {\n if (!effectIds.has(id)) {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n particlesRef.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n }, [effects, enabled, maxParticles]);\n\n React.useEffect(() => {\n const currentParticles = particlesRef.current;\n const currentPoolParticles = poolParticlesRef.current;\n const currentCompletedEffects = completedEffectsRef.current;\n \n return () => {\n currentParticles.forEach((particles) => {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n });\n \n currentPoolParticles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n \n poolParticlesRef.current = [];\n \n currentParticles.clear();\n currentCompletedEffects.clear();\n };\n }, []);\n\n const initialPositions = useMemo(() => {\n const positions = new Float32Array(maxParticles * 3);\n return positions;\n }, [maxParticles]);\n\n useFrame((_, delta) => {\n if (!enabled || !pointsRef.current) return;\n\n const safeDelta = Math.min(delta, BLOOD_CONSTANTS.MAX_DELTA);\n\n let totalParticleIndex = 0;\n const attr = pointsRef.current.geometry.attributes.position;\n const posArray = attr.array as Float32Array;\n\n particlesRef.current.forEach((particles, effectId) => {\n let hasActiveParticles = false;\n\n for (let i = 0; i < particles.length; i++) {\n const p = particles[i];\n p.age += safeDelta;\n\n if (!p.settled) {\n p.velocity.y += BLOOD_CONSTANTS.GRAVITY * safeDelta;\n\n p.position.addScaledVector(p.velocity, safeDelta);\n\n if (p.position.y <= BLOOD_CONSTANTS.FLOOR_Y) {\n p.position.y = BLOOD_CONSTANTS.FLOOR_Y;\n p.velocity.set(0, 0, 0);\n p.settled = true;\n p.lifetime = BLOOD_CONSTANTS.POOL_FADE_DURATION;\n p.age = 0;\n\n poolParticlesRef.current.push(p);\n } else {\n hasActiveParticles = true;\n }\n }\n\n if (totalParticleIndex < posArray.length / 3) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n } else if (process.env.NODE_ENV === \"development\") {\n console.warn(\n `BloodParticles3D: Particle buffer full (${posArray.length / 3} particles). ` +\n `Additional particles are not rendered. Consider reducing particle counts or ` +\n `increasing maxParticles if this happens frequently.`\n );\n }\n }\n\n if (!hasActiveParticles && !completedEffectsRef.current.has(effectId)) {\n completedEffectsRef.current.add(effectId);\n onEffectComplete?.(effectId);\n }\n });\n\n const maxVisibleParticles = Math.min(maxParticles, posArray.length / 3);\n\n if (totalParticleIndex >= maxVisibleParticles) {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n \n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n \n return isAlive;\n });\n } else {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n\n if (isAlive && totalParticleIndex < maxVisibleParticles) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n }\n\n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n\n return isAlive;\n });\n }\n\n attr.needsUpdate = true;\n });\n\n if (!enabled || effects.length === 0) {\n return null;\n }\n\n return (\n <Points\n ref={pointsRef}\n positions={initialPositions}\n data-testid=\"blood-particles-3d\"\n >\n <PointMaterial\n color={bloodColor}\n size={BLOOD_CONSTANTS.PARTICLE_SIZE}\n sizeAttenuation\n transparent\n opacity={0.9}\n depthWrite={false}\n blending={THREE.NormalBlending}\n />\n </Points>\n );\n};\n\nexport default BloodParticles3D;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BloodParticles3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodParticles3D.tsx"],"sourcesContent":["/**\n * BloodParticles3D - Realistic blood splatter particle system\n *\n * Creates physics-based blood particles that spray from impact points with gravity,\n * creating pools on the arena floor. Optimized for 60fps with instanced rendering.\n *\n * Features:\n * - Gravity-based particle physics\n * - Blood pool accumulation on floor\n * - 10-second fade-out for pools\n * - Instanced rendering for performance\n * - Mobile-optimized particle counts\n *\n * @module components/combat/BloodParticles3D\n * @category Combat Effects\n * @korean 피입자3D\n */\n\nimport { Points, PointMaterial } from \"@react-three/drei\";\nimport { useFrame } from \"@react-three/fiber\";\nimport React, { useRef, useMemo } from \"react\";\nimport * as THREE from \"three\";\nimport { KOREAN_COLORS } from \"../../../../../types/constants\";\nimport { ThreeObjectPools } from \"../../../../../utils/threeObjectPool\";\n\n/**\n * Blood particle data structure for efficient simulation\n * \n * PERFORMANCE: position and velocity now use pooled Vector3 objects\n * that are acquired on particle creation and released on expiration\n */\ninterface BloodParticle {\n /** Current position [x, y, z] - POOLED Vector3 */\n position: THREE.Vector3;\n /** Current velocity [x, y, z] - POOLED Vector3 */\n velocity: THREE.Vector3;\n /** Particle lifetime in seconds */\n lifetime: number;\n /** Time elapsed since creation */\n age: number;\n /** Whether particle has settled on floor */\n settled: boolean;\n /** Flag to track if vectors are pooled and need release */\n isPooled: boolean;\n}\n\n/**\n * Blood splatter effect configuration\n */\nexport interface BloodSplatterEffect {\n /** Unique identifier */\n readonly id: string;\n /** Origin position in 3D world space */\n readonly position: [number, number, number];\n /** Impact direction for splatter */\n readonly direction: [number, number, number];\n /** Intensity of effect (0.0 to 1.0) */\n readonly intensity: number;\n /** Timestamp when effect was created */\n readonly startTime: number;\n}\n\n/**\n * Props for BloodParticles3D component\n */\nexport interface BloodParticles3DProps {\n /** Active blood splatter effects to render */\n readonly effects: readonly BloodSplatterEffect[];\n /** Whether to enable blood effects (violence settings) */\n readonly enabled?: boolean;\n /** Mobile device mode (reduced particle count) */\n readonly isMobile?: boolean;\n /** Callback when effect completes */\n readonly onEffectComplete?: (effectId: string) => void;\n}\n\n/**\n * Performance and physics constants\n */\nconst BLOOD_CONSTANTS = {\n /** Gravity acceleration (m/s²) */\n GRAVITY: -9.8,\n /** Maximum particles per splatter effect */\n MAX_PARTICLES_DESKTOP: 300,\n MAX_PARTICLES_MOBILE: 100,\n /** Particle lifetime in seconds */\n PARTICLE_LIFETIME: 2.0,\n /** Blood pool fade duration in seconds */\n POOL_FADE_DURATION: 10.0,\n /** Initial velocity range */\n VELOCITY_MIN: 2.0,\n VELOCITY_MAX: 5.0,\n /** Spread angle in radians */\n SPREAD_ANGLE: Math.PI / 3,\n /** Floor Y position */\n FLOOR_Y: 0.0,\n /** Particle size */\n PARTICLE_SIZE: 0.05,\n /**\n * Maximum per-frame delta time (seconds) used to clamp the blood physics update.\n * \n * We cap the simulation step at ~33ms (1/30) to avoid the classic\n * \"spiral of death\" when the frame rate drops: very large timesteps can\n * cause unstable motion, particles tunneling through the floor, or\n * visually exaggerated splatter. Treating anything below 30fps as\n * \"slow motion\" for this effect keeps the blood behavior stable even\n * on slower devices. If the engine's minimum target frame rate changes,\n * adjust this threshold accordingly.\n */\n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * Generate initial particles for a blood splatter effect\n * \n * PERFORMANCE OPTIMIZATION: Uses ThreeObjectPools for ALL Vector3 allocations\n * \n * Pooling Strategy (UPDATED):\n * - Acquire temp vectors for calculations (baseDir, origin, axis vectors) - RELEASED\n * - Acquire position/velocity from pool for particle ownership - MUST BE RELEASED on expiration\n * \n * Memory Impact:\n * - Before: 600 Vector3 allocations per splatter (2 per particle × 300)\n * - After: 5 temp vectors (reused) + particles use pooled vectors (released on expiration)\n * - Reduction: ~99.2% fewer allocations (600 → 5 temp + pooled reuse)\n * \n * @param effect - Blood splatter effect configuration\n * @param maxParticles - Maximum number of particles to generate\n * @returns Array of blood particles with ALL vectors from pool (must be released later)\n */\nconst generateBloodParticles = (\n effect: BloodSplatterEffect,\n maxParticles: number\n): BloodParticle[] => {\n const particles: BloodParticle[] = [];\n const particleCount = Math.floor(maxParticles * effect.intensity);\n\n const baseDir = ThreeObjectPools.vector3.acquire();\n const origin = ThreeObjectPools.vector3.acquire();\n const direction = ThreeObjectPools.vector3.acquire();\n const yAxis = ThreeObjectPools.vector3.acquire();\n const zAxis = ThreeObjectPools.vector3.acquire();\n\n baseDir.set(...effect.direction).normalize();\n origin.set(...effect.position);\n yAxis.set(0, 1, 0);\n zAxis.set(0, 0, 1);\n\n for (let i = 0; i < particleCount; i++) {\n const spreadAngle = BLOOD_CONSTANTS.SPREAD_ANGLE;\n const phi = (Math.random() - 0.5) * spreadAngle;\n const theta = Math.random() * Math.PI * 2;\n\n direction.copy(baseDir);\n direction.applyAxisAngle(yAxis, phi);\n direction.applyAxisAngle(zAxis, theta);\n\n const speed =\n BLOOD_CONSTANTS.VELOCITY_MIN +\n Math.random() * (BLOOD_CONSTANTS.VELOCITY_MAX - BLOOD_CONSTANTS.VELOCITY_MIN);\n\n const particlePosition = ThreeObjectPools.vector3.acquire();\n const particleVelocity = ThreeObjectPools.vector3.acquire();\n \n particlePosition.copy(origin);\n particleVelocity.copy(direction).multiplyScalar(speed);\n\n particles.push({\n position: particlePosition,\n velocity: particleVelocity,\n lifetime: BLOOD_CONSTANTS.PARTICLE_LIFETIME,\n age: 0,\n settled: false,\n isPooled: true, // Mark as pooled for cleanup\n });\n }\n\n ThreeObjectPools.vector3.release(baseDir);\n ThreeObjectPools.vector3.release(origin);\n ThreeObjectPools.vector3.release(direction);\n ThreeObjectPools.vector3.release(yAxis);\n ThreeObjectPools.vector3.release(zAxis);\n\n return particles;\n};\n\n/**\n * BloodParticles3D Component\n *\n * Renders realistic blood splatter effects using physics-based particle simulation.\n * Optimized for performance with instanced rendering and efficient physics updates.\n *\n * @example\n * ```tsx\n * const [bloodEffects, setBloodEffects] = useState<BloodSplatterEffect[]>([]);\n *\n * // On hit event\n * const handleHit = (position: [number, number, number], direction: [number, number, number]) => {\n * setBloodEffects([...bloodEffects, {\n * id: generateId(),\n * position,\n * direction,\n * intensity: 0.8,\n * startTime: Date.now(),\n * }]);\n * };\n *\n * <BloodParticles3D\n * effects={bloodEffects}\n * enabled={violenceSettings.blood}\n * isMobile={isMobile}\n * onEffectComplete={(id) => {\n * setBloodEffects(prev => prev.filter(e => e.id !== id));\n * }}\n * />\n * ```\n */\nexport const BloodParticles3D: React.FC<BloodParticles3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const particlesRef = useRef<Map<string, BloodParticle[]>>(new Map());\n const poolParticlesRef = useRef<BloodParticle[]>([]);\n const pointsRef = useRef<THREE.Points>(null);\n const completedEffectsRef = useRef<Set<string>>(new Set());\n\n const maxParticles = isMobile\n ? BLOOD_CONSTANTS.MAX_PARTICLES_MOBILE\n : BLOOD_CONSTANTS.MAX_PARTICLES_DESKTOP;\n\n const bloodColor = useMemo(() => KOREAN_COLORS.BLOODLOSS_INDICATOR, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n effects.forEach((effect) => {\n if (!particlesRef.current.has(effect.id)) {\n const particles = generateBloodParticles(effect, maxParticles);\n particlesRef.current.set(effect.id, particles);\n }\n });\n\n const effectIds = new Set(effects.map((e) => e.id));\n particlesRef.current.forEach((particles, id) => {\n if (!effectIds.has(id)) {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n particlesRef.current.delete(id);\n completedEffectsRef.current.delete(id);\n }\n });\n }, [effects, enabled, maxParticles]);\n\n React.useEffect(() => {\n const currentParticles = particlesRef.current;\n const currentPoolParticles = poolParticlesRef.current;\n const currentCompletedEffects = completedEffectsRef.current;\n \n return () => {\n currentParticles.forEach((particles) => {\n particles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n });\n \n currentPoolParticles.forEach((p) => {\n if (p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false;\n }\n });\n \n poolParticlesRef.current = [];\n \n currentParticles.clear();\n currentCompletedEffects.clear();\n };\n }, []);\n\n const initialPositions = useMemo(() => {\n const positions = new Float32Array(maxParticles * 3);\n return positions;\n }, [maxParticles]);\n\n useFrame((_, delta) => {\n if (!enabled || !pointsRef.current) return;\n\n const safeDelta = Math.min(delta, BLOOD_CONSTANTS.MAX_DELTA);\n\n let totalParticleIndex = 0;\n const attr = pointsRef.current.geometry.attributes.position;\n const posArray = attr.array as Float32Array;\n\n particlesRef.current.forEach((particles, effectId) => {\n let hasActiveParticles = false;\n\n for (let i = 0; i < particles.length; i++) {\n const p = particles[i];\n p.age += safeDelta;\n\n if (!p.settled) {\n p.velocity.y += BLOOD_CONSTANTS.GRAVITY * safeDelta;\n\n p.position.addScaledVector(p.velocity, safeDelta);\n\n if (p.position.y <= BLOOD_CONSTANTS.FLOOR_Y) {\n p.position.y = BLOOD_CONSTANTS.FLOOR_Y;\n p.velocity.set(0, 0, 0);\n p.settled = true;\n p.lifetime = BLOOD_CONSTANTS.POOL_FADE_DURATION;\n p.age = 0;\n\n poolParticlesRef.current.push(p);\n } else {\n hasActiveParticles = true;\n }\n }\n\n if (totalParticleIndex < posArray.length / 3) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n } else if (process.env.NODE_ENV === \"development\") {\n console.warn(\n `BloodParticles3D: Particle buffer full (${posArray.length / 3} particles). ` +\n `Additional particles are not rendered. Consider reducing particle counts or ` +\n `increasing maxParticles if this happens frequently.`\n );\n }\n }\n\n if (!hasActiveParticles && !completedEffectsRef.current.has(effectId)) {\n completedEffectsRef.current.add(effectId);\n onEffectComplete?.(effectId);\n }\n });\n\n const maxVisibleParticles = Math.min(maxParticles, posArray.length / 3);\n\n if (totalParticleIndex >= maxVisibleParticles) {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n \n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n \n return isAlive;\n });\n } else {\n poolParticlesRef.current = poolParticlesRef.current.filter((p) => {\n p.age += safeDelta;\n const isAlive = p.age < p.lifetime;\n\n if (isAlive && totalParticleIndex < maxVisibleParticles) {\n posArray[totalParticleIndex * 3] = p.position.x;\n posArray[totalParticleIndex * 3 + 1] = p.position.y;\n posArray[totalParticleIndex * 3 + 2] = p.position.z;\n totalParticleIndex++;\n }\n\n if (!isAlive && p.isPooled) {\n ThreeObjectPools.vector3.release(p.position);\n ThreeObjectPools.vector3.release(p.velocity);\n p.isPooled = false; // Mark as released\n }\n\n return isAlive;\n });\n }\n\n attr.needsUpdate = true;\n });\n\n if (!enabled || effects.length === 0) {\n return null;\n }\n\n return (\n <Points\n ref={pointsRef}\n positions={initialPositions}\n data-testid=\"blood-particles-3d\"\n >\n <PointMaterial\n color={bloodColor}\n size={BLOOD_CONSTANTS.PARTICLE_SIZE}\n sizeAttenuation\n transparent\n opacity={0.9}\n depthWrite={false}\n blending={THREE.NormalBlending}\n />\n </Points>\n );\n};\n\nexport default BloodParticles3D;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BloodViscosity3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodViscosity3D.tsx"],"sourcesContent":["/**\n * BloodViscosity3D - Enhanced blood droplets with thicker physics for brutal combat realism\n *\n * Priority #5: Enhanced Blood Viscosity\n * - Thicker droplets with slower fall rate\n * - Variable splatter sizes (gouts vs mist)\n * - Cling/drip physics (stick to surfaces)\n * - Enhanced viscosity simulation\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~85+ per effect to ~4 pooled objects\n * - Pooling strategy:\n * 1. Temporary calculation objects (direction, color, velocity) use pool\n * 2. Owned objects (particle velocities) are cloned from pooled objects\n * 3. All pooled objects released after particle creation\n * - Estimated reduction: \n * - thin: 50 Color + 50 Vector3 = 100 allocations → 4 pooled objects\n * - medium: 80 Color + 80 Vector3 = 160 allocations → 4 pooled objects\n * - thick: 120 Color + 120 Vector3 = 240 allocations → 4 pooled objects\n * - gout: 200 Color + 200 Vector3 = 400 allocations → 4 pooled objects\n *\n * Korean martial arts context:\n * - 절단격 (Cutting strikes) - Thin blood mist\n * - 타격 (Impact strikes) - Medium blood splatter\n * - 관통격 (Penetrating strikes) - Thick blood droplets\n * - 깊은 상처 (Deep wounds) - Large blood gouts\n */\n\nimport React, { useEffect, useMemo } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Viscosity types for blood splatter\n */\nexport type ViscosityType = 'thin' | 'medium' | 'thick' | 'gout';\n\n/**\n * Individual blood viscosity effect\n */\nexport interface BloodViscosityEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly direction: [number, number, number];\n readonly viscosityType: ViscosityType;\n readonly intensity: number; // 0.0-1.0\n readonly startTime: number;\n}\n\n/**\n * Props for BloodViscosity3D component\n */\nexport interface BloodViscosity3DProps {\n readonly effects: readonly BloodViscosityEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst BLOOD_VISCOSITY_CONSTANTS = {\n GRAVITY: -9.8, // m/s²\n AIR_RESISTANCE: 0.94, // Thicker than arterial (0.97) or bone (0.96)\n \n PARTICLE_COUNT: {\n thin: 50, // Mist\n medium: 80, // Normal splatter\n thick: 120, // Heavy droplets\n gout: 200, // Deep wound large gouts\n },\n \n VELOCITY: {\n thin: { min: 2.0, max: 4.0 },\n medium: { min: 1.5, max: 3.0 },\n thick: { min: 1.0, max: 2.5 },\n gout: { min: 0.5, max: 2.0 },\n },\n \n SIZE: {\n thin: { min: 0.02, max: 0.04 },\n medium: { min: 0.04, max: 0.08 },\n thick: { min: 0.06, max: 0.12 },\n gout: { min: 0.10, max: 0.20 },\n },\n \n SPREAD_ANGLE: {\n thin: Math.PI / 3, // 60° wide spray\n medium: Math.PI / 4, // 45° normal\n thick: Math.PI / 6, // 30° narrow\n gout: Math.PI / 8, // 22.5° very narrow\n },\n \n ACTIVE_LIFETIME: 2.5, // Active falling time\n CLING_LIFETIME: 3.0, // Time stuck to ground\n TOTAL_LIFETIME: 5.5, // Total before cleanup\n \n GROUND_LEVEL: 0.1, // y-position for ground contact\n CLING_DAMPING: 0.3, // Velocity reduction on contact\n \n BLOOD_COLOR: 0x8b0000, // Dark red\n \n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * BloodViscosity3D - Enhanced blood droplets with realistic thicker physics\n *\n * Features:\n * - Variable viscosity types (thin mist to thick gouts)\n * - Cling/drip physics when hitting ground\n * - Slower fall rates than arterial spray\n * - Larger droplet sizes\n * - Mobile optimization (50% particles)\n */\nexport const BloodViscosity3D: React.FC<BloodViscosity3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n particles: THREE.Points;\n velocities: THREE.Vector3[];\n startTime: number;\n effect: BloodViscosityEffect;\n clinging: boolean[];\n }\n >\n >(new Map());\n\n const getParticleCount = useMemo(\n () => (viscosityType: ViscosityType) => {\n const baseCount = BLOOD_VISCOSITY_CONSTANTS.PARTICLE_COUNT[viscosityType];\n return isMobile ? Math.floor(baseCount * 0.5) : baseCount;\n },\n [isMobile]\n );\n\n const createBloodParticles = useMemo(\n () => (effect: BloodViscosityEffect) => {\n const count = getParticleCount(effect.viscosityType);\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(count * 3);\n const colors = new Float32Array(count * 3);\n const sizes = new Float32Array(count);\n const velocities: THREE.Vector3[] = [];\n const clinging: boolean[] = [];\n\n const tempDirection = ThreeObjectPools.vector3.acquire();\n const tempColor = ThreeObjectPools.color.acquire();\n const tempVelocity = ThreeObjectPools.vector3.acquire();\n const tempDeviation = ThreeObjectPools.vector3.acquire();\n\n try {\n tempDirection.set(...effect.direction).normalize();\n const spreadAngle = BLOOD_VISCOSITY_CONSTANTS.SPREAD_ANGLE[effect.viscosityType];\n const velocityRange = BLOOD_VISCOSITY_CONSTANTS.VELOCITY[effect.viscosityType];\n const sizeRange = BLOOD_VISCOSITY_CONSTANTS.SIZE[effect.viscosityType];\n\n tempColor.set(BLOOD_VISCOSITY_CONSTANTS.BLOOD_COLOR);\n\n for (let i = 0; i < count; i++) {\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const size =\n sizeRange.min +\n Math.random() * (sizeRange.max - sizeRange.min) *\n effect.intensity;\n sizes[i] = size;\n\n const speed =\n velocityRange.min +\n Math.random() * (velocityRange.max - velocityRange.min);\n\n const theta = (Math.random() - 0.5) * spreadAngle;\n const phi = Math.random() * Math.PI * 2;\n\n tempDeviation.set(\n Math.sin(theta) * Math.cos(phi),\n Math.sin(theta) * Math.sin(phi),\n Math.cos(theta)\n );\n\n tempVelocity.copy(tempDirection)\n .add(tempDeviation)\n .normalize()\n .multiplyScalar(speed);\n\n velocities.push(tempVelocity.clone());\n clinging.push(false);\n }\n } finally {\n ThreeObjectPools.vector3.release(tempDirection);\n ThreeObjectPools.color.release(tempColor);\n ThreeObjectPools.vector3.release(tempVelocity);\n ThreeObjectPools.vector3.release(tempDeviation);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.08,\n vertexColors: true,\n transparent: true,\n opacity: 0.9,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return { points, velocities, clinging };\n },\n [getParticleCount]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const { points, velocities, clinging } = createBloodParticles(effect);\n updated.set(effect.id, {\n particles: points,\n velocities,\n startTime: effect.startTime,\n effect,\n clinging,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.particles.geometry.dispose();\n (instance.particles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createBloodParticles]);\n\n useFrame((_state, delta) => {\n if (!enabled || effectInstances.size === 0) return;\n\n const clampedDelta = Math.min(delta, BLOOD_VISCOSITY_CONSTANTS.MAX_DELTA);\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.TOTAL_LIFETIME) {\n completedIds.push(id);\n return;\n }\n\n const geometry = instance.particles.geometry;\n const positions = geometry.attributes.position.array as Float32Array;\n const count = positions.length / 3;\n\n for (let i = 0; i < count; i++) {\n const idx = i * 3;\n\n if (!instance.clinging[i]) {\n instance.velocities[i].y +=\n BLOOD_VISCOSITY_CONSTANTS.GRAVITY * clampedDelta;\n\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.AIR_RESISTANCE\n );\n\n positions[idx] += instance.velocities[i].x * clampedDelta;\n positions[idx + 1] += instance.velocities[i].y * clampedDelta;\n positions[idx + 2] += instance.velocities[i].z * clampedDelta;\n\n if (positions[idx + 1] <= BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL) {\n positions[idx + 1] = BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL;\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.CLING_DAMPING\n );\n instance.clinging[i] = true;\n }\n }\n }\n\n geometry.attributes.position.needsUpdate = true;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) {\n const fadeProgress =\n (elapsed - BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) /\n BLOOD_VISCOSITY_CONSTANTS.CLING_LIFETIME;\n const material = instance.particles.material as THREE.PointsMaterial;\n material.opacity = 0.9 * (1.0 - fadeProgress);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <primitive key={instance.effect.id} object={instance.particles} />\n ))}\n </>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BloodViscosity3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/BloodViscosity3D.tsx"],"sourcesContent":["/**\n * BloodViscosity3D - Enhanced blood droplets with thicker physics for brutal combat realism\n *\n * Priority #5: Enhanced Blood Viscosity\n * - Thicker droplets with slower fall rate\n * - Variable splatter sizes (gouts vs mist)\n * - Cling/drip physics (stick to surfaces)\n * - Enhanced viscosity simulation\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~85+ per effect to ~4 pooled objects\n * - Pooling strategy:\n * 1. Temporary calculation objects (direction, color, velocity) use pool\n * 2. Owned objects (particle velocities) are cloned from pooled objects\n * 3. All pooled objects released after particle creation\n * - Estimated reduction: \n * - thin: 50 Color + 50 Vector3 = 100 allocations → 4 pooled objects\n * - medium: 80 Color + 80 Vector3 = 160 allocations → 4 pooled objects\n * - thick: 120 Color + 120 Vector3 = 240 allocations → 4 pooled objects\n * - gout: 200 Color + 200 Vector3 = 400 allocations → 4 pooled objects\n *\n * Korean martial arts context:\n * - 절단격 (Cutting strikes) - Thin blood mist\n * - 타격 (Impact strikes) - Medium blood splatter\n * - 관통격 (Penetrating strikes) - Thick blood droplets\n * - 깊은 상처 (Deep wounds) - Large blood gouts\n */\n\nimport React, { useEffect, useMemo } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Viscosity types for blood splatter\n */\nexport type ViscosityType = 'thin' | 'medium' | 'thick' | 'gout';\n\n/**\n * Individual blood viscosity effect\n */\nexport interface BloodViscosityEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly direction: [number, number, number];\n readonly viscosityType: ViscosityType;\n readonly intensity: number; // 0.0-1.0\n readonly startTime: number;\n}\n\n/**\n * Props for BloodViscosity3D component\n */\nexport interface BloodViscosity3DProps {\n readonly effects: readonly BloodViscosityEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst BLOOD_VISCOSITY_CONSTANTS = {\n GRAVITY: -9.8, // m/s²\n AIR_RESISTANCE: 0.94, // Thicker than arterial (0.97) or bone (0.96)\n \n PARTICLE_COUNT: {\n thin: 50, // Mist\n medium: 80, // Normal splatter\n thick: 120, // Heavy droplets\n gout: 200, // Deep wound large gouts\n },\n \n VELOCITY: {\n thin: { min: 2.0, max: 4.0 },\n medium: { min: 1.5, max: 3.0 },\n thick: { min: 1.0, max: 2.5 },\n gout: { min: 0.5, max: 2.0 },\n },\n \n SIZE: {\n thin: { min: 0.02, max: 0.04 },\n medium: { min: 0.04, max: 0.08 },\n thick: { min: 0.06, max: 0.12 },\n gout: { min: 0.10, max: 0.20 },\n },\n \n SPREAD_ANGLE: {\n thin: Math.PI / 3, // 60° wide spray\n medium: Math.PI / 4, // 45° normal\n thick: Math.PI / 6, // 30° narrow\n gout: Math.PI / 8, // 22.5° very narrow\n },\n \n ACTIVE_LIFETIME: 2.5, // Active falling time\n CLING_LIFETIME: 3.0, // Time stuck to ground\n TOTAL_LIFETIME: 5.5, // Total before cleanup\n \n GROUND_LEVEL: 0.1, // y-position for ground contact\n CLING_DAMPING: 0.3, // Velocity reduction on contact\n \n BLOOD_COLOR: 0x8b0000, // Dark red\n \n MAX_DELTA: 1 / 30,\n} as const;\n\n/**\n * BloodViscosity3D - Enhanced blood droplets with realistic thicker physics\n *\n * Features:\n * - Variable viscosity types (thin mist to thick gouts)\n * - Cling/drip physics when hitting ground\n * - Slower fall rates than arterial spray\n * - Larger droplet sizes\n * - Mobile optimization (50% particles)\n */\nexport const BloodViscosity3D: React.FC<BloodViscosity3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n particles: THREE.Points;\n velocities: THREE.Vector3[];\n startTime: number;\n effect: BloodViscosityEffect;\n clinging: boolean[];\n }\n >\n >(new Map());\n\n const getParticleCount = useMemo(\n () => (viscosityType: ViscosityType) => {\n const baseCount = BLOOD_VISCOSITY_CONSTANTS.PARTICLE_COUNT[viscosityType];\n return isMobile ? Math.floor(baseCount * 0.5) : baseCount;\n },\n [isMobile]\n );\n\n const createBloodParticles = useMemo(\n () => (effect: BloodViscosityEffect) => {\n const count = getParticleCount(effect.viscosityType);\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(count * 3);\n const colors = new Float32Array(count * 3);\n const sizes = new Float32Array(count);\n const velocities: THREE.Vector3[] = [];\n const clinging: boolean[] = [];\n\n const tempDirection = ThreeObjectPools.vector3.acquire();\n const tempColor = ThreeObjectPools.color.acquire();\n const tempVelocity = ThreeObjectPools.vector3.acquire();\n const tempDeviation = ThreeObjectPools.vector3.acquire();\n\n try {\n tempDirection.set(...effect.direction).normalize();\n const spreadAngle = BLOOD_VISCOSITY_CONSTANTS.SPREAD_ANGLE[effect.viscosityType];\n const velocityRange = BLOOD_VISCOSITY_CONSTANTS.VELOCITY[effect.viscosityType];\n const sizeRange = BLOOD_VISCOSITY_CONSTANTS.SIZE[effect.viscosityType];\n\n tempColor.set(BLOOD_VISCOSITY_CONSTANTS.BLOOD_COLOR);\n\n for (let i = 0; i < count; i++) {\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const size =\n sizeRange.min +\n Math.random() * (sizeRange.max - sizeRange.min) *\n effect.intensity;\n sizes[i] = size;\n\n const speed =\n velocityRange.min +\n Math.random() * (velocityRange.max - velocityRange.min);\n\n const theta = (Math.random() - 0.5) * spreadAngle;\n const phi = Math.random() * Math.PI * 2;\n\n tempDeviation.set(\n Math.sin(theta) * Math.cos(phi),\n Math.sin(theta) * Math.sin(phi),\n Math.cos(theta)\n );\n\n tempVelocity.copy(tempDirection)\n .add(tempDeviation)\n .normalize()\n .multiplyScalar(speed);\n\n velocities.push(tempVelocity.clone());\n clinging.push(false);\n }\n } finally {\n ThreeObjectPools.vector3.release(tempDirection);\n ThreeObjectPools.color.release(tempColor);\n ThreeObjectPools.vector3.release(tempVelocity);\n ThreeObjectPools.vector3.release(tempDeviation);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.08,\n vertexColors: true,\n transparent: true,\n opacity: 0.9,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return { points, velocities, clinging };\n },\n [getParticleCount]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const { points, velocities, clinging } = createBloodParticles(effect);\n updated.set(effect.id, {\n particles: points,\n velocities,\n startTime: effect.startTime,\n effect,\n clinging,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.particles.geometry.dispose();\n (instance.particles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createBloodParticles]);\n\n useFrame((_state, delta) => {\n if (!enabled || effectInstances.size === 0) return;\n\n const clampedDelta = Math.min(delta, BLOOD_VISCOSITY_CONSTANTS.MAX_DELTA);\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.TOTAL_LIFETIME) {\n completedIds.push(id);\n return;\n }\n\n const geometry = instance.particles.geometry;\n const positions = geometry.attributes.position.array as Float32Array;\n const count = positions.length / 3;\n\n for (let i = 0; i < count; i++) {\n const idx = i * 3;\n\n if (!instance.clinging[i]) {\n instance.velocities[i].y +=\n BLOOD_VISCOSITY_CONSTANTS.GRAVITY * clampedDelta;\n\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.AIR_RESISTANCE\n );\n\n positions[idx] += instance.velocities[i].x * clampedDelta;\n positions[idx + 1] += instance.velocities[i].y * clampedDelta;\n positions[idx + 2] += instance.velocities[i].z * clampedDelta;\n\n if (positions[idx + 1] <= BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL) {\n positions[idx + 1] = BLOOD_VISCOSITY_CONSTANTS.GROUND_LEVEL;\n instance.velocities[i].multiplyScalar(\n BLOOD_VISCOSITY_CONSTANTS.CLING_DAMPING\n );\n instance.clinging[i] = true;\n }\n }\n }\n\n geometry.attributes.position.needsUpdate = true;\n\n if (elapsed >= BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) {\n const fadeProgress =\n (elapsed - BLOOD_VISCOSITY_CONSTANTS.ACTIVE_LIFETIME) /\n BLOOD_VISCOSITY_CONSTANTS.CLING_LIFETIME;\n const material = instance.particles.material as THREE.PointsMaterial;\n material.opacity = 0.9 * (1.0 - fadeProgress);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <primitive key={instance.effect.id} object={instance.particles} />\n ))}\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CombatParticleEffects3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/CombatParticleEffects3D.tsx"],"sourcesContent":["/**\n * CombatParticleEffects3D - Particle effects coordinator for combat\n * 전투 입자 효과 통합 관리\n *\n * Maps HitEffect events to advanced particle effects:\n * - BloodViscosity3D for blood physics on hits\n * - InternalDamage3D for organ damage visualization on vital point strikes\n * - ParticleAudio3D for synchronized combat audio\n *\n * @module components/effects\n * @category Combat Effects\n * @korean 전투입자효과\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HitEffect } from \"../../../../../systems\";\nimport { HitEffectType } from \"../../../../../systems/effects\";\nimport {\n BloodViscosity3D,\n type BloodViscosityEffect,\n type ViscosityType,\n} from \"./BloodViscosity3D\";\nimport {\n InternalDamage3D,\n type InternalDamageEffect,\n type OrganType,\n type PenetrationDepth,\n} from \"./InternalDamage3D\";\nimport {\n ParticleAudio3D,\n type ParticleAudioTrigger,\n type ParticleEffectType,\n} from \"./ParticleAudio3D\";\n\n/**\n * Props for CombatParticleEffects3D\n */\nexport interface CombatParticleEffects3DProps {\n /** Active hit effects from the combat state */\n readonly hitEffects: readonly HitEffect[];\n /** Whether effects are enabled */\n readonly enabled?: boolean;\n /** Mobile optimization flag */\n readonly isMobile?: boolean;\n}\n\n/**\n * Map HitEffectType → blood viscosity type\n * 타격 유형 → 혈액 점도 매핑\n */\nfunction getViscosityForHitType(type: HitEffectType): ViscosityType | null {\n switch (type) {\n case HitEffectType.HIT:\n return \"thin\";\n case HitEffectType.COUNTER:\n return \"medium\";\n case HitEffectType.CRITICAL_HIT:\n return \"thick\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"gout\";\n default:\n return null;\n }\n}\n\n/**\n * Map HitEffectType → organ type based on strike position\n * 급소 유형 → 장기 매핑 (타격 위치 기반)\n */\nfunction getOrganForPosition(\n position: { x: number; y: number } | undefined,\n): OrganType {\n if (!position) return \"stomach\";\n const y = position.y;\n if (y > 1.4) return \"heart\"; // 심장 - upper chest\n if (y > 1.1) return \"stomach\"; // 명치 - solar plexus\n if (y > 0.8) return \"liver\"; // 간 - right side mid-torso\n if (y > 0.5) return \"spleen\"; // 비장 - left side mid-torso\n return \"kidney\"; // 신장 - lower back\n}\n\n/**\n * Map HitEffect intensity → penetration depth\n * 타격 강도 → 관통 깊이 매핑\n */\nfunction getPenetrationDepth(intensity: number): PenetrationDepth {\n if (intensity >= 1.5) return \"critical\";\n if (intensity >= 1.0) return \"deep\";\n if (intensity >= 0.5) return \"shallow\";\n return \"surface\";\n}\n\n/**\n * Map HitEffectType → audio effect type\n * 타격 유형 → 오디오 효과 매핑\n */\nfunction getAudioEffectType(type: HitEffectType): ParticleEffectType | null {\n switch (type) {\n case HitEffectType.HIT:\n case HitEffectType.COUNTER:\n return \"viscosity\";\n case HitEffectType.CRITICAL_HIT:\n return \"bone\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"organ\";\n case HitEffectType.BLOCK:\n case HitEffectType.PARRY:\n return \"bone\";\n default:\n return null;\n }\n}\n\n/** Maximum concurrent effects to prevent performance issues */\nconst MAX_BLOOD_EFFECTS = 8;\nconst MAX_ORGAN_EFFECTS = 4;\n\n/**\n * Simple deterministic hash from string → [0, 1)\n * Used for deterministic direction calculations inside useMemo\n */\nfunction hashToFloat(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 31 + str.charCodeAt(i)) | 0;\n }\n return Math.abs(hash % 10000) / 10000;\n}\n\n/**\n * CombatParticleEffects3D - Coordinates particle effects for combat\n *\n * Converts HitEffect events into:\n * - Blood viscosity particles (thin/medium/thick/gout based on strike type)\n * - Internal organ damage pulses (for vital point strikes)\n * - Synchronized audio triggers\n *\n * @korean 전투 입자 효과 통합 (타격→혈액물리+장기손상+오디오)\n */\nexport const CombatParticleEffects3D: React.FC<\n CombatParticleEffects3DProps\n> = ({ hitEffects, enabled = true, isMobile = false }) => {\n const processedIds = useRef<Set<string>>(new Set());\n\n const [bloodEffects, setBloodEffects] = useState<BloodViscosityEffect[]>([]);\n const [organEffects, setOrganEffects] = useState<InternalDamageEffect[]>([]);\n const [audioTriggers, setAudioTriggers] = useState<ParticleAudioTrigger[]>(\n [],\n );\n\n useEffect(() => {\n const newBlood: BloodViscosityEffect[] = [];\n const newOrgan: InternalDamageEffect[] = [];\n const newAudio: ParticleAudioTrigger[] = [];\n\n for (const hit of hitEffects) {\n if (processedIds.current.has(hit.id)) continue;\n processedIds.current.add(hit.id);\n\n const pos: [number, number, number] = [\n hit.position?.x ?? 0,\n hit.position?.y ?? 1.0,\n 0,\n ];\n\n const viscosity = getViscosityForHitType(hit.type);\n if (viscosity) {\n const angle = hashToFloat(hit.id) * Math.PI * 2;\n const ySpread = 0.3 + hashToFloat(hit.id + \"_y\") * 0.4;\n const dir: [number, number, number] = [\n Math.cos(angle) * 0.5,\n ySpread,\n Math.sin(angle) * 0.5,\n ];\n\n newBlood.push({\n id: `blood_${hit.id}`,\n position: pos,\n direction: dir,\n viscosityType: viscosity,\n intensity: Math.min(1, hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n if (hit.type === HitEffectType.VITAL_POINT_STRIKE) {\n newOrgan.push({\n id: `organ_${hit.id}`,\n position: pos,\n organType: getOrganForPosition(hit.position),\n penetrationDepth: getPenetrationDepth(hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n const audioType = getAudioEffectType(hit.type);\n if (audioType) {\n newAudio.push({\n effectType: audioType,\n intensity: Math.min(1, hit.intensity),\n timestamp: hit.timestamp,\n });\n }\n }\n\n if (newBlood.length > 0) {\n setBloodEffects((prev) =>\n [...prev, ...newBlood].slice(-MAX_BLOOD_EFFECTS),\n );\n }\n if (newOrgan.length > 0) {\n setOrganEffects((prev) =>\n [...prev, ...newOrgan].slice(-MAX_ORGAN_EFFECTS),\n );\n }\n if (newAudio.length > 0) {\n setAudioTriggers((prev) => [...prev, ...newAudio]);\n }\n\n if (processedIds.current.size > 500) {\n const arr = Array.from(processedIds.current);\n processedIds.current = new Set(arr.slice(-250));\n }\n }, [hitEffects]);\n\n const handleBloodComplete = useCallback((id: string) => {\n setBloodEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleOrganComplete = useCallback((id: string) => {\n setOrganEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleAudioProcessed = useCallback((timestamp: number) => {\n setAudioTriggers((prev) => prev.filter((t) => t.timestamp !== timestamp));\n }, []);\n\n if (!enabled) return null;\n\n return (\n <>\n {/* Blood Viscosity Particles - 혈액 점도 입자 */}\n <BloodViscosity3D\n effects={bloodEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleBloodComplete}\n />\n\n {/* Internal Organ Damage Pulses - 장기 손상 시각화 */}\n <InternalDamage3D\n effects={organEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleOrganComplete}\n />\n\n {/* Audio Coordination - 입자 효과 오디오 동기화 */}\n <ParticleAudio3D\n triggers={audioTriggers}\n enabled={enabled}\n onTriggerProcessed={handleAudioProcessed}\n />\n </>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CombatParticleEffects3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/CombatParticleEffects3D.tsx"],"sourcesContent":["/**\n * CombatParticleEffects3D - Particle effects coordinator for combat\n * 전투 입자 효과 통합 관리\n *\n * Maps HitEffect events to advanced particle effects:\n * - BloodViscosity3D for blood physics on hits\n * - InternalDamage3D for organ damage visualization on vital point strikes\n * - ParticleAudio3D for synchronized combat audio\n *\n * @module components/effects\n * @category Combat Effects\n * @korean 전투입자효과\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { HitEffect } from \"../../../../../systems\";\nimport { HitEffectType } from \"../../../../../systems/effects\";\nimport {\n BloodViscosity3D,\n type BloodViscosityEffect,\n type ViscosityType,\n} from \"./BloodViscosity3D\";\nimport {\n InternalDamage3D,\n type InternalDamageEffect,\n type OrganType,\n type PenetrationDepth,\n} from \"./InternalDamage3D\";\nimport {\n ParticleAudio3D,\n type ParticleAudioTrigger,\n type ParticleEffectType,\n} from \"./ParticleAudio3D\";\n\n/**\n * Props for CombatParticleEffects3D\n */\nexport interface CombatParticleEffects3DProps {\n /** Active hit effects from the combat state */\n readonly hitEffects: readonly HitEffect[];\n /** Whether effects are enabled */\n readonly enabled?: boolean;\n /** Mobile optimization flag */\n readonly isMobile?: boolean;\n}\n\n/**\n * Map HitEffectType → blood viscosity type\n * 타격 유형 → 혈액 점도 매핑\n */\nfunction getViscosityForHitType(type: HitEffectType): ViscosityType | null {\n switch (type) {\n case HitEffectType.HIT:\n return \"thin\";\n case HitEffectType.COUNTER:\n return \"medium\";\n case HitEffectType.CRITICAL_HIT:\n return \"thick\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"gout\";\n default:\n return null;\n }\n}\n\n/**\n * Map HitEffectType → organ type based on strike position\n * 급소 유형 → 장기 매핑 (타격 위치 기반)\n */\nfunction getOrganForPosition(\n position: { x: number; y: number } | undefined,\n): OrganType {\n if (!position) return \"stomach\";\n const y = position.y;\n if (y > 1.4) return \"heart\"; // 심장 - upper chest\n if (y > 1.1) return \"stomach\"; // 명치 - solar plexus\n if (y > 0.8) return \"liver\"; // 간 - right side mid-torso\n if (y > 0.5) return \"spleen\"; // 비장 - left side mid-torso\n return \"kidney\"; // 신장 - lower back\n}\n\n/**\n * Map HitEffect intensity → penetration depth\n * 타격 강도 → 관통 깊이 매핑\n */\nfunction getPenetrationDepth(intensity: number): PenetrationDepth {\n if (intensity >= 1.5) return \"critical\";\n if (intensity >= 1.0) return \"deep\";\n if (intensity >= 0.5) return \"shallow\";\n return \"surface\";\n}\n\n/**\n * Map HitEffectType → audio effect type\n * 타격 유형 → 오디오 효과 매핑\n */\nfunction getAudioEffectType(type: HitEffectType): ParticleEffectType | null {\n switch (type) {\n case HitEffectType.HIT:\n case HitEffectType.COUNTER:\n return \"viscosity\";\n case HitEffectType.CRITICAL_HIT:\n return \"bone\";\n case HitEffectType.VITAL_POINT_STRIKE:\n return \"organ\";\n case HitEffectType.BLOCK:\n case HitEffectType.PARRY:\n return \"bone\";\n default:\n return null;\n }\n}\n\n/** Maximum concurrent effects to prevent performance issues */\nconst MAX_BLOOD_EFFECTS = 8;\nconst MAX_ORGAN_EFFECTS = 4;\n\n/**\n * Simple deterministic hash from string → [0, 1)\n * Used for deterministic direction calculations inside useMemo\n */\nfunction hashToFloat(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 31 + str.charCodeAt(i)) | 0;\n }\n return Math.abs(hash % 10000) / 10000;\n}\n\n/**\n * CombatParticleEffects3D - Coordinates particle effects for combat\n *\n * Converts HitEffect events into:\n * - Blood viscosity particles (thin/medium/thick/gout based on strike type)\n * - Internal organ damage pulses (for vital point strikes)\n * - Synchronized audio triggers\n *\n * @korean 전투 입자 효과 통합 (타격→혈액물리+장기손상+오디오)\n */\nexport const CombatParticleEffects3D: React.FC<\n CombatParticleEffects3DProps\n> = ({ hitEffects, enabled = true, isMobile = false }) => {\n const processedIds = useRef<Set<string>>(new Set());\n\n const [bloodEffects, setBloodEffects] = useState<BloodViscosityEffect[]>([]);\n const [organEffects, setOrganEffects] = useState<InternalDamageEffect[]>([]);\n const [audioTriggers, setAudioTriggers] = useState<ParticleAudioTrigger[]>(\n [],\n );\n\n useEffect(() => {\n const newBlood: BloodViscosityEffect[] = [];\n const newOrgan: InternalDamageEffect[] = [];\n const newAudio: ParticleAudioTrigger[] = [];\n\n for (const hit of hitEffects) {\n if (processedIds.current.has(hit.id)) continue;\n processedIds.current.add(hit.id);\n\n const pos: [number, number, number] = [\n hit.position?.x ?? 0,\n hit.position?.y ?? 1.0,\n 0,\n ];\n\n const viscosity = getViscosityForHitType(hit.type);\n if (viscosity) {\n const angle = hashToFloat(hit.id) * Math.PI * 2;\n const ySpread = 0.3 + hashToFloat(hit.id + \"_y\") * 0.4;\n const dir: [number, number, number] = [\n Math.cos(angle) * 0.5,\n ySpread,\n Math.sin(angle) * 0.5,\n ];\n\n newBlood.push({\n id: `blood_${hit.id}`,\n position: pos,\n direction: dir,\n viscosityType: viscosity,\n intensity: Math.min(1, hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n if (hit.type === HitEffectType.VITAL_POINT_STRIKE) {\n newOrgan.push({\n id: `organ_${hit.id}`,\n position: pos,\n organType: getOrganForPosition(hit.position),\n penetrationDepth: getPenetrationDepth(hit.intensity),\n startTime: hit.startTime,\n });\n }\n\n const audioType = getAudioEffectType(hit.type);\n if (audioType) {\n newAudio.push({\n effectType: audioType,\n intensity: Math.min(1, hit.intensity),\n timestamp: hit.timestamp,\n });\n }\n }\n\n if (newBlood.length > 0) {\n setBloodEffects((prev) =>\n [...prev, ...newBlood].slice(-MAX_BLOOD_EFFECTS),\n );\n }\n if (newOrgan.length > 0) {\n setOrganEffects((prev) =>\n [...prev, ...newOrgan].slice(-MAX_ORGAN_EFFECTS),\n );\n }\n if (newAudio.length > 0) {\n setAudioTriggers((prev) => [...prev, ...newAudio]);\n }\n\n if (processedIds.current.size > 500) {\n const arr = Array.from(processedIds.current);\n processedIds.current = new Set(arr.slice(-250));\n }\n }, [hitEffects]);\n\n const handleBloodComplete = useCallback((id: string) => {\n setBloodEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleOrganComplete = useCallback((id: string) => {\n setOrganEffects((prev) => prev.filter((e) => e.id !== id));\n }, []);\n\n const handleAudioProcessed = useCallback((timestamp: number) => {\n setAudioTriggers((prev) => prev.filter((t) => t.timestamp !== timestamp));\n }, []);\n\n if (!enabled) return null;\n\n return (\n <>\n {/* Blood Viscosity Particles - 혈액 점도 입자 */}\n <BloodViscosity3D\n effects={bloodEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleBloodComplete}\n />\n\n {/* Internal Organ Damage Pulses - 장기 손상 시각화 */}\n <InternalDamage3D\n effects={organEffects}\n enabled={enabled}\n isMobile={isMobile}\n onEffectComplete={handleOrganComplete}\n />\n\n {/* Audio Coordination - 입자 효과 오디오 동기화 */}\n <ParticleAudio3D\n triggers={audioTriggers}\n enabled={enabled}\n onTriggerProcessed={handleAudioProcessed}\n />\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InternalDamage3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/InternalDamage3D.tsx"],"sourcesContent":["/**\n * InternalDamage3D - Deep red organ pulses for Korean martial arts organ strikes\n *\n * Priority #4: Internal Damage Visualization\n * - Deep red organ pulses (liver, kidney, spleen, stomach, heart)\n * - Tissue deformation ripples\n * - Penetration depth visualization\n * - Critical organ damage feedback\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~120+ per effect to 2 pooled objects\n * - Pooling strategy:\n * 1. Temporary Color objects for particle initialization use pool\n * 2. Colors are copied to Float32Array (no ownership needed)\n * 3. All pooled objects released after particle system creation\n * - Estimated reduction:\n * - Pulse particles: 60 Color allocations → 1 pooled object\n * - Ripple particles: 30 Color allocations → 1 pooled object\n * - Total: ~90 Color allocations per effect → 2 pooled objects\n *\n * Korean martial arts context (내장 공격 - Internal organ strikes):\n * - 간격 (Liver strike) - Right side body blow\n * - 신장격 (Kidney strike) - Lower back strike\n * - 비장격 (Spleen strike) - Left side body blow\n * - 명치격 (Solar plexus) - Stomach strike\n * - 심장격 (Heart strike) - Chest strike (critical)\n */\n\nimport React, { useEffect, useMemo, useRef } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Organ types for Korean martial arts internal strikes\n */\nexport type OrganType = 'liver' | 'kidney' | 'spleen' | 'stomach' | 'heart';\n\n/**\n * Penetration depth levels for organ damage\n */\nexport type PenetrationDepth = 'surface' | 'shallow' | 'deep' | 'critical';\n\n/**\n * Individual internal damage effect\n */\nexport interface InternalDamageEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly organType: OrganType;\n readonly penetrationDepth: PenetrationDepth;\n readonly startTime: number;\n}\n\n/**\n * Props for InternalDamage3D component\n */\nexport interface InternalDamage3DProps {\n readonly effects: readonly InternalDamageEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst INTERNAL_DAMAGE_CONSTANTS = {\n PULSE_SPEED: 2.5, // m/s expansion rate\n \n PULSE_LIFETIME: 1.5, // seconds for pulse to complete\n RIPPLE_LIFETIME: 0.8, // seconds for tissue ripple\n \n MAX_RADIUS: {\n surface: 0.4,\n shallow: 0.7,\n deep: 1.0,\n critical: 1.2,\n },\n \n INTENSITY: {\n surface: 0.5,\n shallow: 0.8,\n deep: 1.2,\n critical: 1.5,\n },\n \n PULSE_PARTICLES: 60,\n RIPPLE_PARTICLES: 30,\n \n ORGAN_COLOR: 0x8b0000, // Dark red for organs\n RIPPLE_COLOR: 0xdc143c, // Crimson for tissue ripples\n \n EMISSIVE_INTENSITY: 0.8,\n} as const;\n\n/**\n * InternalDamage3D - Visualizes internal organ damage with deep red pulses\n *\n * Features:\n * - Expanding sphere pulses from organ impacts\n * - Tissue deformation ripples\n * - Penetration depth-based sizing\n * - Korean martial arts organ strike feedback\n * - Mobile optimization (50% particles)\n */\nexport const InternalDamage3D: React.FC<InternalDamage3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >\n >(new Map());\n\n const particleCounts = useMemo(() => {\n const pulseCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES;\n const rippleCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES;\n return { pulseCount, rippleCount };\n }, [isMobile]);\n\n const createPulseParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { pulseCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(pulseCount * 3);\n const colors = new Float32Array(pulseCount * 3);\n const sizes = new Float32Array(pulseCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.ORGAN_COLOR);\n\n for (let i = 0; i < pulseCount; i++) {\n\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const baseSize = 0.03;\n const depthMultiplier =\n INTERNAL_DAMAGE_CONSTANTS.INTENSITY[effect.penetrationDepth];\n sizes[i] = baseSize * depthMultiplier;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.05,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData = { positions: positions.slice() };\n for (let i = 0; i < pulseCount; i++) {\n const phi = Math.acos(1 - 2 * (i + 0.5) / pulseCount);\n const theta = Math.PI * (1 + Math.sqrt(5)) * i;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`phi_${i}`] = phi;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`theta_${i}`] = theta;\n }\n\n return points;\n },\n [particleCounts]\n );\n\n const createRippleParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { rippleCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(rippleCount * 3);\n const colors = new Float32Array(rippleCount * 3);\n const sizes = new Float32Array(rippleCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_COLOR);\n\n for (let i = 0; i < rippleCount; i++) {\n const angle = (i / rippleCount) * Math.PI * 2;\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n sizes[i] = 0.02;\n\n (geometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`] = angle;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.03,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return points;\n },\n [particleCounts]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const pulseParticles = createPulseParticles(effect);\n const rippleParticles = createRippleParticles(effect);\n updated.set(effect.id, {\n pulseParticles,\n rippleParticles,\n startTime: effect.startTime,\n effect,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createPulseParticles, createRippleParticles]);\n\n const effectInstancesRef = useRef<Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >>(effectInstances);\n useEffect(() => {\n effectInstancesRef.current = effectInstances;\n }, [effectInstances]);\n\n useEffect(() => {\n return () => {\n effectInstancesRef.current.forEach((instance: {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }) => {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n });\n };\n }, []); // Empty deps - cleanup on unmount only\n\n useFrame(() => {\n if (!enabled || effectInstances.size === 0) return;\n\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n const { effect } = instance;\n\n const pulseProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME,\n 1.0\n );\n if (pulseProgress < 1.0) {\n const pulseGeometry = instance.pulseParticles.geometry;\n const pulsePositions = pulseGeometry.attributes.position\n .array as Float32Array;\n const maxRadius =\n INTERNAL_DAMAGE_CONSTANTS.MAX_RADIUS[effect.penetrationDepth];\n\n const { pulseCount } = particleCounts;\n const sphereData = (pulseGeometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData;\n for (let i = 0; i < pulseCount; i++) {\n const phi = sphereData[`phi_${i}`] as number;\n const theta = sphereData[`theta_${i}`] as number;\n const radius = maxRadius * pulseProgress;\n\n pulsePositions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);\n pulsePositions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);\n pulsePositions[i * 3 + 2] = radius * Math.cos(phi);\n }\n\n pulseGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.pulseParticles.material as THREE.PointsMaterial;\n material.opacity = 1.0 - pulseProgress * 0.7;\n }\n\n const rippleProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME,\n 1.0\n );\n if (rippleProgress < 1.0) {\n const rippleGeometry = instance.rippleParticles.geometry;\n const ripplePositions = rippleGeometry.attributes.position\n .array as Float32Array;\n\n const { rippleCount } = particleCounts;\n const rippleRadius = 0.6 * rippleProgress;\n for (let i = 0; i < rippleCount; i++) {\n const angle = (rippleGeometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`];\n ripplePositions[i * 3] = rippleRadius * Math.cos(angle);\n ripplePositions[i * 3 + 1] = 0;\n ripplePositions[i * 3 + 2] = rippleRadius * Math.sin(angle);\n }\n\n rippleGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.rippleParticles\n .material as THREE.PointsMaterial;\n material.opacity = 1.0 - rippleProgress;\n }\n\n if (\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME &&\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME\n ) {\n completedIds.push(id);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <React.Fragment key={instance.effect.id}>\n <primitive object={instance.pulseParticles} />\n <primitive object={instance.rippleParticles} />\n </React.Fragment>\n ))}\n </>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InternalDamage3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/InternalDamage3D.tsx"],"sourcesContent":["/**\n * InternalDamage3D - Deep red organ pulses for Korean martial arts organ strikes\n *\n * Priority #4: Internal Damage Visualization\n * - Deep red organ pulses (liver, kidney, spleen, stomach, heart)\n * - Tissue deformation ripples\n * - Penetration depth visualization\n * - Critical organ damage feedback\n *\n * PERFORMANCE OPTIMIZATION (Object Pooling):\n * - Reduced allocations from ~120+ per effect to 2 pooled objects\n * - Pooling strategy:\n * 1. Temporary Color objects for particle initialization use pool\n * 2. Colors are copied to Float32Array (no ownership needed)\n * 3. All pooled objects released after particle system creation\n * - Estimated reduction:\n * - Pulse particles: 60 Color allocations → 1 pooled object\n * - Ripple particles: 30 Color allocations → 1 pooled object\n * - Total: ~90 Color allocations per effect → 2 pooled objects\n *\n * Korean martial arts context (내장 공격 - Internal organ strikes):\n * - 간격 (Liver strike) - Right side body blow\n * - 신장격 (Kidney strike) - Lower back strike\n * - 비장격 (Spleen strike) - Left side body blow\n * - 명치격 (Solar plexus) - Stomach strike\n * - 심장격 (Heart strike) - Chest strike (critical)\n */\n\nimport React, { useEffect, useMemo, useRef } from 'react';\nimport { useFrame } from '@react-three/fiber';\nimport * as THREE from 'three';\nimport { ThreeObjectPools } from '../../../../../utils/threeObjectPool';\n\n/**\n * Organ types for Korean martial arts internal strikes\n */\nexport type OrganType = 'liver' | 'kidney' | 'spleen' | 'stomach' | 'heart';\n\n/**\n * Penetration depth levels for organ damage\n */\nexport type PenetrationDepth = 'surface' | 'shallow' | 'deep' | 'critical';\n\n/**\n * Individual internal damage effect\n */\nexport interface InternalDamageEffect {\n readonly id: string;\n readonly position: [number, number, number];\n readonly organType: OrganType;\n readonly penetrationDepth: PenetrationDepth;\n readonly startTime: number;\n}\n\n/**\n * Props for InternalDamage3D component\n */\nexport interface InternalDamage3DProps {\n readonly effects: readonly InternalDamageEffect[];\n readonly enabled?: boolean;\n readonly isMobile?: boolean;\n readonly onEffectComplete?: (id: string) => void;\n}\n\nconst INTERNAL_DAMAGE_CONSTANTS = {\n PULSE_SPEED: 2.5, // m/s expansion rate\n \n PULSE_LIFETIME: 1.5, // seconds for pulse to complete\n RIPPLE_LIFETIME: 0.8, // seconds for tissue ripple\n \n MAX_RADIUS: {\n surface: 0.4,\n shallow: 0.7,\n deep: 1.0,\n critical: 1.2,\n },\n \n INTENSITY: {\n surface: 0.5,\n shallow: 0.8,\n deep: 1.2,\n critical: 1.5,\n },\n \n PULSE_PARTICLES: 60,\n RIPPLE_PARTICLES: 30,\n \n ORGAN_COLOR: 0x8b0000, // Dark red for organs\n RIPPLE_COLOR: 0xdc143c, // Crimson for tissue ripples\n \n EMISSIVE_INTENSITY: 0.8,\n} as const;\n\n/**\n * InternalDamage3D - Visualizes internal organ damage with deep red pulses\n *\n * Features:\n * - Expanding sphere pulses from organ impacts\n * - Tissue deformation ripples\n * - Penetration depth-based sizing\n * - Korean martial arts organ strike feedback\n * - Mobile optimization (50% particles)\n */\nexport const InternalDamage3D: React.FC<InternalDamage3DProps> = ({\n effects,\n enabled = true,\n isMobile = false,\n onEffectComplete,\n}) => {\n const [effectInstances, setEffectInstances] = React.useState<\n Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >\n >(new Map());\n\n const particleCounts = useMemo(() => {\n const pulseCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.PULSE_PARTICLES;\n const rippleCount = isMobile\n ? Math.floor(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES * 0.5)\n : INTERNAL_DAMAGE_CONSTANTS.RIPPLE_PARTICLES;\n return { pulseCount, rippleCount };\n }, [isMobile]);\n\n const createPulseParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { pulseCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(pulseCount * 3);\n const colors = new Float32Array(pulseCount * 3);\n const sizes = new Float32Array(pulseCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.ORGAN_COLOR);\n\n for (let i = 0; i < pulseCount; i++) {\n\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n const baseSize = 0.03;\n const depthMultiplier =\n INTERNAL_DAMAGE_CONSTANTS.INTENSITY[effect.penetrationDepth];\n sizes[i] = baseSize * depthMultiplier;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.05,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData = { positions: positions.slice() };\n for (let i = 0; i < pulseCount; i++) {\n const phi = Math.acos(1 - 2 * (i + 0.5) / pulseCount);\n const theta = Math.PI * (1 + Math.sqrt(5)) * i;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`phi_${i}`] = phi;\n (geometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData[`theta_${i}`] = theta;\n }\n\n return points;\n },\n [particleCounts]\n );\n\n const createRippleParticles = useMemo(\n () => (effect: InternalDamageEffect) => {\n const { rippleCount } = particleCounts;\n const geometry = new THREE.BufferGeometry();\n const positions = new Float32Array(rippleCount * 3);\n const colors = new Float32Array(rippleCount * 3);\n const sizes = new Float32Array(rippleCount);\n\n const tempColor = ThreeObjectPools.color.acquire();\n\n try {\n tempColor.set(INTERNAL_DAMAGE_CONSTANTS.RIPPLE_COLOR);\n\n for (let i = 0; i < rippleCount; i++) {\n const angle = (i / rippleCount) * Math.PI * 2;\n positions[i * 3] = 0;\n positions[i * 3 + 1] = 0;\n positions[i * 3 + 2] = 0;\n\n colors[i * 3] = tempColor.r;\n colors[i * 3 + 1] = tempColor.g;\n colors[i * 3 + 2] = tempColor.b;\n\n sizes[i] = 0.02;\n\n (geometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`] = angle;\n }\n } finally {\n ThreeObjectPools.color.release(tempColor);\n }\n\n geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));\n geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));\n geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));\n\n const material = new THREE.PointsMaterial({\n size: 0.03,\n vertexColors: true,\n transparent: true,\n opacity: 1.0,\n blending: THREE.NormalBlending,\n depthWrite: false,\n sizeAttenuation: true,\n });\n\n const points = new THREE.Points(geometry, material);\n points.position.set(...effect.position);\n\n return points;\n },\n [particleCounts]\n );\n\n useEffect(() => {\n if (!enabled) return;\n\n setEffectInstances((prev) => {\n const updated = new Map(prev);\n\n effects.forEach((effect) => {\n if (!updated.has(effect.id)) {\n const pulseParticles = createPulseParticles(effect);\n const rippleParticles = createRippleParticles(effect);\n updated.set(effect.id, {\n pulseParticles,\n rippleParticles,\n startTime: effect.startTime,\n effect,\n });\n }\n });\n\n const currentIds = new Set(effects.map((e) => e.id));\n Array.from(updated.keys()).forEach((id) => {\n if (!currentIds.has(id)) {\n const instance = updated.get(id);\n if (instance) {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n }\n updated.delete(id);\n }\n });\n\n return updated;\n });\n }, [effects, enabled, createPulseParticles, createRippleParticles]);\n\n const effectInstancesRef = useRef<Map<\n string,\n {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }\n >>(effectInstances);\n useEffect(() => {\n effectInstancesRef.current = effectInstances;\n }, [effectInstances]);\n\n useEffect(() => {\n return () => {\n effectInstancesRef.current.forEach((instance: {\n pulseParticles: THREE.Points;\n rippleParticles: THREE.Points;\n startTime: number;\n effect: InternalDamageEffect;\n }) => {\n instance.pulseParticles.geometry.dispose();\n (instance.pulseParticles.material as THREE.Material).dispose();\n instance.rippleParticles.geometry.dispose();\n (instance.rippleParticles.material as THREE.Material).dispose();\n });\n };\n }, []); // Empty deps - cleanup on unmount only\n\n useFrame(() => {\n if (!enabled || effectInstances.size === 0) return;\n\n const now = Date.now();\n const completedIds: string[] = [];\n\n effectInstances.forEach((instance, id) => {\n const elapsed = (now - instance.startTime) / 1000;\n const { effect } = instance;\n\n const pulseProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME,\n 1.0\n );\n if (pulseProgress < 1.0) {\n const pulseGeometry = instance.pulseParticles.geometry;\n const pulsePositions = pulseGeometry.attributes.position\n .array as Float32Array;\n const maxRadius =\n INTERNAL_DAMAGE_CONSTANTS.MAX_RADIUS[effect.penetrationDepth];\n\n const { pulseCount } = particleCounts;\n const sphereData = (pulseGeometry as THREE.BufferGeometry & { sphereData: Record<string, number | Float32Array> }).sphereData;\n for (let i = 0; i < pulseCount; i++) {\n const phi = sphereData[`phi_${i}`] as number;\n const theta = sphereData[`theta_${i}`] as number;\n const radius = maxRadius * pulseProgress;\n\n pulsePositions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);\n pulsePositions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);\n pulsePositions[i * 3 + 2] = radius * Math.cos(phi);\n }\n\n pulseGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.pulseParticles.material as THREE.PointsMaterial;\n material.opacity = 1.0 - pulseProgress * 0.7;\n }\n\n const rippleProgress = Math.min(\n elapsed / INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME,\n 1.0\n );\n if (rippleProgress < 1.0) {\n const rippleGeometry = instance.rippleParticles.geometry;\n const ripplePositions = rippleGeometry.attributes.position\n .array as Float32Array;\n\n const { rippleCount } = particleCounts;\n const rippleRadius = 0.6 * rippleProgress;\n for (let i = 0; i < rippleCount; i++) {\n const angle = (rippleGeometry as THREE.BufferGeometry & { [key: string]: number })[`angle_${i}`];\n ripplePositions[i * 3] = rippleRadius * Math.cos(angle);\n ripplePositions[i * 3 + 1] = 0;\n ripplePositions[i * 3 + 2] = rippleRadius * Math.sin(angle);\n }\n\n rippleGeometry.attributes.position.needsUpdate = true;\n\n const material = instance.rippleParticles\n .material as THREE.PointsMaterial;\n material.opacity = 1.0 - rippleProgress;\n }\n\n if (\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.PULSE_LIFETIME &&\n elapsed >= INTERNAL_DAMAGE_CONSTANTS.RIPPLE_LIFETIME\n ) {\n completedIds.push(id);\n }\n });\n\n completedIds.forEach((id) => {\n onEffectComplete?.(id);\n });\n });\n\n return (\n <>\n {Array.from(effectInstances.values()).map((instance) => (\n <React.Fragment key={instance.effect.id}>\n <primitive object={instance.pulseParticles} />\n <primitive object={instance.rippleParticles} />\n </React.Fragment>\n ))}\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ParticleAudio3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/ParticleAudio3D.tsx"],"sourcesContent":["/* eslint-disable react-refresh/only-export-components */\n/**\n * ParticleAudio3D - Audio integration for particle effects\n *\n * Priority #6: Combat Audio Integration\n * - Maps particle effects to appropriate sounds\n * - Uses existing sound assets only\n * - Synchronizes with particle lifecycles\n * - Debounces rapid triggers\n *\n * Sound mappings (existing assets):\n * - Arterial spray → ki_release variants (electric energy release)\n * - Bone fractures → block_break variants (bone crack sounds)\n * - Nerve strikes → energy_pulse variants (electric pulse)\n * - Organ damage → hit_flesh + body_realistic_sound (layered impact)\n * - Blood viscosity → hit_flesh variants (lighter flesh impacts)\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\n\n/**\n * Particle effect types for audio mapping\n */\nexport type ParticleEffectType =\n | \"arterial\"\n | \"bone\"\n | \"nerve\"\n | \"organ\"\n | \"viscosity\";\n\n/**\n * Audio trigger for particle effect\n */\nexport interface ParticleAudioTrigger {\n readonly effectType: ParticleEffectType;\n readonly intensity: number; // 0.0-1.0, affects volume\n readonly timestamp: number; // Used for deduplication\n}\n\n/**\n * Props for ParticleAudio3D component\n */\nexport interface ParticleAudio3DProps {\n readonly triggers: readonly ParticleAudioTrigger[];\n readonly enabled?: boolean;\n readonly onTriggerProcessed?: (timestamp: number) => void;\n}\n\nconst DEBOUNCE_TIME = 100;\n\nconst SOUND_MAPPINGS: Record<ParticleEffectType, string[]> = {\n arterial: [\n \"ki_release\",\n \"ki_release_1\",\n \"ki_release_2\",\n \"ki_release_3\",\n \"ki_release_4\",\n ],\n bone: [\n \"block_break\",\n \"block_break_1\",\n \"block_break_2\",\n \"block_break_3\",\n \"block_break_4\",\n ],\n nerve: [\n \"energy_pulse\",\n \"energy_pulse_1\",\n \"energy_pulse_2\",\n \"energy_pulse_3\",\n \"energy_pulse_4\",\n ],\n organ: [\"hit_flesh\", \"hit_flesh_1\", \"hit_flesh_2\", \"body_realistic_sound\"],\n viscosity: [\"hit_flesh_3\", \"hit_flesh_4\"],\n};\n\n/**\n * ParticleAudio3D - Lightweight audio coordination for particle effects\n *\n * Features:\n * - Debounced audio triggers (max 1 per 100ms per type)\n * - Intensity-based volume scaling\n * - Random sound variant selection\n * - Uses existing audio assets only\n * - No Three.js rendering (pure coordination logic)\n */\nexport const ParticleAudio3D: React.FC<ParticleAudio3DProps> = ({\n triggers,\n enabled = true,\n onTriggerProcessed,\n}) => {\n const audio = useAudio();\n\n const lastTriggerTime = useRef<Record<ParticleEffectType, number>>({\n arterial: 0,\n bone: 0,\n nerve: 0,\n organ: 0,\n viscosity: 0,\n });\n\n const processedTimestamps = useRef<Set<number>>(new Set());\n\n useEffect(() => {\n if (!enabled || !audio.isInitialized) return;\n\n const now = Date.now();\n\n triggers.forEach((trigger) => {\n if (processedTimestamps.current.has(trigger.timestamp)) {\n return;\n }\n\n const lastTime = lastTriggerTime.current[trigger.effectType];\n if (now - lastTime < DEBOUNCE_TIME) {\n return;\n }\n\n const soundIds = SOUND_MAPPINGS[trigger.effectType];\n const soundId = soundIds[Math.floor(Math.random() * soundIds.length)];\n\n try {\n audio.playSFX(soundId);\n } catch (error) {\n console.warn(`Failed to play particle audio: ${soundId}`, error);\n }\n\n lastTriggerTime.current[trigger.effectType] = now;\n processedTimestamps.current.add(trigger.timestamp);\n\n onTriggerProcessed?.(trigger.timestamp);\n });\n\n if (processedTimestamps.current.size > 1000) {\n const timestamps = Array.from(processedTimestamps.current).sort(\n (a, b) => b - a,\n );\n processedTimestamps.current = new Set(timestamps.slice(0, 500));\n }\n }, [triggers, enabled, audio, onTriggerProcessed]);\n\n return null;\n};\n\n/**\n * Helper function to create audio triggers from particle effects\n */\nexport function createAudioTrigger(\n effectType: ParticleEffectType,\n intensity: number,\n): ParticleAudioTrigger {\n return {\n effectType,\n intensity: Math.max(0, Math.min(1, intensity)),\n timestamp: Date.now(),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiDA,IAAM,gBAAgB;AAEtB,IAAM,iBAAuD;CAC3D,UAAU;EACR;EACA;EACA;EACA;EACA;CACF;CACA,MAAM;EACJ;EACA;EACA;EACA;EACA;CACF;CACA,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;CACA,OAAO;EAAC;EAAa;EAAe;EAAe;CAAsB;CACzE,WAAW,CAAC,eAAe,aAAa;AAC1C;;;;;;;;;;;AAYA,IAAa,mBAAmD,EAC9D,UACA,UAAU,MACV,yBACI;CACJ,MAAM,QAAQ,SAAS;CAEvB,MAAM,kBAAkB,OAA2C;EACjE,UAAU;EACV,MAAM;EACN,OAAO;EACP,OAAO;EACP,WAAW;CACb,CAAC;CAED,MAAM,sBAAsB,uBAAoB,IAAI,IAAI,CAAC;CAEzD,gBAAgB;EACd,IAAI,CAAC,WAAW,CAAC,MAAM,eAAe;EAEtC,MAAM,MAAM,KAAK,IAAI;EAErB,SAAS,SAAS,YAAY;GAC5B,IAAI,oBAAoB,QAAQ,IAAI,QAAQ,SAAS,GACnD;GAIF,IAAI,MADa,gBAAgB,QAAQ,QAAQ,cAC5B,eACnB;GAGF,MAAM,WAAW,eAAe,QAAQ;GACxC,MAAM,UAAU,SAAS,KAAK,MAAM,KAAK,OAAO,IAAI,SAAS,MAAM;GAEnE,IAAI;IACF,MAAM,QAAQ,OAAO;GACvB,SAAS,OAAO;IACd,QAAQ,KAAK,kCAAkC,WAAW,KAAK;GACjE;GAEA,gBAAgB,QAAQ,QAAQ,cAAc;GAC9C,oBAAoB,QAAQ,IAAI,QAAQ,SAAS;GAEjD,qBAAqB,QAAQ,SAAS;EACxC,CAAC;EAED,IAAI,oBAAoB,QAAQ,OAAO,KAAM;GAC3C,MAAM,aAAa,MAAM,KAAK,oBAAoB,OAAO,
|
|
1
|
+
{"version":3,"file":"ParticleAudio3D.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/effects/ParticleAudio3D.tsx"],"sourcesContent":["/* eslint-disable react-refresh/only-export-components */\n/**\n * ParticleAudio3D - Audio integration for particle effects\n *\n * Priority #6: Combat Audio Integration\n * - Maps particle effects to appropriate sounds\n * - Uses existing sound assets only\n * - Synchronizes with particle lifecycles\n * - Debounces rapid triggers\n *\n * Sound mappings (existing assets):\n * - Arterial spray → ki_release variants (electric energy release)\n * - Bone fractures → block_break variants (bone crack sounds)\n * - Nerve strikes → energy_pulse variants (electric pulse)\n * - Organ damage → hit_flesh + body_realistic_sound (layered impact)\n * - Blood viscosity → hit_flesh variants (lighter flesh impacts)\n */\n\nimport { useEffect, useRef } from \"react\";\nimport { useAudio } from \"../../../../../audio/AudioProvider\";\n\n/**\n * Particle effect types for audio mapping\n */\nexport type ParticleEffectType =\n | \"arterial\"\n | \"bone\"\n | \"nerve\"\n | \"organ\"\n | \"viscosity\";\n\n/**\n * Audio trigger for particle effect\n */\nexport interface ParticleAudioTrigger {\n readonly effectType: ParticleEffectType;\n readonly intensity: number; // 0.0-1.0, affects volume\n readonly timestamp: number; // Used for deduplication\n}\n\n/**\n * Props for ParticleAudio3D component\n */\nexport interface ParticleAudio3DProps {\n readonly triggers: readonly ParticleAudioTrigger[];\n readonly enabled?: boolean;\n readonly onTriggerProcessed?: (timestamp: number) => void;\n}\n\nconst DEBOUNCE_TIME = 100;\n\nconst SOUND_MAPPINGS: Record<ParticleEffectType, string[]> = {\n arterial: [\n \"ki_release\",\n \"ki_release_1\",\n \"ki_release_2\",\n \"ki_release_3\",\n \"ki_release_4\",\n ],\n bone: [\n \"block_break\",\n \"block_break_1\",\n \"block_break_2\",\n \"block_break_3\",\n \"block_break_4\",\n ],\n nerve: [\n \"energy_pulse\",\n \"energy_pulse_1\",\n \"energy_pulse_2\",\n \"energy_pulse_3\",\n \"energy_pulse_4\",\n ],\n organ: [\"hit_flesh\", \"hit_flesh_1\", \"hit_flesh_2\", \"body_realistic_sound\"],\n viscosity: [\"hit_flesh_3\", \"hit_flesh_4\"],\n};\n\n/**\n * ParticleAudio3D - Lightweight audio coordination for particle effects\n *\n * Features:\n * - Debounced audio triggers (max 1 per 100ms per type)\n * - Intensity-based volume scaling\n * - Random sound variant selection\n * - Uses existing audio assets only\n * - No Three.js rendering (pure coordination logic)\n */\nexport const ParticleAudio3D: React.FC<ParticleAudio3DProps> = ({\n triggers,\n enabled = true,\n onTriggerProcessed,\n}) => {\n const audio = useAudio();\n\n const lastTriggerTime = useRef<Record<ParticleEffectType, number>>({\n arterial: 0,\n bone: 0,\n nerve: 0,\n organ: 0,\n viscosity: 0,\n });\n\n const processedTimestamps = useRef<Set<number>>(new Set());\n\n useEffect(() => {\n if (!enabled || !audio.isInitialized) return;\n\n const now = Date.now();\n\n triggers.forEach((trigger) => {\n if (processedTimestamps.current.has(trigger.timestamp)) {\n return;\n }\n\n const lastTime = lastTriggerTime.current[trigger.effectType];\n if (now - lastTime < DEBOUNCE_TIME) {\n return;\n }\n\n const soundIds = SOUND_MAPPINGS[trigger.effectType];\n const soundId = soundIds[Math.floor(Math.random() * soundIds.length)];\n\n try {\n audio.playSFX(soundId);\n } catch (error) {\n console.warn(`Failed to play particle audio: ${soundId}`, error);\n }\n\n lastTriggerTime.current[trigger.effectType] = now;\n processedTimestamps.current.add(trigger.timestamp);\n\n onTriggerProcessed?.(trigger.timestamp);\n });\n\n if (processedTimestamps.current.size > 1000) {\n const timestamps = Array.from(processedTimestamps.current).sort(\n (a, b) => b - a,\n );\n processedTimestamps.current = new Set(timestamps.slice(0, 500));\n }\n }, [triggers, enabled, audio, onTriggerProcessed]);\n\n return null;\n};\n\n/**\n * Helper function to create audio triggers from particle effects\n */\nexport function createAudioTrigger(\n effectType: ParticleEffectType,\n intensity: number,\n): ParticleAudioTrigger {\n return {\n effectType,\n intensity: Math.max(0, Math.min(1, intensity)),\n timestamp: Date.now(),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiDA,IAAM,gBAAgB;AAEtB,IAAM,iBAAuD;CAC3D,UAAU;EACR;EACA;EACA;EACA;EACA;CACF;CACA,MAAM;EACJ;EACA;EACA;EACA;EACA;CACF;CACA,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;CACA,OAAO;EAAC;EAAa;EAAe;EAAe;CAAsB;CACzE,WAAW,CAAC,eAAe,aAAa;AAC1C;;;;;;;;;;;AAYA,IAAa,mBAAmD,EAC9D,UACA,UAAU,MACV,yBACI;CACJ,MAAM,QAAQ,SAAS;CAEvB,MAAM,kBAAkB,OAA2C;EACjE,UAAU;EACV,MAAM;EACN,OAAO;EACP,OAAO;EACP,WAAW;CACb,CAAC;CAED,MAAM,sBAAsB,uBAAoB,IAAI,IAAI,CAAC;CAEzD,gBAAgB;EACd,IAAI,CAAC,WAAW,CAAC,MAAM,eAAe;EAEtC,MAAM,MAAM,KAAK,IAAI;EAErB,SAAS,SAAS,YAAY;GAC5B,IAAI,oBAAoB,QAAQ,IAAI,QAAQ,SAAS,GACnD;GAIF,IAAI,MADa,gBAAgB,QAAQ,QAAQ,cAC5B,eACnB;GAGF,MAAM,WAAW,eAAe,QAAQ;GACxC,MAAM,UAAU,SAAS,KAAK,MAAM,KAAK,OAAO,IAAI,SAAS,MAAM;GAEnE,IAAI;IACF,MAAM,QAAQ,OAAO;GACvB,SAAS,OAAO;IACd,QAAQ,KAAK,kCAAkC,WAAW,KAAK;GACjE;GAEA,gBAAgB,QAAQ,QAAQ,cAAc;GAC9C,oBAAoB,QAAQ,IAAI,QAAQ,SAAS;GAEjD,qBAAqB,QAAQ,SAAS;EACxC,CAAC;EAED,IAAI,oBAAoB,QAAQ,OAAO,KAAM;GAC3C,MAAM,aAAa,MAAM,KAAK,oBAAoB,OAAO,CAAC,CAAC,MACxD,GAAG,MAAM,IAAI,CAChB;GACA,oBAAoB,UAAU,IAAI,IAAI,WAAW,MAAM,GAAG,GAAG,CAAC;EAChE;CACF,GAAG;EAAC;EAAU;EAAS;EAAO;CAAkB,CAAC;CAEjD,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FPSMonitor.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/hud/FPSMonitor.tsx"],"sourcesContent":["/**\n * FPSMonitor - Real-time performance monitoring component\n *\n * Displays current FPS (frames per second) and provides performance warnings\n * when frame rate drops below target thresholds. Helps verify 60fps target\n * and identify performance bottlenecks during combat.\n *\n * @module components/combat/components/FPSMonitor\n * @category Performance Monitoring\n * @korean FPS모니터\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { Html } from \"@react-three/drei\";\nimport React, { useRef, useState, useCallback, useEffect, useMemo } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\n\nexport interface FPSMonitorProps {\n /** Whether to show the FPS monitor */\n readonly enabled?: boolean;\n /** FPS threshold for warning (default: 50) */\n readonly warningThreshold?: number;\n /** FPS threshold for critical warning (default: 30) */\n readonly criticalThreshold?: number;\n /** Callback when FPS drops below threshold */\n readonly onFPSDrop?: (fps: number) => void;\n /** Position from top in pixels (default: 10) */\n readonly top?: number;\n /** Position from right in pixels (default: 10) */\n readonly right?: number;\n}\n\n/**\n * FPSMonitor Component\n *\n * Monitors and displays real-time FPS during combat. Uses Three.js useFrame\n * to calculate frames per second and provides visual feedback:\n * - Green: 60+ fps (excellent)\n * - Yellow: 50-59 fps (warning)\n * - Orange: 30-49 fps (poor)\n * - Red: <30 fps (critical)\n *\n * @example\n * ```tsx\n * <FPSMonitor\n * enabled={showPerformanceStats}\n * warningThreshold={50}\n * onFPSDrop={(fps) => console.warn(`Performance drop: ${fps} fps`)}\n * />\n * ```\n */\nexport const FPSMonitor: React.FC<FPSMonitorProps> = ({\n enabled = true,\n warningThreshold = 50,\n criticalThreshold = 30,\n onFPSDrop,\n top = 10,\n right = 10,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"small\", isMobile: false });\n const [fps, setFps] = useState(60);\n const frameCountRef = useRef(0);\n const lastTimeRef = useRef(0);\n const lastWarningTimeRef = useRef(0);\n const initializedRef = useRef(false);\n \n useEffect(() => {\n lastTimeRef.current = performance.now();\n initializedRef.current = true;\n }, []);\n\n const updateFPS = useCallback(() => {\n if (!initializedRef.current) return;\n \n frameCountRef.current++;\n const currentTime = performance.now();\n const deltaTime = currentTime - lastTimeRef.current;\n\n if (deltaTime >= 1000) {\n const currentFps = Math.round((frameCountRef.current * 1000) / deltaTime);\n setFps(currentFps);\n \n if (onFPSDrop && currentFps < warningThreshold) {\n const timeSinceLastWarning = currentTime - lastWarningTimeRef.current;\n if (timeSinceLastWarning >= 3000) {\n onFPSDrop(currentFps);\n lastWarningTimeRef.current = currentTime;\n }\n }\n\n frameCountRef.current = 0;\n lastTimeRef.current = currentTime;\n }\n }, [onFPSDrop, warningThreshold]);\n\n useFrame(() => {\n if (enabled) {\n updateFPS();\n }\n });\n\n const colorHex = useMemo(() => {\n let color: number;\n if (fps >= 60) {\n color = theme.colors.ACCENT_GOLD; // Excellent\n } else if (fps >= warningThreshold) {\n color = 0xffff00; // Warning (yellow)\n } else if (fps >= criticalThreshold) {\n color = 0xff8800; // Poor (orange)\n } else {\n color = theme.colors.PRIMARY_RED; // Critical (red)\n }\n return `#${color.toString(16).padStart(6, \"0\")}`;\n }, [fps, warningThreshold, criticalThreshold, theme.colors.ACCENT_GOLD, theme.colors.PRIMARY_RED]);\n\n const getStatus = () => {\n if (fps >= 60) return \"우수 | Excellent\";\n if (fps >= warningThreshold) return \"경고 | Warning\";\n if (fps >= criticalThreshold) return \"저하 | Poor\";\n return \"심각 | Critical\";\n };\n\n if (!enabled) {\n return null;\n }\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"fps-monitor\"\n style={{\n position: \"absolute\",\n top: `${top}px`,\n right: `${right}px`,\n padding: \"8px 12px\",\n background: \"rgba(0, 0, 0, 0.8)\",\n border: `2px solid ${colorHex}`,\n borderRadius: \"4px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: \"12px\",\n color: colorHex,\n pointerEvents: \"none\",\n zIndex: 9999,\n }}\n >\n <div style={{ fontWeight: \"bold\", marginBottom: \"2px\" }}>\n FPS: {fps}\n </div>\n <div style={{ fontSize: \"10px\", opacity: 0.9 }}>\n {getStatus()}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default FPSMonitor;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,cAAyC,EACpD,UAAU,MACV,mBAAmB,IACnB,oBAAoB,IACpB,WACA,MAAM,IACN,QAAQ,SACJ;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAS,UAAU;CAAM,CAAC;CACnF,MAAM,CAAC,KAAK,UAAU,SAAS,EAAE;CACjC,MAAM,gBAAgB,OAAO,CAAC;CAC9B,MAAM,cAAc,OAAO,CAAC;CAC5B,MAAM,qBAAqB,OAAO,CAAC;CACnC,MAAM,iBAAiB,OAAO,KAAK;CAEnC,gBAAgB;EACd,YAAY,UAAU,YAAY,IAAI;EACtC,eAAe,UAAU;CAC3B,GAAG,CAAC,CAAC;CAEL,MAAM,YAAY,kBAAkB;EAClC,IAAI,CAAC,eAAe,SAAS;EAE7B,cAAc;EACd,MAAM,cAAc,YAAY,IAAI;EACpC,MAAM,YAAY,cAAc,YAAY;EAE5C,IAAI,aAAa,KAAM;GACrB,MAAM,aAAa,KAAK,MAAO,cAAc,UAAU,MAAQ,SAAS;GACxE,OAAO,UAAU;GAEjB,IAAI,aAAa,aAAa;QACC,cAAc,mBAAmB,WAClC,KAAM;KAChC,UAAU,UAAU;KACpB,mBAAmB,UAAU;IAC/B;;GAGF,cAAc,UAAU;GACxB,YAAY,UAAU;EACxB;CACF,GAAG,CAAC,WAAW,gBAAgB,CAAC;CAEhC,eAAe;EACb,IAAI,SACF,UAAU;CAEd,CAAC;CAED,MAAM,WAAW,cAAc;EAC7B,IAAI;EACJ,IAAI,OAAO,IACT,QAAQ,MAAM,OAAO;OAChB,IAAI,OAAO,kBAChB,QAAQ;OACH,IAAI,OAAO,mBAChB,QAAQ;OAER,QAAQ,MAAM,OAAO;EAEvB,OAAO,IAAI,MAAM,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"FPSMonitor.js","names":[],"sources":["../../../../../../src/components/screens/combat/components/hud/FPSMonitor.tsx"],"sourcesContent":["/**\n * FPSMonitor - Real-time performance monitoring component\n *\n * Displays current FPS (frames per second) and provides performance warnings\n * when frame rate drops below target thresholds. Helps verify 60fps target\n * and identify performance bottlenecks during combat.\n *\n * @module components/combat/components/FPSMonitor\n * @category Performance Monitoring\n * @korean FPS모니터\n */\n\nimport { useFrame } from \"@react-three/fiber\";\nimport { Html } from \"@react-three/drei\";\nimport React, { useRef, useState, useCallback, useEffect, useMemo } from \"react\";\nimport { useKoreanTheme } from \"../../../../shared/base/useKoreanTheme\";\n\nexport interface FPSMonitorProps {\n /** Whether to show the FPS monitor */\n readonly enabled?: boolean;\n /** FPS threshold for warning (default: 50) */\n readonly warningThreshold?: number;\n /** FPS threshold for critical warning (default: 30) */\n readonly criticalThreshold?: number;\n /** Callback when FPS drops below threshold */\n readonly onFPSDrop?: (fps: number) => void;\n /** Position from top in pixels (default: 10) */\n readonly top?: number;\n /** Position from right in pixels (default: 10) */\n readonly right?: number;\n}\n\n/**\n * FPSMonitor Component\n *\n * Monitors and displays real-time FPS during combat. Uses Three.js useFrame\n * to calculate frames per second and provides visual feedback:\n * - Green: 60+ fps (excellent)\n * - Yellow: 50-59 fps (warning)\n * - Orange: 30-49 fps (poor)\n * - Red: <30 fps (critical)\n *\n * @example\n * ```tsx\n * <FPSMonitor\n * enabled={showPerformanceStats}\n * warningThreshold={50}\n * onFPSDrop={(fps) => console.warn(`Performance drop: ${fps} fps`)}\n * />\n * ```\n */\nexport const FPSMonitor: React.FC<FPSMonitorProps> = ({\n enabled = true,\n warningThreshold = 50,\n criticalThreshold = 30,\n onFPSDrop,\n top = 10,\n right = 10,\n}) => {\n const theme = useKoreanTheme({ variant: \"primary\", size: \"small\", isMobile: false });\n const [fps, setFps] = useState(60);\n const frameCountRef = useRef(0);\n const lastTimeRef = useRef(0);\n const lastWarningTimeRef = useRef(0);\n const initializedRef = useRef(false);\n \n useEffect(() => {\n lastTimeRef.current = performance.now();\n initializedRef.current = true;\n }, []);\n\n const updateFPS = useCallback(() => {\n if (!initializedRef.current) return;\n \n frameCountRef.current++;\n const currentTime = performance.now();\n const deltaTime = currentTime - lastTimeRef.current;\n\n if (deltaTime >= 1000) {\n const currentFps = Math.round((frameCountRef.current * 1000) / deltaTime);\n setFps(currentFps);\n \n if (onFPSDrop && currentFps < warningThreshold) {\n const timeSinceLastWarning = currentTime - lastWarningTimeRef.current;\n if (timeSinceLastWarning >= 3000) {\n onFPSDrop(currentFps);\n lastWarningTimeRef.current = currentTime;\n }\n }\n\n frameCountRef.current = 0;\n lastTimeRef.current = currentTime;\n }\n }, [onFPSDrop, warningThreshold]);\n\n useFrame(() => {\n if (enabled) {\n updateFPS();\n }\n });\n\n const colorHex = useMemo(() => {\n let color: number;\n if (fps >= 60) {\n color = theme.colors.ACCENT_GOLD; // Excellent\n } else if (fps >= warningThreshold) {\n color = 0xffff00; // Warning (yellow)\n } else if (fps >= criticalThreshold) {\n color = 0xff8800; // Poor (orange)\n } else {\n color = theme.colors.PRIMARY_RED; // Critical (red)\n }\n return `#${color.toString(16).padStart(6, \"0\")}`;\n }, [fps, warningThreshold, criticalThreshold, theme.colors.ACCENT_GOLD, theme.colors.PRIMARY_RED]);\n\n const getStatus = () => {\n if (fps >= 60) return \"우수 | Excellent\";\n if (fps >= warningThreshold) return \"경고 | Warning\";\n if (fps >= criticalThreshold) return \"저하 | Poor\";\n return \"심각 | Critical\";\n };\n\n if (!enabled) {\n return null;\n }\n\n return (\n <Html fullscreen>\n <div\n data-testid=\"fps-monitor\"\n style={{\n position: \"absolute\",\n top: `${top}px`,\n right: `${right}px`,\n padding: \"8px 12px\",\n background: \"rgba(0, 0, 0, 0.8)\",\n border: `2px solid ${colorHex}`,\n borderRadius: \"4px\",\n fontFamily: theme.fontFamily.KOREAN,\n fontSize: \"12px\",\n color: colorHex,\n pointerEvents: \"none\",\n zIndex: 9999,\n }}\n >\n <div style={{ fontWeight: \"bold\", marginBottom: \"2px\" }}>\n FPS: {fps}\n </div>\n <div style={{ fontSize: \"10px\", opacity: 0.9 }}>\n {getStatus()}\n </div>\n </div>\n </Html>\n );\n};\n\nexport default FPSMonitor;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAa,cAAyC,EACpD,UAAU,MACV,mBAAmB,IACnB,oBAAoB,IACpB,WACA,MAAM,IACN,QAAQ,SACJ;CACJ,MAAM,QAAQ,eAAe;EAAE,SAAS;EAAW,MAAM;EAAS,UAAU;CAAM,CAAC;CACnF,MAAM,CAAC,KAAK,UAAU,SAAS,EAAE;CACjC,MAAM,gBAAgB,OAAO,CAAC;CAC9B,MAAM,cAAc,OAAO,CAAC;CAC5B,MAAM,qBAAqB,OAAO,CAAC;CACnC,MAAM,iBAAiB,OAAO,KAAK;CAEnC,gBAAgB;EACd,YAAY,UAAU,YAAY,IAAI;EACtC,eAAe,UAAU;CAC3B,GAAG,CAAC,CAAC;CAEL,MAAM,YAAY,kBAAkB;EAClC,IAAI,CAAC,eAAe,SAAS;EAE7B,cAAc;EACd,MAAM,cAAc,YAAY,IAAI;EACpC,MAAM,YAAY,cAAc,YAAY;EAE5C,IAAI,aAAa,KAAM;GACrB,MAAM,aAAa,KAAK,MAAO,cAAc,UAAU,MAAQ,SAAS;GACxE,OAAO,UAAU;GAEjB,IAAI,aAAa,aAAa;QACC,cAAc,mBAAmB,WAClC,KAAM;KAChC,UAAU,UAAU;KACpB,mBAAmB,UAAU;IAC/B;;GAGF,cAAc,UAAU;GACxB,YAAY,UAAU;EACxB;CACF,GAAG,CAAC,WAAW,gBAAgB,CAAC;CAEhC,eAAe;EACb,IAAI,SACF,UAAU;CAEd,CAAC;CAED,MAAM,WAAW,cAAc;EAC7B,IAAI;EACJ,IAAI,OAAO,IACT,QAAQ,MAAM,OAAO;OAChB,IAAI,OAAO,kBAChB,QAAQ;OACH,IAAI,OAAO,mBAChB,QAAQ;OAER,QAAQ,MAAM,OAAO;EAEvB,OAAO,IAAI,MAAM,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG;CAC/C,GAAG;EAAC;EAAK;EAAkB;EAAmB,MAAM,OAAO;EAAa,MAAM,OAAO;CAAW,CAAC;CAEjG,MAAM,kBAAkB;EACtB,IAAI,OAAO,IAAI,OAAO;EACtB,IAAI,OAAO,kBAAkB,OAAO;EACpC,IAAI,OAAO,mBAAmB,OAAO;EACrC,OAAO;CACT;CAEA,IAAI,CAAC,SACH,OAAO;CAGT,OACE,oBAAC,MAAD;EAAM,YAAA;YACJ,qBAAC,OAAD;GACE,eAAY;GACZ,OAAO;IACL,UAAU;IACV,KAAK,GAAG,IAAI;IACZ,OAAO,GAAG,MAAM;IAChB,SAAS;IACT,YAAY;IACZ,QAAQ,aAAa;IACrB,cAAc;IACd,YAAY,MAAM,WAAW;IAC7B,UAAU;IACV,OAAO;IACP,eAAe;IACf,QAAQ;GACV;aAfF,CAiBE,qBAAC,OAAD;IAAK,OAAO;KAAE,YAAY;KAAQ,cAAc;IAAM;cAAtD,CAAyD,SACjD,GACH;OACL,oBAAC,OAAD;IAAK,OAAO;KAAE,UAAU;KAAQ,SAAS;IAAI;cAC1C,UAAU;GACR,CAAA,CACF;;CACD,CAAA;AAEV"}
|