blacktrigram 0.7.98 → 0.7.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/App2.js.map +1 -1
- package/lib/audio/AudioManager.js +2 -4
- package/lib/audio/AudioManager.js.map +1 -1
- package/lib/components/screens/combat/CombatScreen3D.js +8 -9
- package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
- package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
- package/lib/components/screens/combat/components/controls/ControlsGuide.js.map +1 -1
- package/lib/components/screens/combat/components/controls/KeyboardHints.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
- package/lib/components/screens/combat/components/controls/PauseMenuButton.js.map +1 -1
- package/lib/components/screens/combat/components/controls/QuickSettings.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodDecals3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js +1 -1
- package/lib/components/screens/combat/components/effects/BloodLossOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodParticles3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js +2 -1
- package/lib/components/screens/combat/components/effects/BloodViscosity3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js +3 -1
- package/lib/components/screens/combat/components/effects/InternalDamage3D.js.map +1 -1
- package/lib/components/screens/combat/components/effects/TraumaOverlay3D.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/MatchCountdown.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +16 -15
- package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundDisplayStatus.js.map +1 -1
- package/lib/components/screens/combat/components/feedback/RoundStartAnnouncementOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatBottomHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatLeftHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatPortraitStatusStrip.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatRightHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/CombatTopHUD.js.map +1 -1
- package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
- package/lib/components/screens/combat/components/hud/FPSMonitor.js.map +1 -1
- package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
- package/lib/components/screens/combat/components/indicators/TechniqueNameDisplay.js.map +1 -1
- package/lib/components/screens/combat/hooks/useAICombat.js +0 -1
- package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js +7 -6
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js +3 -1
- package/lib/components/screens/combat/hooks/useCombatLayout.js.map +1 -1
- package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
- package/lib/components/screens/controls/components/ControlBindingsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js +7 -6
- package/lib/components/screens/controls/components/ControlCategoryTabsOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/GamepadVisualization3D.js.map +1 -1
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +34 -30
- package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
- package/lib/components/screens/controls/components/Key3D.js.map +1 -1
- package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
- package/lib/components/screens/endscreen/EndScreen3D.js +2 -2
- package/lib/components/screens/endscreen/EndScreen3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js +1 -1
- package/lib/components/screens/endscreen/components/DefeatAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/MatchStatisticsDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/NavigationButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceBreakdownOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/PerformanceRatingOverlayHtml.js.map +1 -1
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js +2 -2
- package/lib/components/screens/endscreen/components/VictoryAnimation3D.js.map +1 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js +2 -1
- package/lib/components/screens/endscreen/components/WinnerDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +2 -4
- package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
- package/lib/components/screens/intro/components/AbilityListOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardGridOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeCardOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/ArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/EnhancedArchetypeDisplayOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/MenuSectionOverlayHtml.js.map +1 -1
- package/lib/components/screens/intro/components/StatBarOverlayHtml.js.map +1 -1
- package/lib/components/screens/philosophy/PhilosophyScreen3D.js.map +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js +10 -9
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js +14 -13
- package/lib/components/screens/training/components/AnatomyControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/AnatomyOverlay3D.js.map +1 -1
- package/lib/components/screens/training/components/FootPlacementMarkers3D.js.map +1 -1
- package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingButtonsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingControlsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingDummy3D.js.map +1 -1
- package/lib/components/screens/training/components/TrainingFeedbackOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingModeSelectorOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/TrainingStatsOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointMarker3D.js +8 -7
- package/lib/components/screens/training/components/VitalPointMarker3D.js.map +1 -1
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js +15 -13
- package/lib/components/screens/training/components/VitalPointTrainingOverlayHtml.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingBottomHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingLeftHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js +3 -2
- package/lib/components/screens/training/components/hud/TrainingRightHUD.js.map +1 -1
- package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js +1 -2
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js +3 -1
- package/lib/components/screens/training/hooks/useTrainingLayout.js.map +1 -1
- package/lib/components/shared/base/BaseButton.js.map +1 -1
- package/lib/components/shared/base/BaseButtonOverlayHtml.js +59 -53
- package/lib/components/shared/base/BaseButtonOverlayHtml.js.map +1 -1
- package/lib/components/shared/base/BasePanel.js +19 -18
- package/lib/components/shared/base/BasePanel.js.map +1 -1
- package/lib/components/shared/base/BaseText.js.map +1 -1
- package/lib/components/shared/mobile/ActionButtons.js.map +1 -1
- package/lib/components/shared/mobile/GestureRecognizerPure.js +22 -18
- package/lib/components/shared/mobile/GestureRecognizerPure.js.map +1 -1
- package/lib/components/shared/mobile/MobileControlsPure.js.map +1 -1
- package/lib/components/shared/mobile/StanceWheelPure.js.map +1 -1
- package/lib/components/shared/mobile/VirtualDPad.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js +4 -5
- package/lib/components/shared/three/anatomy/BoneAttachedMuscles.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneClothing.js +29 -24
- package/lib/components/shared/three/anatomy/BoneClothing.js.map +1 -1
- package/lib/components/shared/three/anatomy/BoneRenderer.js.map +1 -1
- package/lib/components/shared/three/anatomy/Face3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Foot3D.js.map +1 -1
- package/lib/components/shared/three/anatomy/Hand3D.js.map +1 -1
- package/lib/components/shared/three/effects/ActionFeedback.js +3 -2
- package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
- package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
- package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
- package/lib/components/shared/three/effects/HitEffects3D.js +3 -2
- package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
- package/lib/components/shared/three/effects/PlayerStateIndicators.js.map +1 -1
- package/lib/components/shared/three/effects/StanceSymbol3D.js.map +1 -1
- package/lib/components/shared/three/effects/StanceTransitionEffect.js.map +1 -1
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js +6 -5
- package/lib/components/shared/three/effects/VitalPointMarkers3D.js.map +1 -1
- package/lib/components/shared/three/indicators/GuardIndicator.js.map +1 -1
- package/lib/components/shared/three/indicators/StanceChangeIndicator.js.map +1 -1
- package/lib/components/shared/three/models/Player3DWithTransitions.js.map +1 -1
- package/lib/components/shared/three/models/SkeletalPlayer3D.js.map +1 -1
- package/lib/components/shared/three/optimization/AdaptiveQuality.js.map +1 -1
- package/lib/components/shared/three/scene/AtmosphericParticles3D.js.map +1 -1
- package/lib/components/shared/three/scene/CombatArena3D.js.map +1 -1
- package/lib/components/shared/three/scene/KoreanSignage3D.js.map +1 -1
- package/lib/components/shared/three/ui/ArchetypeCard.js.map +1 -1
- package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -1
- package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
- package/lib/components/shared/three/ui/CombatReadinessBar.js.map +1 -1
- package/lib/components/shared/three/ui/ComboCounter.js.map +1 -1
- package/lib/components/shared/three/ui/HealthBar.js.map +1 -1
- package/lib/components/shared/three/ui/MenuList.js.map +1 -1
- package/lib/components/shared/three/ui/PlayerHUD.js +2 -1
- package/lib/components/shared/three/ui/PlayerHUD.js.map +1 -1
- package/lib/components/shared/three/ui/ProgressBar.js.map +1 -1
- package/lib/components/shared/three/ui/SpeedIndicatorHUD.js.map +1 -1
- package/lib/components/shared/three/ui/StaminaBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueBar.js +25 -24
- package/lib/components/shared/three/ui/TechniqueBar.js.map +1 -1
- package/lib/components/shared/three/ui/TechniqueCard.js +1 -3
- package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
- package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
- package/lib/components/shared/ui/BackButton.js +2 -1
- package/lib/components/shared/ui/BackButton.js.map +1 -1
- package/lib/components/shared/ui/BaseHUDContainer.js +69 -68
- package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
- package/lib/components/shared/ui/CombatTimer.js.map +1 -1
- package/lib/components/shared/ui/ErrorModal.js.map +1 -1
- package/lib/components/shared/ui/LoadingState.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/components/shared/ui/SplashScreen.js.map +1 -1
- package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
- package/lib/components/shared/ui/VolumeControl.js.map +1 -1
- package/lib/components/shared/ui/shared/ConfirmDialog.js.map +1 -1
- package/lib/components/ui/combat/BalanceIndicatorOverlayHtml.js.map +1 -1
- package/lib/hooks/useKeyboardControls.js +0 -1
- package/lib/hooks/useKeyboardControls.js.map +1 -1
- package/lib/hooks/useSkeletalAnimation.js +12 -3
- package/lib/hooks/useSkeletalAnimation.js.map +1 -1
- package/lib/systems/CombatSystem.js +6 -2
- package/lib/systems/CombatSystem.js.map +1 -1
- package/lib/systems/PlayerEffectManager.js +0 -1
- package/lib/systems/PlayerEffectManager.js.map +1 -1
- package/lib/systems/ai/AdaptiveDifficulty.js.map +1 -1
- package/lib/systems/ai/DecisionTree.js +10 -21
- package/lib/systems/ai/DecisionTree.js.map +1 -1
- package/lib/systems/animation/core/AnimationStateMachine.js.map +1 -1
- package/lib/systems/animation/systems/AdvancedJointMovements.js +1 -3
- package/lib/systems/animation/systems/AdvancedJointMovements.js.map +1 -1
- package/lib/systems/animation/systems/MuscleActivation.js +3 -4
- package/lib/systems/animation/systems/MuscleActivation.js.map +1 -1
- package/lib/systems/bodypart/MovementPenaltySystem.js.map +1 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js +2 -1
- package/lib/systems/bodypart/PlayerInjuryTrackingManager.js.map +1 -1
- package/lib/systems/breathing/BreathingDisruptionSystem.js.map +1 -1
- package/lib/systems/combat/BalanceSystem.js.map +1 -1
- package/lib/systems/combat/CombatStateSystem.js.map +1 -1
- package/lib/systems/combat/ConsciousnessSystem.js.map +1 -1
- package/lib/systems/combat/GrappleSystem.js +3 -9
- package/lib/systems/combat/GrappleSystem.js.map +1 -1
- package/lib/systems/combat/LimbExposureSystem.js.map +1 -1
- package/lib/systems/combat/PainResponseSystem.js.map +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js +1 -1
- package/lib/systems/movement/InjuryMovementModifier.js.map +1 -1
- package/lib/systems/physics/CoordinateMapper.js.map +1 -1
- package/lib/systems/vitalpoint/DamageCalculator.js +8 -5
- package/lib/systems/vitalpoint/DamageCalculator.js.map +1 -1
- package/lib/types/facial.js.map +1 -1
- package/lib/utils/accessibility.js +0 -1
- package/lib/utils/accessibility.js.map +1 -1
- package/lib/utils/effectUtils.js +1 -3
- package/lib/utils/effectUtils.js.map +1 -1
- package/lib/utils/inputSystem.js +0 -2
- package/lib/utils/inputSystem.js.map +1 -1
- package/lib/utils/koreanThemeHelpers.js +6 -3
- package/lib/utils/koreanThemeHelpers.js.map +1 -1
- package/lib/utils/performance/PerformanceOverlay3D.js.map +1 -1
- package/lib/utils/playerUtils.js +4 -2
- package/lib/utils/playerUtils.js.map +1 -1
- package/package.json +14 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InjuryMovementModifier.js","names":[],"sources":["../../../src/systems/movement/InjuryMovementModifier.ts"],"sourcesContent":["/**\n * Injury-Based Movement Modifier System\n * \n * **Korean**: 손상 기반 이동 시스템 (Injury-Based Movement System)\n * \n * Dynamically calculates movement speed modifiers based on leg injuries, torso\n * damage, stance configuration, and pain levels. Part of the 12 combat realism\n * systems for authentic Korean martial arts gameplay.\n * \n * ## Movement Speed Calculation\n * \n * Base speed is modified by multiple factors:\n * - **Leg Injuries**: 0-100% penalty based on health\n * - **Torso Injuries**: 0-30% minor penalty\n * - **Both Legs Injured**: Additional 20% cumulative penalty\n * - **Stance Modifiers**: -20% (defensive) to +25% (offensive)\n * - **Pain Overload**: -15% when pain ≥ 80\n * \n * ## Injury Severity Thresholds\n * \n * | Leg Health | State | Speed Penalty |\n * |-----------|-------|---------------|\n * | 70-100% | Normal | 0% |\n * | 30-69% | Limping | 0-40% |\n * | 10-29% | Severe Limp | 40-80% |\n * | 0-9% | Critical | 80-100% |\n * \n * @module systems/movement/InjuryMovementModifier\n * @category Movement System\n * @korean 손상기반이동\n */\n\nimport type { TrigramStance } from \"@/types/common\";\nimport type { BodyPartHealth } from \"../bodypart/types\";\nimport { STANCE_SPEED_MODIFIERS } from \"../physics/MovementPhysics\";\n\n/**\n * Configuration for injury-based movement calculations.\n * \n * **Korean**: 손상 이동 설정\n * \n * @category Movement System\n */\nexport interface InjuryMovementConfig {\n /** Leg health thresholds for injury states */\n readonly legThresholds: {\n readonly normal: number; // 70% - no penalty\n readonly limping: number; // 30% - start of severe penalty\n readonly critical: number; // 10% - near-maximum penalty\n };\n /** Maximum torso injury penalty (0.3 = 30%) */\n readonly maxTorsoPenalty: number;\n /** Both legs injured cumulative penalty multiplier (0.8 = -20%) */\n readonly bothLegsInjuredMultiplier: number;\n /** Pain overload threshold (80) */\n readonly painOverloadThreshold: number;\n /** Pain overload penalty multiplier (0.85 = -15%) */\n readonly painOverloadMultiplier: number;\n /** Minimum speed multiplier (0.1 = 10% of base speed) */\n readonly minSpeedMultiplier: number;\n}\n\n/**\n * Deep partial type for InjuryMovementConfig to support partial overrides.\n * Allows callers to override individual threshold values without providing all.\n * \n * @category Movement System\n */\nexport type PartialInjuryMovementConfig = {\n readonly legThresholds?: Partial<InjuryMovementConfig[\"legThresholds\"]>;\n readonly maxTorsoPenalty?: number;\n readonly bothLegsInjuredMultiplier?: number;\n readonly painOverloadThreshold?: number;\n readonly painOverloadMultiplier?: number;\n readonly minSpeedMultiplier?: number;\n};\n\n/**\n * Default configuration matching acceptance criteria.\n */\nexport const DEFAULT_INJURY_MOVEMENT_CONFIG: InjuryMovementConfig = {\n legThresholds: {\n normal: 70,\n limping: 30,\n critical: 10,\n },\n maxTorsoPenalty: 0.3,\n bothLegsInjuredMultiplier: 0.8,\n painOverloadThreshold: 80,\n painOverloadMultiplier: 0.85,\n minSpeedMultiplier: 0.1,\n} as const;\n\n/**\n * Result of injury-based movement calculation.\n * \n * **Korean**: 손상 이동 결과\n * \n * @category Movement System\n */\nexport interface InjuryMovementResult {\n /** Final speed in units/second */\n readonly finalSpeed: number;\n /** Combined speed multiplier (0.1-1.0+) */\n readonly speedMultiplier: number;\n /** Breakdown of penalty sources */\n readonly penalties: {\n readonly leftLegPenalty: number;\n readonly rightLegPenalty: number;\n readonly torsoPenalty: number;\n readonly bothLegsInjured: boolean;\n readonly stanceModifier: number;\n readonly painOverload: boolean;\n };\n /** Whether player is limping */\n readonly isLimping: boolean;\n /** Whether player has severe limp */\n readonly isSevereLimp: boolean;\n /** Bilingual status text */\n readonly statusText: {\n readonly korean: string;\n readonly english: string;\n };\n}\n\n/**\n * Injury-Based Movement Modifier System.\n * \n * **Korean**: 손상 기반 이동 수정자 시스템\n * \n * Calculates dynamic movement speed based on injuries, stance, and pain.\n * Integrates with MovementPhysics to apply realistic combat trauma effects.\n * \n * @example\n * ```typescript\n * const modifier = new InjuryMovementModifier();\n * \n * const result = modifier.calculateMovementSpeed(\n * 5.0, // base speed\n * bodyPartHealth, // current health\n * TrigramStance.GEON, // current stance\n * 65 // pain level\n * );\n * \n * console.log(`Speed: ${result.finalSpeed} m/s`);\n * console.log(`Status: ${result.statusText.korean} | ${result.statusText.english}`);\n * ```\n * \n * @category Movement System\n * @korean 손상기반이동수정자\n */\nexport class InjuryMovementModifier {\n private readonly config: InjuryMovementConfig;\n\n /**\n * Threshold for determining if both legs are significantly injured.\n * Uses epsilon to avoid floating-point rounding issues at boundary.\n * \n * @private\n */\n private static readonly BOTH_LEGS_THRESHOLD = 0.3 + 1e-10;\n\n /**\n * Creates a new InjuryMovementModifier with optional configuration.\n * \n * @param config - Optional configuration overrides (supports partial threshold overrides)\n */\n constructor(config?: PartialInjuryMovementConfig) {\n // Deep merge leg thresholds to prevent partial overrides from breaking the config.\n // Use ?? to avoid undefined values overwriting defaults when exactOptionalPropertyTypes is disabled.\n const mergedLegThresholds = {\n normal:\n config?.legThresholds?.normal ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.legThresholds.normal,\n limping:\n config?.legThresholds?.limping ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.legThresholds.limping,\n critical:\n config?.legThresholds?.critical ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.legThresholds.critical,\n };\n\n const mergedConfig: InjuryMovementConfig = {\n legThresholds: mergedLegThresholds,\n maxTorsoPenalty:\n config?.maxTorsoPenalty ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.maxTorsoPenalty,\n bothLegsInjuredMultiplier:\n config?.bothLegsInjuredMultiplier ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.bothLegsInjuredMultiplier,\n painOverloadThreshold:\n config?.painOverloadThreshold ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.painOverloadThreshold,\n painOverloadMultiplier:\n config?.painOverloadMultiplier ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.painOverloadMultiplier,\n minSpeedMultiplier:\n config?.minSpeedMultiplier ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.minSpeedMultiplier,\n };\n\n // Development-mode sanity check: ensure thresholds are finite and ordered\n if (process.env.NODE_ENV !== \"production\") {\n const { normal, limping, critical } = mergedConfig.legThresholds;\n const allFinite =\n Number.isFinite(normal) &&\n Number.isFinite(limping) &&\n Number.isFinite(critical);\n const ordered = normal >= limping && limping >= critical;\n\n if (!allFinite || !ordered) {\n // Non-throwing warning to avoid breaking existing behavior\n console.warn(\n \"[InjuryMovementModifier] Invalid legThresholds configuration detected:\",\n mergedConfig.legThresholds\n );\n }\n }\n\n this.config = mergedConfig;\n }\n\n /**\n * Calculate modified movement speed based on all injury factors.\n * \n * **Korean**: 이동 속도 계산\n * \n * Applies penalties from:\n * 1. Leg injuries (primary factor)\n * 2. Torso injuries (minor factor)\n * 3. Both legs injured (cumulative penalty)\n * 4. Stance modifiers\n * 5. Pain overload\n * \n * @param baseSpeed - Base movement speed (m/s)\n * @param bodyPartHealth - Current body part health values\n * @param stance - Current trigram stance\n * @param painLevel - Current pain level (0-100)\n * @returns Complete movement calculation result\n * \n * @korean 이동속도계산\n */\n public calculateMovementSpeed(\n baseSpeed: number,\n bodyPartHealth: BodyPartHealth,\n stance: TrigramStance,\n painLevel: number\n ): InjuryMovementResult {\n // Calculate individual leg penalties\n const leftLegPenalty = this.calculateLegPenalty(bodyPartHealth.legLeft);\n const rightLegPenalty = this.calculateLegPenalty(bodyPartHealth.legRight);\n\n // Use worst leg penalty as base\n const baseLegPenalty = Math.max(leftLegPenalty, rightLegPenalty);\n let speedMultiplier = 1.0 - baseLegPenalty;\n\n // Check if both legs are significantly injured using centralized method\n const bothLegsInjured = this.areBothLegsInjured(leftLegPenalty, rightLegPenalty);\n if (bothLegsInjured) {\n // Additional 20% penalty when both legs injured\n speedMultiplier *= this.config.bothLegsInjuredMultiplier;\n }\n\n // Calculate torso penalty (minor effect, 0-30% max)\n const avgTorsoHealth = (bodyPartHealth.torsoUpper + bodyPartHealth.torsoLower) / 2;\n // Clamp torso health to prevent negative penalties or exceeding max penalty\n const clampedAvgTorsoHealth = Math.min(100, Math.max(0, avgTorsoHealth));\n const torsoPenalty = ((100 - clampedAvgTorsoHealth) / 100) * this.config.maxTorsoPenalty;\n speedMultiplier *= (1 - torsoPenalty);\n\n // Apply stance modifier\n const stanceModifier = this.getStanceSpeedModifier(stance);\n speedMultiplier *= stanceModifier;\n\n // Apply pain overload penalty if applicable (>= threshold, not just >)\n const painOverload = painLevel >= this.config.painOverloadThreshold;\n if (painOverload) {\n speedMultiplier *= this.config.painOverloadMultiplier; // -15%\n }\n\n // Clamp to minimum speed (10% of base)\n speedMultiplier = Math.max(speedMultiplier, this.config.minSpeedMultiplier);\n\n // Calculate final speed\n const finalSpeed = baseSpeed * speedMultiplier;\n\n // Determine injury state for status text based on worst leg health\n // This ensures status matches the actual movement penalty (which uses worst leg)\n // Note: isLimping = true when leg health is in range [30-70) (limping range with 0-40% penalty)\n // isSevereLimp = true when leg health is < 30 (severe/critical with 40-100% penalty)\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n const isLimping =\n worstLegHealth < this.config.legThresholds.normal &&\n worstLegHealth >= this.config.legThresholds.limping;\n const isSevereLimp = worstLegHealth < this.config.legThresholds.limping;\n\n // Generate status text\n const statusText = this.generateStatusText(worstLegHealth, bothLegsInjured, painOverload);\n\n return {\n finalSpeed,\n speedMultiplier,\n penalties: {\n leftLegPenalty,\n rightLegPenalty,\n torsoPenalty,\n bothLegsInjured,\n stanceModifier,\n painOverload,\n },\n isLimping,\n isSevereLimp,\n statusText,\n };\n }\n\n /**\n * Calculate movement penalty from leg injury severity.\n * \n * **Korean**: 다리 부상 페널티 계산\n * \n * Progressive penalty scaling:\n * - 100-70%: No penalty (0%)\n * - 70-30%: Linear scaling (0-40%)\n * - 30-10%: Accelerated scaling (40-80%)\n * - 10-0%: Critical scaling (80-100%)\n * \n * @param legHealth - Leg health (0-100)\n * @returns Penalty factor (0.0-1.0), clamped to valid range\n * \n * @private\n */\n private calculateLegPenalty(legHealth: number): number {\n const { normal, limping, critical } = this.config.legThresholds;\n\n // Clamp input health to valid range to prevent runaway penalties\n const clampedHealth = Math.min(Math.max(legHealth, 0), 100);\n\n let penalty: number;\n\n if (clampedHealth >= normal) {\n // 70-100%: No penalty\n penalty = 0;\n } else if (clampedHealth >= limping) {\n // 30-70%: 0-40% penalty (linear)\n const healthRange = normal - limping;\n // Guard against division by zero if thresholds are misconfigured\n const healthFactor = healthRange > 0 \n ? (normal - clampedHealth) / healthRange \n : 0;\n penalty = healthFactor * 0.4;\n } else if (clampedHealth >= critical) {\n // 10-30%: 40-80% penalty (accelerated)\n const healthRange = limping - critical;\n // Guard against division by zero if thresholds are misconfigured\n const healthFactor = healthRange > 0 \n ? (limping - clampedHealth) / healthRange \n : 0;\n penalty = 0.4 + (healthFactor * 0.4);\n } else {\n // 0-10%: 80-100% penalty (critical)\n // Guard against division by zero if critical is 0\n const healthFactor = critical > 0 \n ? (critical - clampedHealth) / critical \n : 1;\n penalty = 0.8 + (healthFactor * 0.2);\n }\n\n // Ensure returned penalty is always within [0, 1]\n return Math.min(Math.max(penalty, 0), 1);\n }\n\n /**\n * Checks if both legs are significantly injured (>30% penalty each).\n * Uses epsilon-based comparison for floating-point robustness.\n * \n * @param leftLegPenalty - Penalty for left leg (0-1)\n * @param rightLegPenalty - Penalty for right leg (0-1)\n * @returns True if both legs are significantly injured\n * @private\n */\n private areBothLegsInjured(leftLegPenalty: number, rightLegPenalty: number): boolean {\n return leftLegPenalty >= InjuryMovementModifier.BOTH_LEGS_THRESHOLD && \n rightLegPenalty >= InjuryMovementModifier.BOTH_LEGS_THRESHOLD;\n }\n\n /**\n * Get stance-based speed modifier.\n * \n * **Korean**: 자세 속도 배수 가져오기\n * \n * @param stance - Current trigram stance\n * @returns Speed multiplier (0.8-1.25), defaults to 1.0 for unknown stances\n * \n */\n public getStanceSpeedModifier(stance: TrigramStance): number {\n const modifier = STANCE_SPEED_MODIFIERS[stance];\n\n // Defensive fallback for unknown stances (e.g., test/invalid values)\n if (modifier == null) {\n // Runtime-safe environment check (works in both Node and browser/Vite environments)\n if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"development\"\n ) {\n console.warn(\n `[InjuryMovementModifier] Unknown stance \"${String(\n stance\n )}\" received in getStanceSpeedModifier; defaulting to 1.0.`\n );\n }\n return 1.0;\n }\n\n return modifier;\n }\n\n /**\n * Generate bilingual status text based on injury state.\n * \n * **Korean**: 상태 텍스트 생성\n * \n * @param legHealthForStatus - Leg health percentage used for status determination (typically worst leg)\n * @param bothLegsInjured - Whether both legs are significantly injured\n * @param painOverload - Whether pain is over threshold\n * @returns Bilingual status text\n * \n * @private\n */\n private generateStatusText(\n legHealthForStatus: number,\n bothLegsInjured: boolean,\n painOverload: boolean\n ): { korean: string; english: string } {\n const { normal, limping, critical } = this.config.legThresholds;\n\n if (legHealthForStatus < critical) {\n const painText = painOverload ? \" | 고통 과부하\" : \"\";\n const painTextEn = painOverload ? \" | Pain Overload\" : \"\";\n const baseKorean = bothLegsInjured ? \"심각한 부상 | 양 다리\" : \"심각한 부상\";\n const baseEnglish = bothLegsInjured ? \"Critical Injury | Both Legs\" : \"Critical Injury\";\n return {\n korean: `${baseKorean}${painText}`,\n english: `${baseEnglish}${painTextEn}`,\n };\n } else if (legHealthForStatus < limping) {\n const painText = painOverload ? \" | 고통 과부하\" : \"\";\n const painTextEn = painOverload ? \" | Pain Overload\" : \"\";\n const baseKorean = bothLegsInjured ? \"중증 절름거림 | 양 다리\" : \"중증 절름거림\";\n const baseEnglish = bothLegsInjured ? \"Severe Limping | Both Legs\" : \"Severe Limping\";\n return {\n korean: `${baseKorean}${painText}`,\n english: `${baseEnglish}${painTextEn}`,\n };\n } else if (legHealthForStatus < normal) {\n const statusKrParts = [\"절름거림\"];\n const statusEnParts = [\"Limping\"];\n\n if (bothLegsInjured) {\n statusKrParts.push(\"양 다리\");\n statusEnParts.push(\"Both Legs\");\n }\n\n if (painOverload) {\n statusKrParts.push(\"고통 과부하\");\n statusEnParts.push(\"Pain Overload\");\n }\n\n return {\n korean: statusKrParts.join(\" | \"),\n english: statusEnParts.join(\" | \"),\n };\n } else if (painOverload) {\n return {\n korean: \"고통 과부하\",\n english: \"Pain Overload\",\n };\n } else {\n return {\n korean: \"정상\",\n english: \"Normal\",\n };\n }\n }\n\n /**\n * Check if player should display limping animation.\n * \n * **Korean**: 절름거림 확인\n * \n * Uses worst leg health to match movement penalty behavior.\n * \n * @param bodyPartHealth - Current body part health\n * @returns True if limping animation should play\n * \n */\n public shouldLimp(bodyPartHealth: BodyPartHealth): boolean {\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n return worstLegHealth < this.config.legThresholds.normal;\n }\n\n /**\n * Check if player has severe limp.\n * \n * **Korean**: 중증 절름거림 확인\n * \n * Uses worst leg health to match movement penalty behavior.\n * \n * @param bodyPartHealth - Current body part health\n * @returns True if severe limp (leg health < 30%)\n * \n */\n public hasSevereLimp(bodyPartHealth: BodyPartHealth): boolean {\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n return worstLegHealth < this.config.legThresholds.limping;\n }\n\n /**\n * Get current injury state description.\n * \n * **Korean**: 부상 상태 설명\n * \n * Uses worst leg health to match movement penalty behavior.\n * \n * @param bodyPartHealth - Current body part health\n * @returns Bilingual injury description\n * \n */\n public getInjuryDescription(bodyPartHealth: BodyPartHealth): {\n korean: string;\n english: string;\n } {\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n const leftLegPenalty = this.calculateLegPenalty(bodyPartHealth.legLeft);\n const rightLegPenalty = this.calculateLegPenalty(bodyPartHealth.legRight);\n const bothLegsInjured = this.areBothLegsInjured(leftLegPenalty, rightLegPenalty);\n\n return this.generateStatusText(worstLegHealth, bothLegsInjured, false);\n }\n}\n\n/**\n * Singleton instance for global access.\n * \n */\nexport const injuryMovementModifier = new InjuryMovementModifier();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InjuryMovementModifier.js","names":[],"sources":["../../../src/systems/movement/InjuryMovementModifier.ts"],"sourcesContent":["/**\n * Injury-Based Movement Modifier System\n * \n * **Korean**: 손상 기반 이동 시스템 (Injury-Based Movement System)\n * \n * Dynamically calculates movement speed modifiers based on leg injuries, torso\n * damage, stance configuration, and pain levels. Part of the 12 combat realism\n * systems for authentic Korean martial arts gameplay.\n * \n * ## Movement Speed Calculation\n * \n * Base speed is modified by multiple factors:\n * - **Leg Injuries**: 0-100% penalty based on health\n * - **Torso Injuries**: 0-30% minor penalty\n * - **Both Legs Injured**: Additional 20% cumulative penalty\n * - **Stance Modifiers**: -20% (defensive) to +25% (offensive)\n * - **Pain Overload**: -15% when pain ≥ 80\n * \n * ## Injury Severity Thresholds\n * \n * | Leg Health | State | Speed Penalty |\n * |-----------|-------|---------------|\n * | 70-100% | Normal | 0% |\n * | 30-69% | Limping | 0-40% |\n * | 10-29% | Severe Limp | 40-80% |\n * | 0-9% | Critical | 80-100% |\n * \n * @module systems/movement/InjuryMovementModifier\n * @category Movement System\n * @korean 손상기반이동\n */\n\nimport type { TrigramStance } from \"@/types/common\";\nimport type { BodyPartHealth } from \"../bodypart/types\";\nimport { STANCE_SPEED_MODIFIERS } from \"../physics/MovementPhysics\";\n\n/**\n * Configuration for injury-based movement calculations.\n * \n * **Korean**: 손상 이동 설정\n * \n * @category Movement System\n */\nexport interface InjuryMovementConfig {\n /** Leg health thresholds for injury states */\n readonly legThresholds: {\n readonly normal: number; // 70% - no penalty\n readonly limping: number; // 30% - start of severe penalty\n readonly critical: number; // 10% - near-maximum penalty\n };\n /** Maximum torso injury penalty (0.3 = 30%) */\n readonly maxTorsoPenalty: number;\n /** Both legs injured cumulative penalty multiplier (0.8 = -20%) */\n readonly bothLegsInjuredMultiplier: number;\n /** Pain overload threshold (80) */\n readonly painOverloadThreshold: number;\n /** Pain overload penalty multiplier (0.85 = -15%) */\n readonly painOverloadMultiplier: number;\n /** Minimum speed multiplier (0.1 = 10% of base speed) */\n readonly minSpeedMultiplier: number;\n}\n\n/**\n * Deep partial type for InjuryMovementConfig to support partial overrides.\n * Allows callers to override individual threshold values without providing all.\n * \n * @category Movement System\n */\nexport type PartialInjuryMovementConfig = {\n readonly legThresholds?: Partial<InjuryMovementConfig[\"legThresholds\"]>;\n readonly maxTorsoPenalty?: number;\n readonly bothLegsInjuredMultiplier?: number;\n readonly painOverloadThreshold?: number;\n readonly painOverloadMultiplier?: number;\n readonly minSpeedMultiplier?: number;\n};\n\n/**\n * Default configuration matching acceptance criteria.\n */\nexport const DEFAULT_INJURY_MOVEMENT_CONFIG: InjuryMovementConfig = {\n legThresholds: {\n normal: 70,\n limping: 30,\n critical: 10,\n },\n maxTorsoPenalty: 0.3,\n bothLegsInjuredMultiplier: 0.8,\n painOverloadThreshold: 80,\n painOverloadMultiplier: 0.85,\n minSpeedMultiplier: 0.1,\n} as const;\n\n/**\n * Result of injury-based movement calculation.\n * \n * **Korean**: 손상 이동 결과\n * \n * @category Movement System\n */\nexport interface InjuryMovementResult {\n /** Final speed in units/second */\n readonly finalSpeed: number;\n /** Combined speed multiplier (0.1-1.0+) */\n readonly speedMultiplier: number;\n /** Breakdown of penalty sources */\n readonly penalties: {\n readonly leftLegPenalty: number;\n readonly rightLegPenalty: number;\n readonly torsoPenalty: number;\n readonly bothLegsInjured: boolean;\n readonly stanceModifier: number;\n readonly painOverload: boolean;\n };\n /** Whether player is limping */\n readonly isLimping: boolean;\n /** Whether player has severe limp */\n readonly isSevereLimp: boolean;\n /** Bilingual status text */\n readonly statusText: {\n readonly korean: string;\n readonly english: string;\n };\n}\n\n/**\n * Injury-Based Movement Modifier System.\n * \n * **Korean**: 손상 기반 이동 수정자 시스템\n * \n * Calculates dynamic movement speed based on injuries, stance, and pain.\n * Integrates with MovementPhysics to apply realistic combat trauma effects.\n * \n * @example\n * ```typescript\n * const modifier = new InjuryMovementModifier();\n * \n * const result = modifier.calculateMovementSpeed(\n * 5.0, // base speed\n * bodyPartHealth, // current health\n * TrigramStance.GEON, // current stance\n * 65 // pain level\n * );\n * \n * console.log(`Speed: ${result.finalSpeed} m/s`);\n * console.log(`Status: ${result.statusText.korean} | ${result.statusText.english}`);\n * ```\n * \n * @category Movement System\n * @korean 손상기반이동수정자\n */\nexport class InjuryMovementModifier {\n private readonly config: InjuryMovementConfig;\n\n /**\n * Threshold for determining if both legs are significantly injured.\n * Uses epsilon to avoid floating-point rounding issues at boundary.\n * \n * @private\n */\n private static readonly BOTH_LEGS_THRESHOLD = 0.3 + 1e-10;\n\n /**\n * Creates a new InjuryMovementModifier with optional configuration.\n * \n * @param config - Optional configuration overrides (supports partial threshold overrides)\n */\n constructor(config?: PartialInjuryMovementConfig) {\n // Deep merge leg thresholds to prevent partial overrides from breaking the config.\n // Use ?? to avoid undefined values overwriting defaults when exactOptionalPropertyTypes is disabled.\n const mergedLegThresholds = {\n normal:\n config?.legThresholds?.normal ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.legThresholds.normal,\n limping:\n config?.legThresholds?.limping ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.legThresholds.limping,\n critical:\n config?.legThresholds?.critical ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.legThresholds.critical,\n };\n\n const mergedConfig: InjuryMovementConfig = {\n legThresholds: mergedLegThresholds,\n maxTorsoPenalty:\n config?.maxTorsoPenalty ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.maxTorsoPenalty,\n bothLegsInjuredMultiplier:\n config?.bothLegsInjuredMultiplier ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.bothLegsInjuredMultiplier,\n painOverloadThreshold:\n config?.painOverloadThreshold ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.painOverloadThreshold,\n painOverloadMultiplier:\n config?.painOverloadMultiplier ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.painOverloadMultiplier,\n minSpeedMultiplier:\n config?.minSpeedMultiplier ??\n DEFAULT_INJURY_MOVEMENT_CONFIG.minSpeedMultiplier,\n };\n\n // Development-mode sanity check: ensure thresholds are finite and ordered\n if (process.env.NODE_ENV !== \"production\") {\n const { normal, limping, critical } = mergedConfig.legThresholds;\n const allFinite =\n Number.isFinite(normal) &&\n Number.isFinite(limping) &&\n Number.isFinite(critical);\n const ordered = normal >= limping && limping >= critical;\n\n if (!allFinite || !ordered) {\n // Non-throwing warning to avoid breaking existing behavior\n console.warn(\n \"[InjuryMovementModifier] Invalid legThresholds configuration detected:\",\n mergedConfig.legThresholds\n );\n }\n }\n\n this.config = mergedConfig;\n }\n\n /**\n * Calculate modified movement speed based on all injury factors.\n * \n * **Korean**: 이동 속도 계산\n * \n * Applies penalties from:\n * 1. Leg injuries (primary factor)\n * 2. Torso injuries (minor factor)\n * 3. Both legs injured (cumulative penalty)\n * 4. Stance modifiers\n * 5. Pain overload\n * \n * @param baseSpeed - Base movement speed (m/s)\n * @param bodyPartHealth - Current body part health values\n * @param stance - Current trigram stance\n * @param painLevel - Current pain level (0-100)\n * @returns Complete movement calculation result\n * \n * @korean 이동속도계산\n */\n public calculateMovementSpeed(\n baseSpeed: number,\n bodyPartHealth: BodyPartHealth,\n stance: TrigramStance,\n painLevel: number\n ): InjuryMovementResult {\n // Calculate individual leg penalties\n const leftLegPenalty = this.calculateLegPenalty(bodyPartHealth.legLeft);\n const rightLegPenalty = this.calculateLegPenalty(bodyPartHealth.legRight);\n\n // Use worst leg penalty as base\n const baseLegPenalty = Math.max(leftLegPenalty, rightLegPenalty);\n let speedMultiplier = 1.0 - baseLegPenalty;\n\n // Check if both legs are significantly injured using centralized method\n const bothLegsInjured = this.areBothLegsInjured(leftLegPenalty, rightLegPenalty);\n if (bothLegsInjured) {\n // Additional 20% penalty when both legs injured\n speedMultiplier *= this.config.bothLegsInjuredMultiplier;\n }\n\n // Calculate torso penalty (minor effect, 0-30% max)\n const avgTorsoHealth = (bodyPartHealth.torsoUpper + bodyPartHealth.torsoLower) / 2;\n // Clamp torso health to prevent negative penalties or exceeding max penalty\n const clampedAvgTorsoHealth = Math.min(100, Math.max(0, avgTorsoHealth));\n const torsoPenalty = ((100 - clampedAvgTorsoHealth) / 100) * this.config.maxTorsoPenalty;\n speedMultiplier *= (1 - torsoPenalty);\n\n // Apply stance modifier\n const stanceModifier = this.getStanceSpeedModifier(stance);\n speedMultiplier *= stanceModifier;\n\n // Apply pain overload penalty if applicable (>= threshold, not just >)\n const painOverload = painLevel >= this.config.painOverloadThreshold;\n if (painOverload) {\n speedMultiplier *= this.config.painOverloadMultiplier; // -15%\n }\n\n // Clamp to minimum speed (10% of base)\n speedMultiplier = Math.max(speedMultiplier, this.config.minSpeedMultiplier);\n\n // Calculate final speed\n const finalSpeed = baseSpeed * speedMultiplier;\n\n // Determine injury state for status text based on worst leg health\n // This ensures status matches the actual movement penalty (which uses worst leg)\n // Note: isLimping = true when leg health is in range [30-70) (limping range with 0-40% penalty)\n // isSevereLimp = true when leg health is < 30 (severe/critical with 40-100% penalty)\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n const isLimping =\n worstLegHealth < this.config.legThresholds.normal &&\n worstLegHealth >= this.config.legThresholds.limping;\n const isSevereLimp = worstLegHealth < this.config.legThresholds.limping;\n\n // Generate status text\n const statusText = this.generateStatusText(worstLegHealth, bothLegsInjured, painOverload);\n\n return {\n finalSpeed,\n speedMultiplier,\n penalties: {\n leftLegPenalty,\n rightLegPenalty,\n torsoPenalty,\n bothLegsInjured,\n stanceModifier,\n painOverload,\n },\n isLimping,\n isSevereLimp,\n statusText,\n };\n }\n\n /**\n * Calculate movement penalty from leg injury severity.\n * \n * **Korean**: 다리 부상 페널티 계산\n * \n * Progressive penalty scaling:\n * - 100-70%: No penalty (0%)\n * - 70-30%: Linear scaling (0-40%)\n * - 30-10%: Accelerated scaling (40-80%)\n * - 10-0%: Critical scaling (80-100%)\n * \n * @param legHealth - Leg health (0-100)\n * @returns Penalty factor (0.0-1.0), clamped to valid range\n * \n * @private\n */\n private calculateLegPenalty(legHealth: number): number {\n const { normal, limping, critical } = this.config.legThresholds;\n\n // Clamp input health to valid range to prevent runaway penalties\n const clampedHealth = Math.min(Math.max(legHealth, 0), 100);\n\n let penalty: number;\n\n if (clampedHealth >= normal) {\n // 70-100%: No penalty\n penalty = 0;\n } else if (clampedHealth >= limping) {\n // 30-70%: 0-40% penalty (linear)\n const healthRange = normal - limping;\n // Guard against division by zero if thresholds are misconfigured\n const healthFactor = healthRange > 0 \n ? (normal - clampedHealth) / healthRange \n : 0;\n penalty = healthFactor * 0.4;\n } else if (clampedHealth >= critical) {\n // 10-30%: 40-80% penalty (accelerated)\n const healthRange = limping - critical;\n // Guard against division by zero if thresholds are misconfigured\n const healthFactor = healthRange > 0 \n ? (limping - clampedHealth) / healthRange \n : 0;\n penalty = 0.4 + (healthFactor * 0.4);\n } else {\n // 0-10%: 80-100% penalty (critical)\n // Guard against division by zero if critical is 0\n const healthFactor = critical > 0 \n ? (critical - clampedHealth) / critical \n : 1;\n penalty = 0.8 + (healthFactor * 0.2);\n }\n\n // Ensure returned penalty is always within [0, 1]\n return Math.min(Math.max(penalty, 0), 1);\n }\n\n /**\n * Checks if both legs are significantly injured (>30% penalty each).\n * Uses epsilon-based comparison for floating-point robustness.\n * \n * @param leftLegPenalty - Penalty for left leg (0-1)\n * @param rightLegPenalty - Penalty for right leg (0-1)\n * @returns True if both legs are significantly injured\n * @private\n */\n private areBothLegsInjured(leftLegPenalty: number, rightLegPenalty: number): boolean {\n return leftLegPenalty >= InjuryMovementModifier.BOTH_LEGS_THRESHOLD && \n rightLegPenalty >= InjuryMovementModifier.BOTH_LEGS_THRESHOLD;\n }\n\n /**\n * Get stance-based speed modifier.\n * \n * **Korean**: 자세 속도 배수 가져오기\n * \n * @param stance - Current trigram stance\n * @returns Speed multiplier (0.8-1.25), defaults to 1.0 for unknown stances\n * \n */\n public getStanceSpeedModifier(stance: TrigramStance): number {\n const modifier = STANCE_SPEED_MODIFIERS[stance];\n\n // Defensive fallback for unknown stances (e.g., test/invalid values)\n if (modifier == null) {\n // Runtime-safe environment check (works in both Node and browser/Vite environments)\n if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"development\"\n ) {\n console.warn(\n `[InjuryMovementModifier] Unknown stance \"${String(\n stance\n )}\" received in getStanceSpeedModifier; defaulting to 1.0.`\n );\n }\n return 1.0;\n }\n\n return modifier;\n }\n\n /**\n * Generate bilingual status text based on injury state.\n * \n * **Korean**: 상태 텍스트 생성\n * \n * @param legHealthForStatus - Leg health percentage used for status determination (typically worst leg)\n * @param bothLegsInjured - Whether both legs are significantly injured\n * @param painOverload - Whether pain is over threshold\n * @returns Bilingual status text\n * \n * @private\n */\n private generateStatusText(\n legHealthForStatus: number,\n bothLegsInjured: boolean,\n painOverload: boolean\n ): { korean: string; english: string } {\n const { normal, limping, critical } = this.config.legThresholds;\n\n if (legHealthForStatus < critical) {\n const painText = painOverload ? \" | 고통 과부하\" : \"\";\n const painTextEn = painOverload ? \" | Pain Overload\" : \"\";\n const baseKorean = bothLegsInjured ? \"심각한 부상 | 양 다리\" : \"심각한 부상\";\n const baseEnglish = bothLegsInjured ? \"Critical Injury | Both Legs\" : \"Critical Injury\";\n return {\n korean: `${baseKorean}${painText}`,\n english: `${baseEnglish}${painTextEn}`,\n };\n } else if (legHealthForStatus < limping) {\n const painText = painOverload ? \" | 고통 과부하\" : \"\";\n const painTextEn = painOverload ? \" | Pain Overload\" : \"\";\n const baseKorean = bothLegsInjured ? \"중증 절름거림 | 양 다리\" : \"중증 절름거림\";\n const baseEnglish = bothLegsInjured ? \"Severe Limping | Both Legs\" : \"Severe Limping\";\n return {\n korean: `${baseKorean}${painText}`,\n english: `${baseEnglish}${painTextEn}`,\n };\n } else if (legHealthForStatus < normal) {\n const statusKrParts = [\"절름거림\"];\n const statusEnParts = [\"Limping\"];\n\n if (bothLegsInjured) {\n statusKrParts.push(\"양 다리\");\n statusEnParts.push(\"Both Legs\");\n }\n\n if (painOverload) {\n statusKrParts.push(\"고통 과부하\");\n statusEnParts.push(\"Pain Overload\");\n }\n\n return {\n korean: statusKrParts.join(\" | \"),\n english: statusEnParts.join(\" | \"),\n };\n } else if (painOverload) {\n return {\n korean: \"고통 과부하\",\n english: \"Pain Overload\",\n };\n } else {\n return {\n korean: \"정상\",\n english: \"Normal\",\n };\n }\n }\n\n /**\n * Check if player should display limping animation.\n * \n * **Korean**: 절름거림 확인\n * \n * Uses worst leg health to match movement penalty behavior.\n * \n * @param bodyPartHealth - Current body part health\n * @returns True if limping animation should play\n * \n */\n public shouldLimp(bodyPartHealth: BodyPartHealth): boolean {\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n return worstLegHealth < this.config.legThresholds.normal;\n }\n\n /**\n * Check if player has severe limp.\n * \n * **Korean**: 중증 절름거림 확인\n * \n * Uses worst leg health to match movement penalty behavior.\n * \n * @param bodyPartHealth - Current body part health\n * @returns True if severe limp (leg health < 30%)\n * \n */\n public hasSevereLimp(bodyPartHealth: BodyPartHealth): boolean {\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n return worstLegHealth < this.config.legThresholds.limping;\n }\n\n /**\n * Get current injury state description.\n * \n * **Korean**: 부상 상태 설명\n * \n * Uses worst leg health to match movement penalty behavior.\n * \n * @param bodyPartHealth - Current body part health\n * @returns Bilingual injury description\n * \n */\n public getInjuryDescription(bodyPartHealth: BodyPartHealth): {\n korean: string;\n english: string;\n } {\n const worstLegHealth = Math.min(bodyPartHealth.legLeft, bodyPartHealth.legRight);\n const leftLegPenalty = this.calculateLegPenalty(bodyPartHealth.legLeft);\n const rightLegPenalty = this.calculateLegPenalty(bodyPartHealth.legRight);\n const bothLegsInjured = this.areBothLegsInjured(leftLegPenalty, rightLegPenalty);\n\n return this.generateStatusText(worstLegHealth, bothLegsInjured, false);\n }\n}\n\n/**\n * Singleton instance for global access.\n * \n */\nexport const injuryMovementModifier = new InjuryMovementModifier();\n"],"mappings":";;;;;AAgFA,IAAa,iCAAuD;CAClE,eAAe;EACb,QAAQ;EACR,SAAS;EACT,UAAU;CACZ;CACA,iBAAiB;CACjB,2BAA2B;CAC3B,uBAAuB;CACvB,wBAAwB;CACxB,oBAAoB;AACtB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,IAAa,yBAAb,MAAa,uBAAuB;CAClC;;;;;;;CAQA,OAAwB,sBAAsB,KAAM;;;;;;CAOpD,YAAY,QAAsC;EAehD,MAAM,eAAqC;GACzC,eAAe;IAZf,QACE,QAAQ,eAAe,UACvB,+BAA+B,cAAc;IAC/C,SACE,QAAQ,eAAe,WACvB,+BAA+B,cAAc;IAC/C,UACE,QAAQ,eAAe,YACvB,+BAA+B,cAAc;GAIhC;GACf,iBACE,QAAQ,mBACR,+BAA+B;GACjC,2BACE,QAAQ,6BACR,+BAA+B;GACjC,uBACE,QAAQ,yBACR,+BAA+B;GACjC,wBACE,QAAQ,0BACR,+BAA+B;GACjC,oBACE,QAAQ,sBACR,+BAA+B;EACnC;EAGA,IAAA,QAAA,IAAA,aAA6B,cAAc;GACzC,MAAM,EAAE,QAAQ,SAAS,aAAa,aAAa;GAOnD,IAAI,EALF,OAAO,SAAS,MAAM,KACtB,OAAO,SAAS,OAAO,KACvB,OAAO,SAAS,QAAQ,MAGR,EAFF,UAAU,WAAW,WAAW,WAI9C,QAAQ,KACN,0EACA,aAAa,aACf;EAEJ;EAEA,KAAK,SAAS;CAChB;;;;;;;;;;;;;;;;;;;;;CAsBA,uBACE,WACA,gBACA,QACA,WACsB;EAEtB,MAAM,iBAAiB,KAAK,oBAAoB,eAAe,OAAO;EACtE,MAAM,kBAAkB,KAAK,oBAAoB,eAAe,QAAQ;EAIxE,IAAI,kBAAkB,IADC,KAAK,IAAI,gBAAgB,eACpB;EAG5B,MAAM,kBAAkB,KAAK,mBAAmB,gBAAgB,eAAe;EAC/E,IAAI,iBAEF,mBAAmB,KAAK,OAAO;EAIjC,MAAM,kBAAkB,eAAe,aAAa,eAAe,cAAc;EAGjF,MAAM,gBAAiB,MADO,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,cAAc,CACzC,KAAyB,MAAO,KAAK,OAAO;EACzE,mBAAoB,IAAI;EAGxB,MAAM,iBAAiB,KAAK,uBAAuB,MAAM;EACzD,mBAAmB;EAGnB,MAAM,eAAe,aAAa,KAAK,OAAO;EAC9C,IAAI,cACF,mBAAmB,KAAK,OAAO;EAIjC,kBAAkB,KAAK,IAAI,iBAAiB,KAAK,OAAO,kBAAkB;EAG1E,MAAM,aAAa,YAAY;EAM/B,MAAM,iBAAiB,KAAK,IAAI,eAAe,SAAS,eAAe,QAAQ;EAC/E,MAAM,YACJ,iBAAiB,KAAK,OAAO,cAAc,UAC3C,kBAAkB,KAAK,OAAO,cAAc;EAC9C,MAAM,eAAe,iBAAiB,KAAK,OAAO,cAAc;EAGhE,MAAM,aAAa,KAAK,mBAAmB,gBAAgB,iBAAiB,YAAY;EAExF,OAAO;GACL;GACA;GACA,WAAW;IACT;IACA;IACA;IACA;IACA;IACA;GACF;GACA;GACA;GACA;EACF;CACF;;;;;;;;;;;;;;;;;CAkBA,oBAA4B,WAA2B;EACrD,MAAM,EAAE,QAAQ,SAAS,aAAa,KAAK,OAAO;EAGlD,MAAM,gBAAgB,KAAK,IAAI,KAAK,IAAI,WAAW,CAAC,GAAG,GAAG;EAE1D,IAAI;EAEJ,IAAI,iBAAiB,QAEnB,UAAU;OACL,IAAI,iBAAiB,SAAS;GAEnC,MAAM,cAAc,SAAS;GAK7B,WAHqB,cAAc,KAC9B,SAAS,iBAAiB,cAC3B,KACqB;EAC3B,OAAO,IAAI,iBAAiB,UAAU;GAEpC,MAAM,cAAc,UAAU;GAK9B,UAAU,MAHW,cAAc,KAC9B,UAAU,iBAAiB,cAC5B,KAC4B;EAClC,OAME,UAAU,MAHW,WAAW,KAC3B,WAAW,iBAAiB,WAC7B,KAC4B;EAIlC,OAAO,KAAK,IAAI,KAAK,IAAI,SAAS,CAAC,GAAG,CAAC;CACzC;;;;;;;;;;CAWA,mBAA2B,gBAAwB,iBAAkC;EACnF,OAAO,kBAAkB,uBAAuB,uBACzC,mBAAmB,uBAAuB;CACnD;;;;;;;;;;CAWA,uBAA8B,QAA+B;EAC3D,MAAM,WAAW,uBAAuB;EAGxC,IAAI,YAAY,MAAM;GAEpB,IACE,OAAO,YAAY,eAAA,QAAA,IAAA,aACO,eAE1B,QAAQ,KACN,4CAA4C,OAC1C,MACF,EAAE,yDACJ;GAEF,OAAO;EACT;EAEA,OAAO;CACT;;;;;;;;;;;;;CAcA,mBACE,oBACA,iBACA,cACqC;EACrC,MAAM,EAAE,QAAQ,SAAS,aAAa,KAAK,OAAO;EAElD,IAAI,qBAAqB,UAAU;GACjC,MAAM,WAAW,eAAe,cAAc;GAC9C,MAAM,aAAa,eAAe,qBAAqB;GACvD,MAAM,aAAa,kBAAkB,kBAAkB;GACvD,MAAM,cAAc,kBAAkB,gCAAgC;GACtE,OAAO;IACL,QAAQ,GAAG,aAAa;IACxB,SAAS,GAAG,cAAc;GAC5B;EACF,OAAO,IAAI,qBAAqB,SAAS;GACvC,MAAM,WAAW,eAAe,cAAc;GAC9C,MAAM,aAAa,eAAe,qBAAqB;GACvD,MAAM,aAAa,kBAAkB,mBAAmB;GACxD,MAAM,cAAc,kBAAkB,+BAA+B;GACrE,OAAO;IACL,QAAQ,GAAG,aAAa;IACxB,SAAS,GAAG,cAAc;GAC5B;EACF,OAAO,IAAI,qBAAqB,QAAQ;GACtC,MAAM,gBAAgB,CAAC,MAAM;GAC7B,MAAM,gBAAgB,CAAC,SAAS;GAEhC,IAAI,iBAAiB;IACnB,cAAc,KAAK,MAAM;IACzB,cAAc,KAAK,WAAW;GAChC;GAEA,IAAI,cAAc;IAChB,cAAc,KAAK,QAAQ;IAC3B,cAAc,KAAK,eAAe;GACpC;GAEA,OAAO;IACL,QAAQ,cAAc,KAAK,KAAK;IAChC,SAAS,cAAc,KAAK,KAAK;GACnC;EACF,OAAO,IAAI,cACT,OAAO;GACL,QAAQ;GACR,SAAS;EACX;OAEA,OAAO;GACL,QAAQ;GACR,SAAS;EACX;CAEJ;;;;;;;;;;;;CAaA,WAAkB,gBAAyC;EAEzD,OADuB,KAAK,IAAI,eAAe,SAAS,eAAe,QAChE,IAAiB,KAAK,OAAO,cAAc;CACpD;;;;;;;;;;;;CAaA,cAAqB,gBAAyC;EAE5D,OADuB,KAAK,IAAI,eAAe,SAAS,eAAe,QAChE,IAAiB,KAAK,OAAO,cAAc;CACpD;;;;;;;;;;;;CAaA,qBAA4B,gBAG1B;EACA,MAAM,iBAAiB,KAAK,IAAI,eAAe,SAAS,eAAe,QAAQ;EAC/E,MAAM,iBAAiB,KAAK,oBAAoB,eAAe,OAAO;EACtE,MAAM,kBAAkB,KAAK,oBAAoB,eAAe,QAAQ;EACxE,MAAM,kBAAkB,KAAK,mBAAmB,gBAAgB,eAAe;EAE/E,OAAO,KAAK,mBAAmB,gBAAgB,iBAAiB,KAAK;CACvE;AACF;;;;;AAMA,IAAa,yBAAyB,IAAI,uBAAuB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoordinateMapper.js","names":[],"sources":["../../../src/systems/physics/CoordinateMapper.ts"],"sourcesContent":["/**\n * Coordinate Mapper for 2D→3D conversion in Black Trigram.\n * \n * **Korean**: 좌표 변환기 (2D → 3D)\n * \n * Maps 2D pixel coordinates from the vital points data (UI overlay) to 3D world\n * coordinates for physics-based collision detection. This allows the UI-based\n * vital point system to work seamlessly with Three.js 3D collision detection.\n * \n * ## Coordinate Systems\n * \n * - **2D Pixel Space**: Origin (0,0) at top-left, used by vital points UI overlay\n * - **3D World Space**: Origin (0,0,0) at character center, used by collision detection\n * \n * ## Character Model Assumptions\n * \n * - Character height: 1.75m (average adult)\n * - Character width: 0.5m (shoulder width)\n * - Character depth: 0.3m (chest to back)\n * - Standing position: feet at y=0, head at y=1.75\n * \n * ## Mapping Strategy\n * \n * The mapper uses anatomical regions to determine the Z-depth (front-to-back position)\n * of vital points, ensuring physically accurate 3D positions for collision detection.\n * \n * @module systems/physics/CoordinateMapper\n * @category Physics System\n * @korean 좌표변환\n */\n\nimport type { Position } from \"@/types/common\";\nimport type { Position3D, AnatomicalRegionPhysics } from \"@/types/physics\";\nimport type { VitalPoint } from \"../vitalpoint/types\";\n\n/**\n * Configuration for the character model coordinate mapping.\n * \n * **Korean**: 캐릭터 모델 설정\n */\nexport interface CharacterModelConfig {\n /** Character height in meters (default: 1.75m) */\n readonly height: number;\n \n /** Character shoulder width in meters (default: 0.5m) */\n readonly width: number;\n \n /** Character depth (chest to back) in meters (default: 0.3m) */\n readonly depth: number;\n \n /** UI overlay width in pixels (default: 200px) */\n readonly overlayWidth: number;\n \n /** UI overlay height in pixels (default: 300px) */\n readonly overlayHeight: number;\n}\n\n/**\n * Average adult standing height used for physics mapping (meters).\n * \n * This value is based on common anthropometric data and provides a\n * reasonable default for a neutral, unarmed character in Black Trigram.\n */\nconst AVERAGE_ADULT_HEIGHT_M = 1.75; // 175cm average adult height\n\n/**\n * Average shoulder width used for frontal collision and UI mapping (meters).\n * \n * This approximates the biacromial (shoulder-to-shoulder) breadth for an\n * adult, which is sufficient for our hitbox and overlay alignment needs.\n */\nconst AVERAGE_SHOULDER_WIDTH_M = 0.5; // 50cm shoulder width\n\n/**\n * Average torso depth (chest to back) used for front/back hit placement (meters).\n */\nconst AVERAGE_TORSO_DEPTH_M = 0.3; // 30cm chest depth\n\n/**\n * Default UI overlay width in pixels.\n * \n * This matches the designed vital-point sprite asset width, ensuring that\n * pixel coordinates from the overlay map consistently to the 3D character.\n */\nconst DEFAULT_OVERLAY_WIDTH_PX = 200; // Standard UI overlay width\n\n/**\n * Default UI overlay height in pixels.\n * \n * This matches the designed vital-point sprite asset height, keeping the\n * 2D→3D mapping resolution-independent but asset-consistent.\n */\nconst DEFAULT_OVERLAY_HEIGHT_PX = 300; // Standard UI overlay height\n\n/**\n * Default character model configuration based on average adult proportions.\n */\nconst DEFAULT_CHARACTER_CONFIG: CharacterModelConfig = {\n height: AVERAGE_ADULT_HEIGHT_M,\n width: AVERAGE_SHOULDER_WIDTH_M,\n depth: AVERAGE_TORSO_DEPTH_M,\n overlayWidth: DEFAULT_OVERLAY_WIDTH_PX,\n overlayHeight: DEFAULT_OVERLAY_HEIGHT_PX,\n};\n\n/**\n * Z-depth (front-to-back) offsets for different anatomical regions.\n * \n * Units are **meters in model space**, measured along the character's local Z axis,\n * and are applied relative to the character's center plane (Z = 0) and the\n * configured {@link CharacterModelConfig.depth}. With the default depth of\n * `0.3m`, an offset of `0.05` corresponds to ≈5 cm of forward displacement.\n *\n * These values were chosen as simple, stable approximations based on average\n * adult proportions and validated visually against the default rig:\n *\n * - `head: 0.05` → head center sits slightly in front of the torso plane\n * (≈1/6 of torso depth) to match typical forward head posture.\n * - `neck: 0.02` → neck base is close to the torso plane but not perfectly\n * centered, acknowledging slight anterior offset of the trachea/sternum.\n * - `torso: 0.0` → torso vital points lie on the reference center plane that\n * approximates the mid‑chest / spine midpoint.\n * - `arms: 0.05` → relaxed arms hang slightly in front of the torso plane,\n * using the same forward offset as the head for consistency.\n * - `legs: 0.0` → leg targets (front of thighs/shins) are modeled on the\n * center plane; depth variation is instead captured by vertical placement.\n *\n * If you change {@link CharacterModelConfig.depth}, you may keep these absolute\n * meter values (for physical fidelity) or re‑express them as fractions of the\n * new depth, but they should continue to represent small (≈0–5 cm) anatomical\n * offsets from the torso midline.\n */\nconst REGION_DEPTH_OFFSETS: Record<AnatomicalRegionPhysics, number> = {\n head: 0.05, // ≈5 cm forward from torso mid-plane to match cranial alignment\n neck: 0.02, // ≈2 cm forward; neck base is near, but not exactly on, center\n torso: 0.0, // Torso reference plane (mid‑chest / spine midpoint)\n arms: 0.05, // ≈5 cm forward; relaxed arms rest slightly in front of torso\n legs: 0.0, // Legs aligned to torso center plane; front/back handled elsewhere\n};\n\n/**\n * Coordinate Mapper for converting 2D pixel coordinates to 3D world coordinates.\n * \n * **Korean**: 좌표 변환기\n * \n * Provides bidirectional mapping between the 2D UI overlay coordinate system\n * and the 3D physics world coordinate system.\n * \n * @category Physics System\n * @korean 좌표변환기\n */\nexport class CoordinateMapper {\n private readonly config: CharacterModelConfig;\n \n /**\n * Creates a new CoordinateMapper with optional custom configuration.\n * \n * @param config - Optional character model configuration\n */\n constructor(config: Partial<CharacterModelConfig> = {}) {\n this.config = { ...DEFAULT_CHARACTER_CONFIG, ...config };\n }\n \n /**\n * Converts a 2D pixel position to a 3D world position.\n * \n * **Korean**: 2D → 3D 변환\n * \n * Maps UI overlay pixel coordinates to 3D world space coordinates suitable\n * for physics collision detection.\n * \n * @param pixel2D - 2D pixel position (origin top-left)\n * @param region - Anatomical region for Z-depth calculation\n * @returns 3D world position in meters (origin at character center)\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const vitalPoint = { position: { x: 100, y: 50 } };\n * const worldPos = mapper.pixel2DToWorld3D(vitalPoint.position, \"head\");\n * // worldPos: { x: 0, y: 1.45, z: 0.05 }\n * ```\n */\n pixel2DToWorld3D(pixel2D: Position, region: AnatomicalRegionPhysics): Position3D {\n // Normalize pixel coordinates to 0-1 range\n const normalizedX = pixel2D.x / this.config.overlayWidth;\n const normalizedY = pixel2D.y / this.config.overlayHeight;\n \n // Convert to world coordinates\n // X: Map from pixel space (0 = left, overlayWidth = right) to world space (-width/2 to +width/2)\n const worldX = (normalizedX - 0.5) * this.config.width;\n \n // Y: Map from pixel space (0 = top, overlayHeight = bottom) to world space (height to 0)\n // Invert Y-axis: pixel 0 = top = character head, pixel overlayHeight = bottom = character feet\n const worldY = (1 - normalizedY) * this.config.height;\n \n // Z: Use anatomical region to determine depth offset\n const worldZ = REGION_DEPTH_OFFSETS[region];\n \n return {\n x: worldX,\n y: worldY,\n z: worldZ,\n };\n }\n \n /**\n * Converts a 3D world position to a 2D pixel position.\n * \n * **Korean**: 3D → 2D 변환\n * \n * Maps 3D world space coordinates back to UI overlay pixel coordinates.\n * Useful for debugging and visual feedback.\n * \n * @param world3D - 3D world position in meters\n * @returns 2D pixel position (origin top-left)\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const worldPos = { x: 0, y: 1.45, z: 0.05 };\n * const pixelPos = mapper.world3DToPixel2D(worldPos);\n * // pixelPos: { x: 100, y: 50 }\n * ```\n */\n world3DToPixel2D(world3D: Position3D): Position {\n // Convert world X to normalized 0-1 range\n const normalizedX = (world3D.x / this.config.width) + 0.5;\n \n // Convert world Y to normalized 0-1 range (inverted)\n const normalizedY = 1 - (world3D.y / this.config.height);\n \n // Convert to pixel coordinates\n return {\n x: Math.round(normalizedX * this.config.overlayWidth),\n y: Math.round(normalizedY * this.config.overlayHeight),\n };\n }\n \n /**\n * Converts a vital point's 2D position to 3D world position.\n * \n * **Korean**: 급소 위치 변환\n * \n * Convenience method that extracts position and region from a VitalPoint object.\n * \n * @param vitalPoint - Vital point with 2D position\n * @returns 3D world position\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const temple = VITAL_POINTS_DATA.find(vp => vp.id === \"head_temple\");\n * const worldPos = mapper.vitalPointToWorld3D(temple);\n * ```\n */\n vitalPointToWorld3D(vitalPoint: VitalPoint): Position3D {\n // Infer region from vital point ID prefix\n const region = this.inferRegionFromId(vitalPoint.id);\n return this.pixel2DToWorld3D(vitalPoint.position, region);\n }\n \n /**\n * Infers the anatomical region from a vital point ID.\n * \n * **Korean**: 급소 ID에서 영역 추론\n * \n * @param vitalPointId - Vital point ID (e.g., \"head_temple\", \"torso_solar_plexus\")\n * @returns Anatomical region\n * \n * @private\n */\n private inferRegionFromId(vitalPointId: string): AnatomicalRegionPhysics {\n if (vitalPointId.startsWith(\"head_\")) return \"head\";\n if (vitalPointId.startsWith(\"neck_\")) return \"neck\";\n if (vitalPointId.startsWith(\"torso_\")) return \"torso\";\n if (vitalPointId.startsWith(\"arm_\")) return \"arms\";\n if (vitalPointId.startsWith(\"leg_\")) return \"legs\";\n \n // Default to torso if no clear region prefix\n return \"torso\";\n }\n \n /**\n * Calculates the distance between a 3D point and a vital point's 3D position.\n * \n * **Korean**: 거리 계산\n * \n * @param point3D - Point in 3D world space\n * @param vitalPoint - Vital point to measure distance to\n * @returns Distance in meters\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const attackPoint = { x: 0, y: 1.5, z: 0.1 };\n * const temple = VITAL_POINTS_DATA.find(vp => vp.id === \"head_temple\");\n * const distance = mapper.distanceToVitalPoint(attackPoint, temple);\n * ```\n */\n distanceToVitalPoint(point3D: Position3D, vitalPoint: VitalPoint): number {\n const vitalPoint3D = this.vitalPointToWorld3D(vitalPoint);\n \n const dx = point3D.x - vitalPoint3D.x;\n const dy = point3D.y - vitalPoint3D.y;\n const dz = point3D.z - vitalPoint3D.z;\n \n return Math.sqrt(dx * dx + dy * dy + dz * dz);\n }\n \n /**\n * Finds the closest vital point to a 3D position within a given region.\n * \n * **Korean**: 가장 가까운 급소 찾기\n * \n * @param point3D - Point in 3D world space\n * @param vitalPoints - Array of vital points to search\n * @param region - Optional region filter\n * @returns Closest vital point and distance, or null if none found\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const hitPoint = { x: 0, y: 1.7, z: 0.05 };\n * const result = mapper.findClosestVitalPoint(hitPoint, VITAL_POINTS_DATA, \"head\");\n * if (result) {\n * console.log(`Hit ${result.vitalPoint.names.english} at ${result.distance}m`);\n * }\n * ```\n */\n findClosestVitalPoint(\n point3D: Position3D,\n vitalPoints: readonly VitalPoint[],\n region?: AnatomicalRegionPhysics\n ): { vitalPoint: VitalPoint; distance: number } | null {\n let closestVitalPoint: VitalPoint | null = null;\n let closestDistance = Infinity;\n \n for (const vitalPoint of vitalPoints) {\n // Filter by region if specified\n if (region) {\n const vpRegion = this.inferRegionFromId(vitalPoint.id);\n if (vpRegion !== region) continue;\n }\n \n const distance = this.distanceToVitalPoint(point3D, vitalPoint);\n \n if (distance < closestDistance) {\n closestDistance = distance;\n closestVitalPoint = vitalPoint;\n }\n }\n \n if (closestVitalPoint === null) {\n return null;\n }\n \n return {\n vitalPoint: closestVitalPoint,\n distance: closestDistance,\n };\n }\n \n /**\n * Gets the current character model configuration.\n * \n * @returns Current configuration\n */\n getConfig(): CharacterModelConfig {\n return { ...this.config };\n }\n}\n\n/**\n * Default singleton instance for convenient access.\n * \n * **Korean**: 기본 인스턴스\n * \n * @example\n * ```typescript\n * import { defaultCoordinateMapper } from './CoordinateMapper';\n * \n * const worldPos = defaultCoordinateMapper.pixel2DToWorld3D({ x: 100, y: 50 }, \"head\");\n * ```\n */\nexport const defaultCoordinateMapper = new CoordinateMapper();\n"],"mappings":";;;;AAiGA,IAAM,2BAAiD;CACrD,QAAQ;CACR,OAAO;CACP,OAAO;CACP,cAAc;CACd,eAAe;AACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAM,uBAAgE;CACpE,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACN,MAAM;AACR;;;;;;;;;;;;AAaA,IAAa,mBAAb,MAA8B;CAC5B;;;;;;CAOA,YAAY,SAAwC,CAAC,GAAG;EACtD,KAAK,SAAS;GAAE,GAAG;GAA0B,GAAG;EAAO;CACzD;;;;;;;;;;;;;;;;;;;;;CAsBA,iBAAiB,SAAmB,QAA6C;EAE/E,MAAM,cAAc,QAAQ,IAAI,KAAK,OAAO;EAC5C,MAAM,cAAc,QAAQ,IAAI,KAAK,OAAO;EAa5C,OAAO;GACL,IAVc,cAAc,MAAO,KAAK,OAAO;GAW/C,IAPc,IAAI,eAAe,KAAK,OAAO;GAQ7C,GALa,qBAAqB;EAMpC;CACF;;;;;;;;;;;;;;;;;;;;CAqBA,iBAAiB,SAA+B;EAE9C,MAAM,cAAe,QAAQ,IAAI,KAAK,OAAO,QAAS;EAGtD,MAAM,cAAc,IAAK,QAAQ,IAAI,KAAK,OAAO;EAGjD,OAAO;GACL,GAAG,KAAK,MAAM,cAAc,KAAK,OAAO,YAAY;GACpD,GAAG,KAAK,MAAM,cAAc,KAAK,OAAO,aAAa;EACvD;CACF;;;;;;;;;;;;;;;;;;CAmBA,oBAAoB,YAAoC;EAEtD,MAAM,SAAS,KAAK,kBAAkB,WAAW,EAAE;EACnD,OAAO,KAAK,iBAAiB,WAAW,UAAU,MAAM;CAC1D;;;;;;;;;;;CAYA,kBAA0B,cAA+C;EACvE,IAAI,aAAa,WAAW,OAAO,GAAG,OAAO;EAC7C,IAAI,aAAa,WAAW,OAAO,GAAG,OAAO;EAC7C,IAAI,aAAa,WAAW,QAAQ,GAAG,OAAO;EAC9C,IAAI,aAAa,WAAW,MAAM,GAAG,OAAO;EAC5C,IAAI,aAAa,WAAW,MAAM,GAAG,OAAO;EAG5C,OAAO;CACT;;;;;;;;;;;;;;;;;;CAmBA,qBAAqB,SAAqB,YAAgC;EACxE,MAAM,eAAe,KAAK,oBAAoB,UAAU;EAExD,MAAM,KAAK,QAAQ,IAAI,aAAa;EACpC,MAAM,KAAK,QAAQ,IAAI,aAAa;EACpC,MAAM,KAAK,QAAQ,IAAI,aAAa;EAEpC,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE;CAC9C;;;;;;;;;;;;;;;;;;;;;CAsBA,sBACE,SACA,aACA,QACqD;EACrD,IAAI,oBAAuC;EAC3C,IAAI,kBAAkB;EAEtB,KAAK,MAAM,cAAc,aAAa;GAEpC,IAAI;QACe,KAAK,kBAAkB,WAAW,EAC/C,MAAa,QAAQ;GAAA;GAG3B,MAAM,WAAW,KAAK,qBAAqB,SAAS,UAAU;GAE9D,IAAI,WAAW,iBAAiB;IAC9B,kBAAkB;IAClB,oBAAoB;GACtB;EACF;EAEA,IAAI,sBAAsB,MACxB,OAAO;EAGT,OAAO;GACL,YAAY;GACZ,UAAU;EACZ;CACF;;;;;;CAOA,YAAkC;EAChC,OAAO,EAAE,GAAG,KAAK,OAAO;CAC1B;AACF;AAcuC,IAAI,iBAAiB"}
|
|
1
|
+
{"version":3,"file":"CoordinateMapper.js","names":[],"sources":["../../../src/systems/physics/CoordinateMapper.ts"],"sourcesContent":["/**\n * Coordinate Mapper for 2D→3D conversion in Black Trigram.\n * \n * **Korean**: 좌표 변환기 (2D → 3D)\n * \n * Maps 2D pixel coordinates from the vital points data (UI overlay) to 3D world\n * coordinates for physics-based collision detection. This allows the UI-based\n * vital point system to work seamlessly with Three.js 3D collision detection.\n * \n * ## Coordinate Systems\n * \n * - **2D Pixel Space**: Origin (0,0) at top-left, used by vital points UI overlay\n * - **3D World Space**: Origin (0,0,0) at character center, used by collision detection\n * \n * ## Character Model Assumptions\n * \n * - Character height: 1.75m (average adult)\n * - Character width: 0.5m (shoulder width)\n * - Character depth: 0.3m (chest to back)\n * - Standing position: feet at y=0, head at y=1.75\n * \n * ## Mapping Strategy\n * \n * The mapper uses anatomical regions to determine the Z-depth (front-to-back position)\n * of vital points, ensuring physically accurate 3D positions for collision detection.\n * \n * @module systems/physics/CoordinateMapper\n * @category Physics System\n * @korean 좌표변환\n */\n\nimport type { Position } from \"@/types/common\";\nimport type { Position3D, AnatomicalRegionPhysics } from \"@/types/physics\";\nimport type { VitalPoint } from \"../vitalpoint/types\";\n\n/**\n * Configuration for the character model coordinate mapping.\n * \n * **Korean**: 캐릭터 모델 설정\n */\nexport interface CharacterModelConfig {\n /** Character height in meters (default: 1.75m) */\n readonly height: number;\n \n /** Character shoulder width in meters (default: 0.5m) */\n readonly width: number;\n \n /** Character depth (chest to back) in meters (default: 0.3m) */\n readonly depth: number;\n \n /** UI overlay width in pixels (default: 200px) */\n readonly overlayWidth: number;\n \n /** UI overlay height in pixels (default: 300px) */\n readonly overlayHeight: number;\n}\n\n/**\n * Average adult standing height used for physics mapping (meters).\n * \n * This value is based on common anthropometric data and provides a\n * reasonable default for a neutral, unarmed character in Black Trigram.\n */\nconst AVERAGE_ADULT_HEIGHT_M = 1.75; // 175cm average adult height\n\n/**\n * Average shoulder width used for frontal collision and UI mapping (meters).\n * \n * This approximates the biacromial (shoulder-to-shoulder) breadth for an\n * adult, which is sufficient for our hitbox and overlay alignment needs.\n */\nconst AVERAGE_SHOULDER_WIDTH_M = 0.5; // 50cm shoulder width\n\n/**\n * Average torso depth (chest to back) used for front/back hit placement (meters).\n */\nconst AVERAGE_TORSO_DEPTH_M = 0.3; // 30cm chest depth\n\n/**\n * Default UI overlay width in pixels.\n * \n * This matches the designed vital-point sprite asset width, ensuring that\n * pixel coordinates from the overlay map consistently to the 3D character.\n */\nconst DEFAULT_OVERLAY_WIDTH_PX = 200; // Standard UI overlay width\n\n/**\n * Default UI overlay height in pixels.\n * \n * This matches the designed vital-point sprite asset height, keeping the\n * 2D→3D mapping resolution-independent but asset-consistent.\n */\nconst DEFAULT_OVERLAY_HEIGHT_PX = 300; // Standard UI overlay height\n\n/**\n * Default character model configuration based on average adult proportions.\n */\nconst DEFAULT_CHARACTER_CONFIG: CharacterModelConfig = {\n height: AVERAGE_ADULT_HEIGHT_M,\n width: AVERAGE_SHOULDER_WIDTH_M,\n depth: AVERAGE_TORSO_DEPTH_M,\n overlayWidth: DEFAULT_OVERLAY_WIDTH_PX,\n overlayHeight: DEFAULT_OVERLAY_HEIGHT_PX,\n};\n\n/**\n * Z-depth (front-to-back) offsets for different anatomical regions.\n * \n * Units are **meters in model space**, measured along the character's local Z axis,\n * and are applied relative to the character's center plane (Z = 0) and the\n * configured {@link CharacterModelConfig.depth}. With the default depth of\n * `0.3m`, an offset of `0.05` corresponds to ≈5 cm of forward displacement.\n *\n * These values were chosen as simple, stable approximations based on average\n * adult proportions and validated visually against the default rig:\n *\n * - `head: 0.05` → head center sits slightly in front of the torso plane\n * (≈1/6 of torso depth) to match typical forward head posture.\n * - `neck: 0.02` → neck base is close to the torso plane but not perfectly\n * centered, acknowledging slight anterior offset of the trachea/sternum.\n * - `torso: 0.0` → torso vital points lie on the reference center plane that\n * approximates the mid‑chest / spine midpoint.\n * - `arms: 0.05` → relaxed arms hang slightly in front of the torso plane,\n * using the same forward offset as the head for consistency.\n * - `legs: 0.0` → leg targets (front of thighs/shins) are modeled on the\n * center plane; depth variation is instead captured by vertical placement.\n *\n * If you change {@link CharacterModelConfig.depth}, you may keep these absolute\n * meter values (for physical fidelity) or re‑express them as fractions of the\n * new depth, but they should continue to represent small (≈0–5 cm) anatomical\n * offsets from the torso midline.\n */\nconst REGION_DEPTH_OFFSETS: Record<AnatomicalRegionPhysics, number> = {\n head: 0.05, // ≈5 cm forward from torso mid-plane to match cranial alignment\n neck: 0.02, // ≈2 cm forward; neck base is near, but not exactly on, center\n torso: 0.0, // Torso reference plane (mid‑chest / spine midpoint)\n arms: 0.05, // ≈5 cm forward; relaxed arms rest slightly in front of torso\n legs: 0.0, // Legs aligned to torso center plane; front/back handled elsewhere\n};\n\n/**\n * Coordinate Mapper for converting 2D pixel coordinates to 3D world coordinates.\n * \n * **Korean**: 좌표 변환기\n * \n * Provides bidirectional mapping between the 2D UI overlay coordinate system\n * and the 3D physics world coordinate system.\n * \n * @category Physics System\n * @korean 좌표변환기\n */\nexport class CoordinateMapper {\n private readonly config: CharacterModelConfig;\n \n /**\n * Creates a new CoordinateMapper with optional custom configuration.\n * \n * @param config - Optional character model configuration\n */\n constructor(config: Partial<CharacterModelConfig> = {}) {\n this.config = { ...DEFAULT_CHARACTER_CONFIG, ...config };\n }\n \n /**\n * Converts a 2D pixel position to a 3D world position.\n * \n * **Korean**: 2D → 3D 변환\n * \n * Maps UI overlay pixel coordinates to 3D world space coordinates suitable\n * for physics collision detection.\n * \n * @param pixel2D - 2D pixel position (origin top-left)\n * @param region - Anatomical region for Z-depth calculation\n * @returns 3D world position in meters (origin at character center)\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const vitalPoint = { position: { x: 100, y: 50 } };\n * const worldPos = mapper.pixel2DToWorld3D(vitalPoint.position, \"head\");\n * // worldPos: { x: 0, y: 1.45, z: 0.05 }\n * ```\n */\n pixel2DToWorld3D(pixel2D: Position, region: AnatomicalRegionPhysics): Position3D {\n // Normalize pixel coordinates to 0-1 range\n const normalizedX = pixel2D.x / this.config.overlayWidth;\n const normalizedY = pixel2D.y / this.config.overlayHeight;\n \n // Convert to world coordinates\n // X: Map from pixel space (0 = left, overlayWidth = right) to world space (-width/2 to +width/2)\n const worldX = (normalizedX - 0.5) * this.config.width;\n \n // Y: Map from pixel space (0 = top, overlayHeight = bottom) to world space (height to 0)\n // Invert Y-axis: pixel 0 = top = character head, pixel overlayHeight = bottom = character feet\n const worldY = (1 - normalizedY) * this.config.height;\n \n // Z: Use anatomical region to determine depth offset\n const worldZ = REGION_DEPTH_OFFSETS[region];\n \n return {\n x: worldX,\n y: worldY,\n z: worldZ,\n };\n }\n \n /**\n * Converts a 3D world position to a 2D pixel position.\n * \n * **Korean**: 3D → 2D 변환\n * \n * Maps 3D world space coordinates back to UI overlay pixel coordinates.\n * Useful for debugging and visual feedback.\n * \n * @param world3D - 3D world position in meters\n * @returns 2D pixel position (origin top-left)\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const worldPos = { x: 0, y: 1.45, z: 0.05 };\n * const pixelPos = mapper.world3DToPixel2D(worldPos);\n * // pixelPos: { x: 100, y: 50 }\n * ```\n */\n world3DToPixel2D(world3D: Position3D): Position {\n // Convert world X to normalized 0-1 range\n const normalizedX = (world3D.x / this.config.width) + 0.5;\n \n // Convert world Y to normalized 0-1 range (inverted)\n const normalizedY = 1 - (world3D.y / this.config.height);\n \n // Convert to pixel coordinates\n return {\n x: Math.round(normalizedX * this.config.overlayWidth),\n y: Math.round(normalizedY * this.config.overlayHeight),\n };\n }\n \n /**\n * Converts a vital point's 2D position to 3D world position.\n * \n * **Korean**: 급소 위치 변환\n * \n * Convenience method that extracts position and region from a VitalPoint object.\n * \n * @param vitalPoint - Vital point with 2D position\n * @returns 3D world position\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const temple = VITAL_POINTS_DATA.find(vp => vp.id === \"head_temple\");\n * const worldPos = mapper.vitalPointToWorld3D(temple);\n * ```\n */\n vitalPointToWorld3D(vitalPoint: VitalPoint): Position3D {\n // Infer region from vital point ID prefix\n const region = this.inferRegionFromId(vitalPoint.id);\n return this.pixel2DToWorld3D(vitalPoint.position, region);\n }\n \n /**\n * Infers the anatomical region from a vital point ID.\n * \n * **Korean**: 급소 ID에서 영역 추론\n * \n * @param vitalPointId - Vital point ID (e.g., \"head_temple\", \"torso_solar_plexus\")\n * @returns Anatomical region\n * \n * @private\n */\n private inferRegionFromId(vitalPointId: string): AnatomicalRegionPhysics {\n if (vitalPointId.startsWith(\"head_\")) return \"head\";\n if (vitalPointId.startsWith(\"neck_\")) return \"neck\";\n if (vitalPointId.startsWith(\"torso_\")) return \"torso\";\n if (vitalPointId.startsWith(\"arm_\")) return \"arms\";\n if (vitalPointId.startsWith(\"leg_\")) return \"legs\";\n \n // Default to torso if no clear region prefix\n return \"torso\";\n }\n \n /**\n * Calculates the distance between a 3D point and a vital point's 3D position.\n * \n * **Korean**: 거리 계산\n * \n * @param point3D - Point in 3D world space\n * @param vitalPoint - Vital point to measure distance to\n * @returns Distance in meters\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const attackPoint = { x: 0, y: 1.5, z: 0.1 };\n * const temple = VITAL_POINTS_DATA.find(vp => vp.id === \"head_temple\");\n * const distance = mapper.distanceToVitalPoint(attackPoint, temple);\n * ```\n */\n distanceToVitalPoint(point3D: Position3D, vitalPoint: VitalPoint): number {\n const vitalPoint3D = this.vitalPointToWorld3D(vitalPoint);\n \n const dx = point3D.x - vitalPoint3D.x;\n const dy = point3D.y - vitalPoint3D.y;\n const dz = point3D.z - vitalPoint3D.z;\n \n return Math.sqrt(dx * dx + dy * dy + dz * dz);\n }\n \n /**\n * Finds the closest vital point to a 3D position within a given region.\n * \n * **Korean**: 가장 가까운 급소 찾기\n * \n * @param point3D - Point in 3D world space\n * @param vitalPoints - Array of vital points to search\n * @param region - Optional region filter\n * @returns Closest vital point and distance, or null if none found\n * \n * @example\n * ```typescript\n * const mapper = new CoordinateMapper();\n * const hitPoint = { x: 0, y: 1.7, z: 0.05 };\n * const result = mapper.findClosestVitalPoint(hitPoint, VITAL_POINTS_DATA, \"head\");\n * if (result) {\n * console.log(`Hit ${result.vitalPoint.names.english} at ${result.distance}m`);\n * }\n * ```\n */\n findClosestVitalPoint(\n point3D: Position3D,\n vitalPoints: readonly VitalPoint[],\n region?: AnatomicalRegionPhysics\n ): { vitalPoint: VitalPoint; distance: number } | null {\n let closestVitalPoint: VitalPoint | null = null;\n let closestDistance = Infinity;\n \n for (const vitalPoint of vitalPoints) {\n // Filter by region if specified\n if (region) {\n const vpRegion = this.inferRegionFromId(vitalPoint.id);\n if (vpRegion !== region) continue;\n }\n \n const distance = this.distanceToVitalPoint(point3D, vitalPoint);\n \n if (distance < closestDistance) {\n closestDistance = distance;\n closestVitalPoint = vitalPoint;\n }\n }\n \n if (closestVitalPoint === null) {\n return null;\n }\n \n return {\n vitalPoint: closestVitalPoint,\n distance: closestDistance,\n };\n }\n \n /**\n * Gets the current character model configuration.\n * \n * @returns Current configuration\n */\n getConfig(): CharacterModelConfig {\n return { ...this.config };\n }\n}\n\n/**\n * Default singleton instance for convenient access.\n * \n * **Korean**: 기본 인스턴스\n * \n * @example\n * ```typescript\n * import { defaultCoordinateMapper } from './CoordinateMapper';\n * \n * const worldPos = defaultCoordinateMapper.pixel2DToWorld3D({ x: 100, y: 50 }, \"head\");\n * ```\n */\nexport const defaultCoordinateMapper = new CoordinateMapper();\n"],"mappings":";;;;AAiGA,IAAM,2BAAiD;CACrD,QAAQ;CACR,OAAO;CACP,OAAO;CACP,cAAc;CACd,eAAe;AACjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAM,uBAAgE;CACpE,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACN,MAAM;AACR;;;;;;;;;;;;AAaA,IAAa,mBAAb,MAA8B;CAC5B;;;;;;CAOA,YAAY,SAAwC,CAAC,GAAG;EACtD,KAAK,SAAS;GAAE,GAAG;GAA0B,GAAG;EAAO;CACzD;;;;;;;;;;;;;;;;;;;;;CAsBA,iBAAiB,SAAmB,QAA6C;EAE/E,MAAM,cAAc,QAAQ,IAAI,KAAK,OAAO;EAC5C,MAAM,cAAc,QAAQ,IAAI,KAAK,OAAO;EAa5C,OAAO;GACL,IAVc,cAAc,MAAO,KAAK,OAAO;GAW/C,IAPc,IAAI,eAAe,KAAK,OAAO;GAQ7C,GALa,qBAAqB;EAMpC;CACF;;;;;;;;;;;;;;;;;;;;CAqBA,iBAAiB,SAA+B;EAE9C,MAAM,cAAe,QAAQ,IAAI,KAAK,OAAO,QAAS;EAGtD,MAAM,cAAc,IAAK,QAAQ,IAAI,KAAK,OAAO;EAGjD,OAAO;GACL,GAAG,KAAK,MAAM,cAAc,KAAK,OAAO,YAAY;GACpD,GAAG,KAAK,MAAM,cAAc,KAAK,OAAO,aAAa;EACvD;CACF;;;;;;;;;;;;;;;;;;CAmBA,oBAAoB,YAAoC;EAEtD,MAAM,SAAS,KAAK,kBAAkB,WAAW,EAAE;EACnD,OAAO,KAAK,iBAAiB,WAAW,UAAU,MAAM;CAC1D;;;;;;;;;;;CAYA,kBAA0B,cAA+C;EACvE,IAAI,aAAa,WAAW,OAAO,GAAG,OAAO;EAC7C,IAAI,aAAa,WAAW,OAAO,GAAG,OAAO;EAC7C,IAAI,aAAa,WAAW,QAAQ,GAAG,OAAO;EAC9C,IAAI,aAAa,WAAW,MAAM,GAAG,OAAO;EAC5C,IAAI,aAAa,WAAW,MAAM,GAAG,OAAO;EAG5C,OAAO;CACT;;;;;;;;;;;;;;;;;;CAmBA,qBAAqB,SAAqB,YAAgC;EACxE,MAAM,eAAe,KAAK,oBAAoB,UAAU;EAExD,MAAM,KAAK,QAAQ,IAAI,aAAa;EACpC,MAAM,KAAK,QAAQ,IAAI,aAAa;EACpC,MAAM,KAAK,QAAQ,IAAI,aAAa;EAEpC,OAAO,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE;CAC9C;;;;;;;;;;;;;;;;;;;;;CAsBA,sBACE,SACA,aACA,QACqD;EACrD,IAAI,oBAAuC;EAC3C,IAAI,kBAAkB;EAEtB,KAAK,MAAM,cAAc,aAAa;GAEpC,IAAI,QACe;QAAA,KAAK,kBAAkB,WAAW,EAC/C,MAAa,QAAQ;GAAA;GAG3B,MAAM,WAAW,KAAK,qBAAqB,SAAS,UAAU;GAE9D,IAAI,WAAW,iBAAiB;IAC9B,kBAAkB;IAClB,oBAAoB;GACtB;EACF;EAEA,IAAI,sBAAsB,MACxB,OAAO;EAGT,OAAO;GACL,YAAY;GACZ,UAAU;EACZ;CACF;;;;;;CAOA,YAAkC;EAChC,OAAO,EAAE,GAAG,KAAK,OAAO;CAC1B;AACF;AAcuC,IAAI,iBAAiB"}
|
|
@@ -185,8 +185,13 @@ var DamageCalculator = class DamageCalculator {
|
|
|
185
185
|
let hipRotationModifier = 1;
|
|
186
186
|
const techniqueName = technique.name?.english || technique.englishName || "";
|
|
187
187
|
const isKickTechnique = technique.type === "kick" || techniqueName.toLowerCase().includes("kick");
|
|
188
|
-
if (kickHipState && isKickTechnique)
|
|
189
|
-
|
|
188
|
+
if (kickHipState && isKickTechnique) {
|
|
189
|
+
const kickType = DamageCalculator.determineKickType(techniqueName);
|
|
190
|
+
hipRotationModifier = calculateKickPowerFromHipRotation(kickHipState, kickType);
|
|
191
|
+
} else if (hipRotationAngle !== 0) {
|
|
192
|
+
const techniqueType = technique.type === "throw" ? "throw" : technique.type === "joint_lock" ? "joint" : "strike";
|
|
193
|
+
hipRotationModifier = calculateHipRotationPowerModifier(hipRotationAngle, techniqueType);
|
|
194
|
+
}
|
|
190
195
|
const criticalMultiplier = accuracy > .9 ? 2 : 1;
|
|
191
196
|
const isCritical = accuracy > .9;
|
|
192
197
|
let totalDamage = baseDamage * stanceEffectiveness * vitalPointMultiplier * accuracyBonus * meridianBonus * timeBonus * archetypeBonus * hipRotationModifier * criticalMultiplier;
|
|
@@ -307,9 +312,7 @@ var DamageCalculator = class DamageCalculator {
|
|
|
307
312
|
case PlayerArchetype.JEONGBO_YOWON:
|
|
308
313
|
if (vitalPoint?.category === "vascular") bonus *= 1.25;
|
|
309
314
|
break;
|
|
310
|
-
case PlayerArchetype.JOJIK_POKRYEOKBAE:
|
|
311
|
-
if (technique.id.includes("ruthless") || technique.id.includes("brutal") || technique.id.includes("dirty")) bonus *= 1.2;
|
|
312
|
-
break;
|
|
315
|
+
case PlayerArchetype.JOJIK_POKRYEOKBAE: if (technique.id.includes("ruthless") || technique.id.includes("brutal") || technique.id.includes("dirty")) bonus *= 1.2;
|
|
313
316
|
}
|
|
314
317
|
return Math.min(1.5, bonus);
|
|
315
318
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DamageCalculator.js","names":[],"sources":["../../../src/systems/vitalpoint/DamageCalculator.ts"],"sourcesContent":["import {\n PlayerArchetype,\n TrigramStance,\n VitalPointSeverity,\n} from \"../../types/common\";\nimport {\n calculateHipRotationPowerModifier,\n calculateKickPowerFromHipRotation,\n type HipRotationState,\n type KickType,\n} from \"../animation\";\nimport { PlayerState } from \"../player\";\nimport { TrigramCalculator } from \"../trigram/TrigramCalculator\";\nimport { asExtendedGonTechnique } from \"../trigram/types/GonTechniqueExtensions\";\nimport { StatusEffect } from \"../types\";\nimport { calculateMeridianFlow } from \"./KoreanAnatomy\";\nimport { getMeridiansForVitalPoint } from \"./MeridianVitalPointMapping\";\nimport {\n DamageResult,\n KoreanTechnique,\n VitalPoint,\n VitalPointHitResult,\n} from \"./types\";\n\nexport class DamageCalculator {\n /**\n * Vital point severity damage multipliers\n */\n private static readonly SEVERITY_MULTIPLIERS: Record<\n VitalPointSeverity,\n number\n > = {\n [VitalPointSeverity.MINOR]: 1.2,\n [VitalPointSeverity.MODERATE]: 1.5,\n [VitalPointSeverity.MAJOR]: 2.0,\n [VitalPointSeverity.CRITICAL]: 2.5,\n [VitalPointSeverity.LETHAL]: 3.0,\n };\n\n /**\n * Accuracy bonus scaling constants\n * Maps 0.0-1.0 accuracy to 0.8x-1.2x multiplier\n */\n private static readonly ACCURACY_MIN_MULTIPLIER = 0.8;\n private static readonly ACCURACY_RANGE = 0.4; // Range from min to max (1.2 - 0.8)\n\n /**\n * Calculate vital point damage with proper archetype bonuses\n */\n static calculateVitalPointDamage(\n vitalPoint: VitalPoint,\n baseDamage: number,\n attacker: PlayerState,\n accuracy: number,\n ): DamageResult {\n const effects: StatusEffect[] = [];\n\n // Fix: Use static method call instead of this\n const archetypeModifier = DamageCalculator.getArchetypeModifier(\n attacker.archetype,\n );\n const techniqueEffectiveness = accuracy;\n\n const finalDamage = Math.max(\n 1,\n (vitalPoint.baseDamage ?? vitalPoint.damage?.min ?? baseDamage) *\n archetypeModifier *\n techniqueEffectiveness,\n );\n\n // Create status effects from vital point\n vitalPoint.effects.forEach((effect) => {\n const statusEffect: StatusEffect = {\n id: `${effect.id}_${Date.now()}`,\n type: \"weakened\", // Map to valid EffectType\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- EffectIntensity type mapping from vital point effect\n intensity: \"moderate\" as any,\n duration: effect.duration,\n description: effect.description,\n stackable: effect.stackable,\n source: vitalPoint.id,\n startTime: Date.now(),\n endTime: Date.now() + effect.duration,\n };\n effects.push(statusEffect);\n });\n\n return {\n damage: finalDamage,\n effects,\n isCritical: accuracy > 0.9,\n isVitalPoint: true,\n };\n }\n\n /**\n * Fix: Add missing getArchetypeModifier method\n */\n static getArchetypeModifier(archetype: PlayerArchetype): number {\n switch (archetype) {\n case PlayerArchetype.MUSA:\n return 1.2; // Traditional warrior - strong base damage\n case PlayerArchetype.AMSALJA:\n return 1.5; // Assassin - high damage modifier\n case PlayerArchetype.HACKER:\n return 1.1; // Cyber warrior - precision over power\n case PlayerArchetype.JEONGBO_YOWON:\n return 1.1; // Intelligence operative - strategic damage\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n return 1.3; // Organized crime - brutal effectiveness\n default:\n return 1.0;\n }\n }\n\n /**\n * Calculate technique damage with vital point consideration\n */\n static calculateTechniqueDamage(\n technique: KoreanTechnique,\n attacker: PlayerState,\n vitalPoint: VitalPoint | null,\n accuracy: number,\n ): DamageResult {\n // Fix: Remove unused baseDamage variable and use technique.damage directly\n const effects: StatusEffect[] = [];\n const archetypeModifier = DamageCalculator.getArchetypeModifier(\n attacker.archetype,\n );\n\n let finalDamage = (technique.damage ?? 15) * archetypeModifier * accuracy;\n\n // Apply vital point multiplier if hitting a vital point\n if (vitalPoint) {\n finalDamage *=\n (vitalPoint.baseDamage ?? vitalPoint.damage?.min ?? 10) / 10;\n\n // Add vital point effects\n vitalPoint.effects.forEach((effect) => {\n const statusEffect: StatusEffect = {\n id: `${effect.id}_${Date.now()}`,\n type: \"weakened\",\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- EffectIntensity type mapping from vital point effect\n intensity: \"moderate\" as any,\n duration: effect.duration,\n description: effect.description,\n stackable: effect.stackable,\n source: vitalPoint.id,\n startTime: Date.now(),\n endTime: Date.now() + effect.duration,\n };\n effects.push(statusEffect);\n });\n }\n\n return {\n damage: Math.max(1, Math.floor(finalDamage)),\n effects,\n isCritical: accuracy > 0.8,\n isVitalPoint: !!vitalPoint,\n };\n }\n\n /**\n * Calculate damage reduction from defense\n */\n static calculateDamageReduction(\n incomingDamage: number,\n defenderDefense: number,\n isBlocking: boolean = false,\n ): number {\n const blockMultiplier = isBlocking ? 0.5 : 1.0;\n const defenseReduction = Math.min(0.8, defenderDefense / 200); // Max 80% reduction\n\n return Math.max(\n 1,\n incomingDamage * (1 - defenseReduction) * blockMultiplier,\n );\n }\n\n /**\n * Calculate critical hit chance\n */\n static calculateCriticalChance(\n baseCritChance: number,\n attacker: PlayerState,\n technique: KoreanTechnique,\n ): number {\n const archetypeBonus =\n DamageCalculator.getArchetypeModifier(attacker.archetype) * 0.1;\n const techniqueBonus = (technique.critChance || 0.1) * 0.5;\n\n return Math.min(0.95, baseCritChance + archetypeBonus + techniqueBonus);\n }\n\n /**\n * Calculate comprehensive vital point damage with all modifiers.\n *\n * **Korean**: 종합 급소 피해 계산 (Comprehensive Vital Point Damage Calculation)\n *\n * Integrates all damage modifiers including:\n * - Base damage from attacker strength and technique power\n * - Stance effectiveness (攻克关系)\n * - Vital point severity multipliers (1.5x-3.0x)\n * - Accuracy bonus (0.8x-1.2x)\n * - Meridian flow bonus (1.0x-1.3x at peak hours)\n * - Time-of-day bonus (+20% for Dark Ops techniques at night)\n * - Archetype-specific bonuses\n * - Hip rotation power modifier (1.0x-1.3x for strikes with full rotation)\n * - Critical hit multiplier (2x when accuracy > 0.9)\n * - Damage variance (±10% randomness)\n * - Defense reduction\n *\n * ## Damage Formula\n *\n * ```typescript\n * finalDamage =\n * baseDamage ×\n * stanceEffectiveness ×\n * vitalPointMultiplier ×\n * accuracyBonus ×\n * meridianBonus ×\n * timeBonus ×\n * archetypeBonus ×\n * hipRotationModifier ×\n * criticalMultiplier ×\n * variance ×\n * defenseReduction\n * ```\n *\n * @param attacker - Attacking player state\n * @param defender - Defending player state\n * @param technique - Korean martial arts technique being used\n * @param vitalPointHit - Result of vital point hit detection\n * @param currentHour - Current hour of day (0-23) for meridian flow\n * @param meridianStates - Current meridian disruption states (0=blocked, 1=normal)\n * @param hipRotationAngle - Hip rotation angle in radians (optional, defaults to 0)\n * @param kickHipState - Hip rotation state for kicks (optional, for kick-specific power)\n * @returns Comprehensive damage result with all modifiers applied\n *\n * @example\n * ```typescript\n * const result = DamageCalculator.calculateEnhancedVitalPointDamage(\n * attackerState,\n * defenderState,\n * technique,\n * vitalPointHitResult,\n * 2, // 2 AM - liver meridian peak\n * { liver: 1.0, gallbladder: 0.8 }\n * );\n * console.log(`Final damage: ${result.damage}`);\n * console.log(`Critical hit: ${result.isCritical}`);\n * ```\n *\n * @korean 종합급소피해계산\n */\n static calculateEnhancedVitalPointDamage(\n attacker: PlayerState,\n defender: PlayerState,\n technique: KoreanTechnique,\n vitalPointHit: VitalPointHitResult,\n currentHour: number,\n meridianStates: Record<string, number>,\n hipRotationAngle: number = 0,\n kickHipState?: HipRotationState,\n ): DamageResult {\n // 1. Base damage from technique\n const attackerStrength = attacker.attackPower || 50;\n const techniquePower = technique.damage || 15;\n const baseDamage = attackerStrength * (techniquePower / 10);\n\n // 2. Stance effectiveness (攻克关系)\n const stanceEffectiveness = TrigramCalculator.calculateStanceEffectiveness(\n attacker.currentStance,\n defender.currentStance,\n );\n\n // 3. Vital point severity multiplier\n const vitalPointMultiplier = vitalPointHit.vitalPointHit\n ? (DamageCalculator.SEVERITY_MULTIPLIERS[vitalPointHit.severity] ?? 1)\n : 1;\n\n // 4. Accuracy bonus (better aim = more damage)\n // Maps 0.0-1.0 accuracy to 0.8x-1.2x multiplier\n const accuracy = vitalPointHit.accuracy ?? 0.5;\n const accuracyBonus =\n DamageCalculator.ACCURACY_MIN_MULTIPLIER +\n accuracy * DamageCalculator.ACCURACY_RANGE;\n\n // 5. Meridian flow bonus\n const meridianBonus = DamageCalculator.calculateMeridianDamageBonus(\n vitalPointHit.vitalPointHit?.id ?? \"\",\n currentHour,\n meridianStates,\n );\n\n // 6. Time-of-day bonus (Dark Ops techniques at night)\n const timeBonus = DamageCalculator.calculateTimeBonus(\n technique,\n currentHour,\n );\n\n // 7. Archetype-specific bonus\n const archetypeBonus = DamageCalculator.getArchetypeDamageBonus(\n attacker.archetype,\n technique,\n vitalPointHit.vitalPointHit,\n );\n\n // 8. Hip rotation power modifier (허리회전 파워 보너스)\n // Determine technique type for appropriate modifier calculation\n let hipRotationModifier = 1.0;\n\n // Check if this is a kick technique and we have kick-specific hip state\n const techniqueName =\n technique.name?.english || technique.englishName || \"\";\n const isKickTechnique =\n technique.type === \"kick\" || techniqueName.toLowerCase().includes(\"kick\");\n\n if (kickHipState && isKickTechnique) {\n // Use advanced kick-specific hip rotation calculation (up to 40% bonus)\n const kickType = DamageCalculator.determineKickType(techniqueName);\n hipRotationModifier = calculateKickPowerFromHipRotation(\n kickHipState,\n kickType,\n );\n } else if (hipRotationAngle !== 0) {\n // Use general hip rotation for strikes/throws/joints (up to 30% bonus)\n const techniqueType: \"strike\" | \"throw\" | \"joint\" =\n technique.type === \"throw\"\n ? \"throw\"\n : technique.type === \"joint_lock\"\n ? \"joint\"\n : \"strike\";\n hipRotationModifier = calculateHipRotationPowerModifier(\n hipRotationAngle,\n techniqueType,\n );\n }\n\n // 9. Critical hit (accuracy > 0.9)\n const criticalMultiplier = accuracy > 0.9 ? 2.0 : 1.0;\n const isCritical = accuracy > 0.9;\n\n // 10. Calculate total damage before defense\n let totalDamage =\n baseDamage *\n stanceEffectiveness *\n vitalPointMultiplier *\n accuracyBonus *\n meridianBonus *\n timeBonus *\n archetypeBonus *\n hipRotationModifier *\n criticalMultiplier;\n\n // 11. Add damage variance (±10%)\n const variance = 0.9 + Math.random() * 0.2; // 0.9 to 1.1\n totalDamage *= variance;\n\n // 12. Apply defense reduction using existing method for consistency\n const defenderDefense = defender.defense || 0;\n const finalDamage = DamageCalculator.calculateDamageReduction(\n totalDamage,\n defenderDefense,\n false,\n );\n\n // 13. Combine effects from vital point hit\n const effects = vitalPointHit.effects || [];\n\n return {\n damage: Math.max(1, finalDamage), // Minimum 1 damage\n effects,\n isCritical,\n isVitalPoint: !!vitalPointHit.vitalPointHit,\n };\n }\n\n /**\n * Determine kick type from technique name for hip rotation power calculation\n *\n * @param techniqueName - Name of the technique\n * @returns Kick type for hip rotation calculation\n *\n * @private\n * @korean 차기유형결정\n */\n private static determineKickType(techniqueName: string): KickType {\n const name = techniqueName.toLowerCase();\n\n if (\n name.includes(\"front\") ||\n name.includes(\"앞차기\") ||\n name.includes(\"snap\")\n ) {\n return \"front\";\n } else if (\n name.includes(\"roundhouse\") ||\n name.includes(\"돌려차기\") ||\n name.includes(\"round\")\n ) {\n return \"roundhouse\";\n } else if (\n name.includes(\"side\") ||\n name.includes(\"옆차기\") ||\n name.includes(\"lateral\")\n ) {\n return \"side\";\n } else if (name.includes(\"hook\") || name.includes(\"후려차기\")) {\n return \"hook\";\n } else if (\n name.includes(\"axe\") ||\n name.includes(\"내려차기\") ||\n name.includes(\"downward\")\n ) {\n return \"axe\";\n }\n\n // Default to front kick for unknown kick types\n return \"front\";\n }\n\n /**\n * Calculate meridian flow damage bonus based on time of day.\n *\n * **Korean**: 경락 유효성 피해 보너스 계산\n *\n * Calculates bonus damage when striking vital points during their peak\n * meridian flow hours. Peak flow provides +30% damage bonus.\n *\n * @param vitalPointId - ID of the vital point being struck\n * @param currentHour - Current hour of day (0-23)\n * @param meridianStates - Current meridian disruption states\n * @returns Damage multiplier (1.0-1.3)\n *\n * @private\n * @korean 경락유효성피해보너스계산\n */\n private static calculateMeridianDamageBonus(\n vitalPointId: string,\n currentHour: number,\n meridianStates: Record<string, number>,\n ): number {\n if (!vitalPointId) return 1;\n\n const relatedMeridians = getMeridiansForVitalPoint(vitalPointId);\n let maxBonus = 1.0;\n\n for (const meridianId of relatedMeridians) {\n const flow = calculateMeridianFlow(meridianId, currentHour);\n const state = meridianStates[meridianId] ?? 1.0;\n\n // Peak flow (>0.9) with normal state gives +30% bonus\n // flow ranges from 0.7 to 1.3, state from 0 (blocked) to 1 (normal)\n const effectiveFlow = flow * state;\n if (effectiveFlow > 0.9) {\n // Bonus scales linearly from 1.0 (no bonus) at effectiveFlow = 0.9\n // to 1.3 (maximum 30% bonus) at effectiveFlow = 1.3:\n // bonus = 1.0 + ((effectiveFlow - 0.9) / 0.4) * 0.3\n const bonus = 1.0 + ((effectiveFlow - 0.9) / 0.4) * 0.3;\n maxBonus = Math.max(maxBonus, Math.min(1.3, bonus));\n }\n }\n\n return maxBonus;\n }\n\n /**\n * Calculate time-of-day bonus for Dark Ops techniques.\n *\n * **Korean**: 시간대 보너스 계산\n *\n * Dark Ops techniques gain +20% damage at night (20:00-05:59)\n * to reflect tactical advantage of darkness.\n *\n * @param technique - Technique being used\n * @param currentHour - Current hour of day (0-23)\n * @returns Time bonus multiplier (1.0 or 1.2)\n *\n * @private\n * @korean 시간대보너스계산\n */\n private static calculateTimeBonus(\n technique: KoreanTechnique,\n currentHour: number,\n ): number {\n // Dark Ops techniques get +20% at night (20:00-05:59)\n // Check if technique has dark_ops or stealth in its ID or type\n const isDarkOpsTechnique =\n technique.id.includes(\"dark_ops\") ||\n technique.id.includes(\"stealth\") ||\n technique.id.includes(\"shadow\") ||\n technique.id.includes(\"night\");\n\n if (isDarkOpsTechnique) {\n // Night hours: 20:00 through 05:59 (6 AM is considered daylight)\n if (currentHour >= 20 || currentHour < 6) {\n return 1.2;\n }\n }\n return 1.0;\n }\n\n /**\n * Get archetype-specific damage bonus for technique and vital point.\n *\n * **Korean**: 원형 특화 피해 보너스\n *\n * Each archetype has specialized bonuses for different techniques\n * and vital point categories:\n *\n * - **무사 (Musa)**: +20% with 건 (Geon) stance techniques\n * - **암살자 (Amsalja)**: +30% on neurological vital points\n * - **해커 (Hacker)**: +15% baseline precision bonus\n * - **정보요원 (Jeongbo)**: +25% on vascular vital points\n * - **조직폭력배 (Jojik)**: +20% on dirty/ruthless techniques\n *\n * @param archetype - Attacker's archetype\n * @param technique - Technique being used\n * @param vitalPoint - Vital point being struck (optional)\n * @returns Archetype damage multiplier (1.0-1.5)\n *\n * @private\n * @korean 원형특화피해보너스\n */\n private static getArchetypeDamageBonus(\n archetype: PlayerArchetype,\n technique: KoreanTechnique,\n vitalPoint?: VitalPoint,\n ): number {\n // Base archetype modifier\n let bonus = DamageCalculator.getArchetypeModifier(archetype);\n\n // Additional bonuses based on technique-archetype synergy\n switch (archetype) {\n case PlayerArchetype.MUSA:\n // Musa gets bonus with Geon (Heaven) stance techniques\n if (technique.stance === TrigramStance.GEON) {\n bonus *= 1.2;\n }\n break;\n\n case PlayerArchetype.AMSALJA:\n // Assassin gets bonus on neurological vital points\n if (vitalPoint?.category === \"neurological\") {\n bonus *= 1.3;\n }\n break;\n\n case PlayerArchetype.HACKER:\n // Hacker gets consistent precision bonus\n bonus *= 1.15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n // Intelligence operative gets bonus on vascular targets\n if (vitalPoint?.category === \"vascular\") {\n bonus *= 1.25;\n }\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n // Organized crime gets bonus on ruthless techniques\n // Check for keywords in technique ID\n if (\n technique.id.includes(\"ruthless\") ||\n technique.id.includes(\"brutal\") ||\n technique.id.includes(\"dirty\")\n ) {\n bonus *= 1.2;\n }\n break;\n }\n\n // Cap maximum bonus at 1.5x\n return Math.min(1.5, bonus);\n }\n\n /**\n * Calculate throw/takedown impact damage with ground multiplier.\n *\n * **Korean**: 던지기 충격 피해 계산 (Throw Impact Damage Calculation)\n *\n * Applies groundImpactMultiplier from Gon (Earth) techniques when opponent\n * hits the ground after a throw or takedown. Integrates with base damage\n * calculation to provide authentic Ssireum/Hapkido throw mechanics.\n *\n * **Formula**:\n * ```\n * impactDamage = baseDamage × groundImpactMultiplier × strengthModifier\n * ```\n *\n * **Ground Impact Multiplier Ranges**:\n * - 1.0-1.2: Low impact (controlled takedowns)\n * - 1.3-1.5: Medium impact (standard throws)\n * - 1.6-1.8: High impact (power throws)\n * - 1.9-2.0: Maximum impact (slams like 대지강타)\n *\n * @param technique - Korean martial arts technique (checks for ExtendedGonTechnique)\n * @param baseDamage - Base damage before ground impact modifier\n * @param attackerStrength - Attacker's strength stat for scaling\n * @param options - Optional configuration for damage calculation\n * @param options.applyVariance - Whether to apply random ±5% variance (default: true)\n * @returns Enhanced damage result with ground impact applied\n *\n * @example\n * ```typescript\n * const throwDamage = DamageCalculator.calculateThrowImpactDamage(\n * ssireumThrowTechnique, // groundImpactMultiplier: 1.7\n * 50, // base damage\n * 80 // attacker strength\n * );\n * // Result: ~85 damage (50 × 1.7 × 1.0) with ±5% variance\n *\n * // For deterministic testing:\n * const testDamage = DamageCalculator.calculateThrowImpactDamage(\n * technique, 50, 80, { applyVariance: false }\n * );\n * ```\n *\n * @korean 던지기충격피해계산\n */\n static calculateThrowImpactDamage(\n technique: KoreanTechnique,\n baseDamage: number,\n attackerStrength: number = 50,\n options: { applyVariance?: boolean } = {},\n ): DamageResult {\n const { applyVariance = true } = options;\n // Check if technique has Gon-specific ground impact metadata\n // Use helper function for safe type casting\n const gonTechnique = asExtendedGonTechnique(technique);\n if (!gonTechnique) {\n // Non-Gon technique: return base damage without ground multiplier\n return {\n damage: Math.max(1, baseDamage),\n effects: [],\n isCritical: false,\n isVitalPoint: false,\n };\n }\n\n // Extract ground impact multiplier from validated Gon technique\n const groundMultiplier = gonTechnique.groundImpactMultiplier;\n\n // Calculate strength modifier (50 = baseline, scales ±20%)\n // Stronger attackers create more impact, weaker create less\n const strengthModifier = 0.8 + (attackerStrength / 250);\n\n // Apply ground impact multiplier with strength scaling\n let impactDamage = baseDamage * groundMultiplier * strengthModifier;\n\n // Add small damage variance (±5%) for realistic impact variation\n // Can be disabled for deterministic testing\n if (applyVariance) {\n const variance = 0.95 + Math.random() * 0.1; // 0.95 to 1.05\n impactDamage *= variance;\n }\n\n // Earth connection healing (대지는 모든 것을 품고 키운다)\n // Supportive healing is applied separately by calling system\n // This method focuses on damage calculation only\n\n return {\n damage: Math.max(1, Math.floor(impactDamage)),\n effects: [],\n isCritical: impactDamage > baseDamage * 1.5, // High impact = critical\n isVitalPoint: false,\n };\n }\n\n /**\n * Calculate earth's supportive healing from Gon technique.\n *\n * **Korean**: 대지 치유 계산 (Earth Healing Calculation)\n *\n * Implements Korean martial arts philosophy:\n * \"대지는 모든 것을 품고 키운다\" (The earth embraces and nurtures all things)\n *\n * Gon techniques restore HP to the attacker after successful execution\n * based on their connection with earth energy. Traditional techniques\n * like Ssireum throws provide stronger healing due to cultural authenticity.\n *\n * **Healing Formula**:\n * ```\n * healingAmount = supportiveHealing × (1 + earthAffinityBonus)\n * ```\n *\n * **Supportive Healing Ranges**:\n * - 0-2: Minimal earth connection (aggressive techniques)\n * - 3-4: Moderate earth connection (standard Ssireum)\n * - 5-6: Strong earth connection (traditional, sacrifice throws)\n * - 7-10: RESERVED for meditation/healing techniques\n *\n * @param technique - Korean martial arts technique (checks for ExtendedGonTechnique)\n * @param earthAffinityBonus - Player's earth affinity stat modifier (0.0-1.0)\n * @returns HP healing amount (0 if not a Gon technique)\n *\n * @example\n * ```typescript\n * const healing = DamageCalculator.calculateEarthHealing(\n * ssireumThrowTechnique, // supportiveHealing: 5\n * 0.3 // 30% earth affinity bonus\n * );\n * // Result: 6.5 HP restored (5 × 1.3)\n * ```\n *\n * @korean 대지치유계산\n */\n static calculateEarthHealing(\n technique: KoreanTechnique,\n earthAffinityBonus: number = 0,\n ): number {\n // Use helper function for safe type casting\n const gonTechnique = asExtendedGonTechnique(technique);\n if (!gonTechnique) {\n return 0; // Non-Gon technique: no earth healing\n }\n\n const baseHealing = gonTechnique.supportiveHealing;\n\n // Apply earth affinity bonus (typically 0-100% bonus)\n const affinityMultiplier = 1 + Math.max(0, Math.min(1, earthAffinityBonus));\n\n // Calculate final healing with bonus\n const healingAmount = baseHealing * affinityMultiplier;\n\n // Return rounded healing value (minimum 0)\n return Math.max(0, Math.floor(healingAmount));\n }\n}\n\nexport default DamageCalculator;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DamageCalculator.js","names":[],"sources":["../../../src/systems/vitalpoint/DamageCalculator.ts"],"sourcesContent":["import {\n PlayerArchetype,\n TrigramStance,\n VitalPointSeverity,\n} from \"../../types/common\";\nimport {\n calculateHipRotationPowerModifier,\n calculateKickPowerFromHipRotation,\n type HipRotationState,\n type KickType,\n} from \"../animation\";\nimport { PlayerState } from \"../player\";\nimport { TrigramCalculator } from \"../trigram/TrigramCalculator\";\nimport { asExtendedGonTechnique } from \"../trigram/types/GonTechniqueExtensions\";\nimport { StatusEffect } from \"../types\";\nimport { calculateMeridianFlow } from \"./KoreanAnatomy\";\nimport { getMeridiansForVitalPoint } from \"./MeridianVitalPointMapping\";\nimport {\n DamageResult,\n KoreanTechnique,\n VitalPoint,\n VitalPointHitResult,\n} from \"./types\";\n\nexport class DamageCalculator {\n /**\n * Vital point severity damage multipliers\n */\n private static readonly SEVERITY_MULTIPLIERS: Record<\n VitalPointSeverity,\n number\n > = {\n [VitalPointSeverity.MINOR]: 1.2,\n [VitalPointSeverity.MODERATE]: 1.5,\n [VitalPointSeverity.MAJOR]: 2.0,\n [VitalPointSeverity.CRITICAL]: 2.5,\n [VitalPointSeverity.LETHAL]: 3.0,\n };\n\n /**\n * Accuracy bonus scaling constants\n * Maps 0.0-1.0 accuracy to 0.8x-1.2x multiplier\n */\n private static readonly ACCURACY_MIN_MULTIPLIER = 0.8;\n private static readonly ACCURACY_RANGE = 0.4; // Range from min to max (1.2 - 0.8)\n\n /**\n * Calculate vital point damage with proper archetype bonuses\n */\n static calculateVitalPointDamage(\n vitalPoint: VitalPoint,\n baseDamage: number,\n attacker: PlayerState,\n accuracy: number,\n ): DamageResult {\n const effects: StatusEffect[] = [];\n\n // Fix: Use static method call instead of this\n const archetypeModifier = DamageCalculator.getArchetypeModifier(\n attacker.archetype,\n );\n const techniqueEffectiveness = accuracy;\n\n const finalDamage = Math.max(\n 1,\n (vitalPoint.baseDamage ?? vitalPoint.damage?.min ?? baseDamage) *\n archetypeModifier *\n techniqueEffectiveness,\n );\n\n // Create status effects from vital point\n vitalPoint.effects.forEach((effect) => {\n const statusEffect: StatusEffect = {\n id: `${effect.id}_${Date.now()}`,\n type: \"weakened\", // Map to valid EffectType\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- EffectIntensity type mapping from vital point effect\n intensity: \"moderate\" as any,\n duration: effect.duration,\n description: effect.description,\n stackable: effect.stackable,\n source: vitalPoint.id,\n startTime: Date.now(),\n endTime: Date.now() + effect.duration,\n };\n effects.push(statusEffect);\n });\n\n return {\n damage: finalDamage,\n effects,\n isCritical: accuracy > 0.9,\n isVitalPoint: true,\n };\n }\n\n /**\n * Fix: Add missing getArchetypeModifier method\n */\n static getArchetypeModifier(archetype: PlayerArchetype): number {\n switch (archetype) {\n case PlayerArchetype.MUSA:\n return 1.2; // Traditional warrior - strong base damage\n case PlayerArchetype.AMSALJA:\n return 1.5; // Assassin - high damage modifier\n case PlayerArchetype.HACKER:\n return 1.1; // Cyber warrior - precision over power\n case PlayerArchetype.JEONGBO_YOWON:\n return 1.1; // Intelligence operative - strategic damage\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n return 1.3; // Organized crime - brutal effectiveness\n default:\n return 1.0;\n }\n }\n\n /**\n * Calculate technique damage with vital point consideration\n */\n static calculateTechniqueDamage(\n technique: KoreanTechnique,\n attacker: PlayerState,\n vitalPoint: VitalPoint | null,\n accuracy: number,\n ): DamageResult {\n // Fix: Remove unused baseDamage variable and use technique.damage directly\n const effects: StatusEffect[] = [];\n const archetypeModifier = DamageCalculator.getArchetypeModifier(\n attacker.archetype,\n );\n\n let finalDamage = (technique.damage ?? 15) * archetypeModifier * accuracy;\n\n // Apply vital point multiplier if hitting a vital point\n if (vitalPoint) {\n finalDamage *=\n (vitalPoint.baseDamage ?? vitalPoint.damage?.min ?? 10) / 10;\n\n // Add vital point effects\n vitalPoint.effects.forEach((effect) => {\n const statusEffect: StatusEffect = {\n id: `${effect.id}_${Date.now()}`,\n type: \"weakened\",\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- EffectIntensity type mapping from vital point effect\n intensity: \"moderate\" as any,\n duration: effect.duration,\n description: effect.description,\n stackable: effect.stackable,\n source: vitalPoint.id,\n startTime: Date.now(),\n endTime: Date.now() + effect.duration,\n };\n effects.push(statusEffect);\n });\n }\n\n return {\n damage: Math.max(1, Math.floor(finalDamage)),\n effects,\n isCritical: accuracy > 0.8,\n isVitalPoint: !!vitalPoint,\n };\n }\n\n /**\n * Calculate damage reduction from defense\n */\n static calculateDamageReduction(\n incomingDamage: number,\n defenderDefense: number,\n isBlocking: boolean = false,\n ): number {\n const blockMultiplier = isBlocking ? 0.5 : 1.0;\n const defenseReduction = Math.min(0.8, defenderDefense / 200); // Max 80% reduction\n\n return Math.max(\n 1,\n incomingDamage * (1 - defenseReduction) * blockMultiplier,\n );\n }\n\n /**\n * Calculate critical hit chance\n */\n static calculateCriticalChance(\n baseCritChance: number,\n attacker: PlayerState,\n technique: KoreanTechnique,\n ): number {\n const archetypeBonus =\n DamageCalculator.getArchetypeModifier(attacker.archetype) * 0.1;\n const techniqueBonus = (technique.critChance || 0.1) * 0.5;\n\n return Math.min(0.95, baseCritChance + archetypeBonus + techniqueBonus);\n }\n\n /**\n * Calculate comprehensive vital point damage with all modifiers.\n *\n * **Korean**: 종합 급소 피해 계산 (Comprehensive Vital Point Damage Calculation)\n *\n * Integrates all damage modifiers including:\n * - Base damage from attacker strength and technique power\n * - Stance effectiveness (攻克关系)\n * - Vital point severity multipliers (1.5x-3.0x)\n * - Accuracy bonus (0.8x-1.2x)\n * - Meridian flow bonus (1.0x-1.3x at peak hours)\n * - Time-of-day bonus (+20% for Dark Ops techniques at night)\n * - Archetype-specific bonuses\n * - Hip rotation power modifier (1.0x-1.3x for strikes with full rotation)\n * - Critical hit multiplier (2x when accuracy > 0.9)\n * - Damage variance (±10% randomness)\n * - Defense reduction\n *\n * ## Damage Formula\n *\n * ```typescript\n * finalDamage =\n * baseDamage ×\n * stanceEffectiveness ×\n * vitalPointMultiplier ×\n * accuracyBonus ×\n * meridianBonus ×\n * timeBonus ×\n * archetypeBonus ×\n * hipRotationModifier ×\n * criticalMultiplier ×\n * variance ×\n * defenseReduction\n * ```\n *\n * @param attacker - Attacking player state\n * @param defender - Defending player state\n * @param technique - Korean martial arts technique being used\n * @param vitalPointHit - Result of vital point hit detection\n * @param currentHour - Current hour of day (0-23) for meridian flow\n * @param meridianStates - Current meridian disruption states (0=blocked, 1=normal)\n * @param hipRotationAngle - Hip rotation angle in radians (optional, defaults to 0)\n * @param kickHipState - Hip rotation state for kicks (optional, for kick-specific power)\n * @returns Comprehensive damage result with all modifiers applied\n *\n * @example\n * ```typescript\n * const result = DamageCalculator.calculateEnhancedVitalPointDamage(\n * attackerState,\n * defenderState,\n * technique,\n * vitalPointHitResult,\n * 2, // 2 AM - liver meridian peak\n * { liver: 1.0, gallbladder: 0.8 }\n * );\n * console.log(`Final damage: ${result.damage}`);\n * console.log(`Critical hit: ${result.isCritical}`);\n * ```\n *\n * @korean 종합급소피해계산\n */\n static calculateEnhancedVitalPointDamage(\n attacker: PlayerState,\n defender: PlayerState,\n technique: KoreanTechnique,\n vitalPointHit: VitalPointHitResult,\n currentHour: number,\n meridianStates: Record<string, number>,\n hipRotationAngle: number = 0,\n kickHipState?: HipRotationState,\n ): DamageResult {\n // 1. Base damage from technique\n const attackerStrength = attacker.attackPower || 50;\n const techniquePower = technique.damage || 15;\n const baseDamage = attackerStrength * (techniquePower / 10);\n\n // 2. Stance effectiveness (攻克关系)\n const stanceEffectiveness = TrigramCalculator.calculateStanceEffectiveness(\n attacker.currentStance,\n defender.currentStance,\n );\n\n // 3. Vital point severity multiplier\n const vitalPointMultiplier = vitalPointHit.vitalPointHit\n ? (DamageCalculator.SEVERITY_MULTIPLIERS[vitalPointHit.severity] ?? 1)\n : 1;\n\n // 4. Accuracy bonus (better aim = more damage)\n // Maps 0.0-1.0 accuracy to 0.8x-1.2x multiplier\n const accuracy = vitalPointHit.accuracy ?? 0.5;\n const accuracyBonus =\n DamageCalculator.ACCURACY_MIN_MULTIPLIER +\n accuracy * DamageCalculator.ACCURACY_RANGE;\n\n // 5. Meridian flow bonus\n const meridianBonus = DamageCalculator.calculateMeridianDamageBonus(\n vitalPointHit.vitalPointHit?.id ?? \"\",\n currentHour,\n meridianStates,\n );\n\n // 6. Time-of-day bonus (Dark Ops techniques at night)\n const timeBonus = DamageCalculator.calculateTimeBonus(\n technique,\n currentHour,\n );\n\n // 7. Archetype-specific bonus\n const archetypeBonus = DamageCalculator.getArchetypeDamageBonus(\n attacker.archetype,\n technique,\n vitalPointHit.vitalPointHit,\n );\n\n // 8. Hip rotation power modifier (허리회전 파워 보너스)\n // Determine technique type for appropriate modifier calculation\n let hipRotationModifier = 1.0;\n\n // Check if this is a kick technique and we have kick-specific hip state\n const techniqueName =\n technique.name?.english || technique.englishName || \"\";\n const isKickTechnique =\n technique.type === \"kick\" || techniqueName.toLowerCase().includes(\"kick\");\n\n if (kickHipState && isKickTechnique) {\n // Use advanced kick-specific hip rotation calculation (up to 40% bonus)\n const kickType = DamageCalculator.determineKickType(techniqueName);\n hipRotationModifier = calculateKickPowerFromHipRotation(\n kickHipState,\n kickType,\n );\n } else if (hipRotationAngle !== 0) {\n // Use general hip rotation for strikes/throws/joints (up to 30% bonus)\n const techniqueType: \"strike\" | \"throw\" | \"joint\" =\n technique.type === \"throw\"\n ? \"throw\"\n : technique.type === \"joint_lock\"\n ? \"joint\"\n : \"strike\";\n hipRotationModifier = calculateHipRotationPowerModifier(\n hipRotationAngle,\n techniqueType,\n );\n }\n\n // 9. Critical hit (accuracy > 0.9)\n const criticalMultiplier = accuracy > 0.9 ? 2.0 : 1.0;\n const isCritical = accuracy > 0.9;\n\n // 10. Calculate total damage before defense\n let totalDamage =\n baseDamage *\n stanceEffectiveness *\n vitalPointMultiplier *\n accuracyBonus *\n meridianBonus *\n timeBonus *\n archetypeBonus *\n hipRotationModifier *\n criticalMultiplier;\n\n // 11. Add damage variance (±10%)\n const variance = 0.9 + Math.random() * 0.2; // 0.9 to 1.1\n totalDamage *= variance;\n\n // 12. Apply defense reduction using existing method for consistency\n const defenderDefense = defender.defense || 0;\n const finalDamage = DamageCalculator.calculateDamageReduction(\n totalDamage,\n defenderDefense,\n false,\n );\n\n // 13. Combine effects from vital point hit\n const effects = vitalPointHit.effects || [];\n\n return {\n damage: Math.max(1, finalDamage), // Minimum 1 damage\n effects,\n isCritical,\n isVitalPoint: !!vitalPointHit.vitalPointHit,\n };\n }\n\n /**\n * Determine kick type from technique name for hip rotation power calculation\n *\n * @param techniqueName - Name of the technique\n * @returns Kick type for hip rotation calculation\n *\n * @private\n * @korean 차기유형결정\n */\n private static determineKickType(techniqueName: string): KickType {\n const name = techniqueName.toLowerCase();\n\n if (\n name.includes(\"front\") ||\n name.includes(\"앞차기\") ||\n name.includes(\"snap\")\n ) {\n return \"front\";\n } else if (\n name.includes(\"roundhouse\") ||\n name.includes(\"돌려차기\") ||\n name.includes(\"round\")\n ) {\n return \"roundhouse\";\n } else if (\n name.includes(\"side\") ||\n name.includes(\"옆차기\") ||\n name.includes(\"lateral\")\n ) {\n return \"side\";\n } else if (name.includes(\"hook\") || name.includes(\"후려차기\")) {\n return \"hook\";\n } else if (\n name.includes(\"axe\") ||\n name.includes(\"내려차기\") ||\n name.includes(\"downward\")\n ) {\n return \"axe\";\n }\n\n // Default to front kick for unknown kick types\n return \"front\";\n }\n\n /**\n * Calculate meridian flow damage bonus based on time of day.\n *\n * **Korean**: 경락 유효성 피해 보너스 계산\n *\n * Calculates bonus damage when striking vital points during their peak\n * meridian flow hours. Peak flow provides +30% damage bonus.\n *\n * @param vitalPointId - ID of the vital point being struck\n * @param currentHour - Current hour of day (0-23)\n * @param meridianStates - Current meridian disruption states\n * @returns Damage multiplier (1.0-1.3)\n *\n * @private\n * @korean 경락유효성피해보너스계산\n */\n private static calculateMeridianDamageBonus(\n vitalPointId: string,\n currentHour: number,\n meridianStates: Record<string, number>,\n ): number {\n if (!vitalPointId) return 1;\n\n const relatedMeridians = getMeridiansForVitalPoint(vitalPointId);\n let maxBonus = 1.0;\n\n for (const meridianId of relatedMeridians) {\n const flow = calculateMeridianFlow(meridianId, currentHour);\n const state = meridianStates[meridianId] ?? 1.0;\n\n // Peak flow (>0.9) with normal state gives +30% bonus\n // flow ranges from 0.7 to 1.3, state from 0 (blocked) to 1 (normal)\n const effectiveFlow = flow * state;\n if (effectiveFlow > 0.9) {\n // Bonus scales linearly from 1.0 (no bonus) at effectiveFlow = 0.9\n // to 1.3 (maximum 30% bonus) at effectiveFlow = 1.3:\n // bonus = 1.0 + ((effectiveFlow - 0.9) / 0.4) * 0.3\n const bonus = 1.0 + ((effectiveFlow - 0.9) / 0.4) * 0.3;\n maxBonus = Math.max(maxBonus, Math.min(1.3, bonus));\n }\n }\n\n return maxBonus;\n }\n\n /**\n * Calculate time-of-day bonus for Dark Ops techniques.\n *\n * **Korean**: 시간대 보너스 계산\n *\n * Dark Ops techniques gain +20% damage at night (20:00-05:59)\n * to reflect tactical advantage of darkness.\n *\n * @param technique - Technique being used\n * @param currentHour - Current hour of day (0-23)\n * @returns Time bonus multiplier (1.0 or 1.2)\n *\n * @private\n * @korean 시간대보너스계산\n */\n private static calculateTimeBonus(\n technique: KoreanTechnique,\n currentHour: number,\n ): number {\n // Dark Ops techniques get +20% at night (20:00-05:59)\n // Check if technique has dark_ops or stealth in its ID or type\n const isDarkOpsTechnique =\n technique.id.includes(\"dark_ops\") ||\n technique.id.includes(\"stealth\") ||\n technique.id.includes(\"shadow\") ||\n technique.id.includes(\"night\");\n\n if (isDarkOpsTechnique) {\n // Night hours: 20:00 through 05:59 (6 AM is considered daylight)\n if (currentHour >= 20 || currentHour < 6) {\n return 1.2;\n }\n }\n return 1.0;\n }\n\n /**\n * Get archetype-specific damage bonus for technique and vital point.\n *\n * **Korean**: 원형 특화 피해 보너스\n *\n * Each archetype has specialized bonuses for different techniques\n * and vital point categories:\n *\n * - **무사 (Musa)**: +20% with 건 (Geon) stance techniques\n * - **암살자 (Amsalja)**: +30% on neurological vital points\n * - **해커 (Hacker)**: +15% baseline precision bonus\n * - **정보요원 (Jeongbo)**: +25% on vascular vital points\n * - **조직폭력배 (Jojik)**: +20% on dirty/ruthless techniques\n *\n * @param archetype - Attacker's archetype\n * @param technique - Technique being used\n * @param vitalPoint - Vital point being struck (optional)\n * @returns Archetype damage multiplier (1.0-1.5)\n *\n * @private\n * @korean 원형특화피해보너스\n */\n private static getArchetypeDamageBonus(\n archetype: PlayerArchetype,\n technique: KoreanTechnique,\n vitalPoint?: VitalPoint,\n ): number {\n // Base archetype modifier\n let bonus = DamageCalculator.getArchetypeModifier(archetype);\n\n // Additional bonuses based on technique-archetype synergy\n switch (archetype) {\n case PlayerArchetype.MUSA:\n // Musa gets bonus with Geon (Heaven) stance techniques\n if (technique.stance === TrigramStance.GEON) {\n bonus *= 1.2;\n }\n break;\n\n case PlayerArchetype.AMSALJA:\n // Assassin gets bonus on neurological vital points\n if (vitalPoint?.category === \"neurological\") {\n bonus *= 1.3;\n }\n break;\n\n case PlayerArchetype.HACKER:\n // Hacker gets consistent precision bonus\n bonus *= 1.15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n // Intelligence operative gets bonus on vascular targets\n if (vitalPoint?.category === \"vascular\") {\n bonus *= 1.25;\n }\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n // Organized crime gets bonus on ruthless techniques\n // Check for keywords in technique ID\n if (\n technique.id.includes(\"ruthless\") ||\n technique.id.includes(\"brutal\") ||\n technique.id.includes(\"dirty\")\n ) {\n bonus *= 1.2;\n }\n break;\n }\n\n // Cap maximum bonus at 1.5x\n return Math.min(1.5, bonus);\n }\n\n /**\n * Calculate throw/takedown impact damage with ground multiplier.\n *\n * **Korean**: 던지기 충격 피해 계산 (Throw Impact Damage Calculation)\n *\n * Applies groundImpactMultiplier from Gon (Earth) techniques when opponent\n * hits the ground after a throw or takedown. Integrates with base damage\n * calculation to provide authentic Ssireum/Hapkido throw mechanics.\n *\n * **Formula**:\n * ```\n * impactDamage = baseDamage × groundImpactMultiplier × strengthModifier\n * ```\n *\n * **Ground Impact Multiplier Ranges**:\n * - 1.0-1.2: Low impact (controlled takedowns)\n * - 1.3-1.5: Medium impact (standard throws)\n * - 1.6-1.8: High impact (power throws)\n * - 1.9-2.0: Maximum impact (slams like 대지강타)\n *\n * @param technique - Korean martial arts technique (checks for ExtendedGonTechnique)\n * @param baseDamage - Base damage before ground impact modifier\n * @param attackerStrength - Attacker's strength stat for scaling\n * @param options - Optional configuration for damage calculation\n * @param options.applyVariance - Whether to apply random ±5% variance (default: true)\n * @returns Enhanced damage result with ground impact applied\n *\n * @example\n * ```typescript\n * const throwDamage = DamageCalculator.calculateThrowImpactDamage(\n * ssireumThrowTechnique, // groundImpactMultiplier: 1.7\n * 50, // base damage\n * 80 // attacker strength\n * );\n * // Result: ~85 damage (50 × 1.7 × 1.0) with ±5% variance\n *\n * // For deterministic testing:\n * const testDamage = DamageCalculator.calculateThrowImpactDamage(\n * technique, 50, 80, { applyVariance: false }\n * );\n * ```\n *\n * @korean 던지기충격피해계산\n */\n static calculateThrowImpactDamage(\n technique: KoreanTechnique,\n baseDamage: number,\n attackerStrength: number = 50,\n options: { applyVariance?: boolean } = {},\n ): DamageResult {\n const { applyVariance = true } = options;\n // Check if technique has Gon-specific ground impact metadata\n // Use helper function for safe type casting\n const gonTechnique = asExtendedGonTechnique(technique);\n if (!gonTechnique) {\n // Non-Gon technique: return base damage without ground multiplier\n return {\n damage: Math.max(1, baseDamage),\n effects: [],\n isCritical: false,\n isVitalPoint: false,\n };\n }\n\n // Extract ground impact multiplier from validated Gon technique\n const groundMultiplier = gonTechnique.groundImpactMultiplier;\n\n // Calculate strength modifier (50 = baseline, scales ±20%)\n // Stronger attackers create more impact, weaker create less\n const strengthModifier = 0.8 + (attackerStrength / 250);\n\n // Apply ground impact multiplier with strength scaling\n let impactDamage = baseDamage * groundMultiplier * strengthModifier;\n\n // Add small damage variance (±5%) for realistic impact variation\n // Can be disabled for deterministic testing\n if (applyVariance) {\n const variance = 0.95 + Math.random() * 0.1; // 0.95 to 1.05\n impactDamage *= variance;\n }\n\n // Earth connection healing (대지는 모든 것을 품고 키운다)\n // Supportive healing is applied separately by calling system\n // This method focuses on damage calculation only\n\n return {\n damage: Math.max(1, Math.floor(impactDamage)),\n effects: [],\n isCritical: impactDamage > baseDamage * 1.5, // High impact = critical\n isVitalPoint: false,\n };\n }\n\n /**\n * Calculate earth's supportive healing from Gon technique.\n *\n * **Korean**: 대지 치유 계산 (Earth Healing Calculation)\n *\n * Implements Korean martial arts philosophy:\n * \"대지는 모든 것을 품고 키운다\" (The earth embraces and nurtures all things)\n *\n * Gon techniques restore HP to the attacker after successful execution\n * based on their connection with earth energy. Traditional techniques\n * like Ssireum throws provide stronger healing due to cultural authenticity.\n *\n * **Healing Formula**:\n * ```\n * healingAmount = supportiveHealing × (1 + earthAffinityBonus)\n * ```\n *\n * **Supportive Healing Ranges**:\n * - 0-2: Minimal earth connection (aggressive techniques)\n * - 3-4: Moderate earth connection (standard Ssireum)\n * - 5-6: Strong earth connection (traditional, sacrifice throws)\n * - 7-10: RESERVED for meditation/healing techniques\n *\n * @param technique - Korean martial arts technique (checks for ExtendedGonTechnique)\n * @param earthAffinityBonus - Player's earth affinity stat modifier (0.0-1.0)\n * @returns HP healing amount (0 if not a Gon technique)\n *\n * @example\n * ```typescript\n * const healing = DamageCalculator.calculateEarthHealing(\n * ssireumThrowTechnique, // supportiveHealing: 5\n * 0.3 // 30% earth affinity bonus\n * );\n * // Result: 6.5 HP restored (5 × 1.3)\n * ```\n *\n * @korean 대지치유계산\n */\n static calculateEarthHealing(\n technique: KoreanTechnique,\n earthAffinityBonus: number = 0,\n ): number {\n // Use helper function for safe type casting\n const gonTechnique = asExtendedGonTechnique(technique);\n if (!gonTechnique) {\n return 0; // Non-Gon technique: no earth healing\n }\n\n const baseHealing = gonTechnique.supportiveHealing;\n\n // Apply earth affinity bonus (typically 0-100% bonus)\n const affinityMultiplier = 1 + Math.max(0, Math.min(1, earthAffinityBonus));\n\n // Calculate final healing with bonus\n const healingAmount = baseHealing * affinityMultiplier;\n\n // Return rounded healing value (minimum 0)\n return Math.max(0, Math.floor(healingAmount));\n }\n}\n\nexport default DamageCalculator;\n"],"mappings":"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"}
|
package/lib/types/facial.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facial.js","names":[],"sources":["../../src/types/facial.ts"],"sourcesContent":["/**\n * Facial animation types for realistic combat emotion and damage\n * \n * Defines facial expression system with damage tracking, eye tracking,\n * and smooth expression transitions for authentic human-like combat feedback.\n * \n * @module types/facial\n * @category Type Definitions\n * @korean 얼굴애니메이션타입\n */\n\nimport * as THREE from \"three\";\n\n/**\n * Facial expression states for combat\n * \n * Represents emotional and physical states visible on fighter's face.\n * Each expression corresponds to specific combat situations.\n * \n * @category Facial Animation\n * @korean 얼굴표정\n */\nexport enum FacialExpression {\n /** Calm, ready state */\n NEUTRAL = \"neutral\",\n /** Concentrated, ready to attack */\n FOCUSED = \"focused\",\n /** Pain response after hit */\n PAINED = \"pained\",\n /** Low stamina, heavy breathing */\n EXHAUSTED = \"exhausted\",\n /** Brief satisfaction after successful strike */\n VICTORIOUS = \"victorious\",\n /** Knocked out, unconscious */\n DEFEATED = \"defeated\",\n}\n\n/**\n * Bilingual names for facial expressions\n * \n * @category Facial Animation\n * @korean 표정이름\n */\nexport const FACIAL_EXPRESSION_NAMES: Record<\n FacialExpression,\n { korean: string; english: string; romanized: string }\n> = {\n [FacialExpression.NEUTRAL]: {\n korean: \"평온\",\n english: \"Neutral\",\n romanized: \"pyeong-on\",\n },\n [FacialExpression.FOCUSED]: {\n korean: \"집중\",\n english: \"Focused\",\n romanized: \"jipjung\",\n },\n [FacialExpression.PAINED]: {\n korean: \"고통\",\n english: \"Pained\",\n romanized: \"gotong\",\n },\n [FacialExpression.EXHAUSTED]: {\n korean: \"지침\",\n english: \"Exhausted\",\n romanized: \"jichim\",\n },\n [FacialExpression.VICTORIOUS]: {\n korean: \"승리\",\n english: \"Victorious\",\n romanized: \"seungri\",\n },\n [FacialExpression.DEFEATED]: {\n korean: \"패배\",\n english: \"Defeated\",\n romanized: \"paebae\",\n },\n};\n\n/**\n * Facial damage state tracking\n * \n * Tracks accumulated damage to face for visual feedback.\n * Includes bruising, swelling, and bleeding effects.\n * \n * @category Facial Animation\n * @korean 얼굴손상상태\n */\nexport interface FacialDamageState {\n /** Left eye swelling (0-1, 0=none, 1=fully swollen) */\n readonly leftEyeSwelling: number;\n /** Right eye swelling (0-1, 0=none, 1=fully swollen) */\n readonly rightEyeSwelling: number;\n /** Mouth/lip bleeding intensity (0-1) */\n readonly mouthBleeding: number;\n /** Nose bleeding intensity (0-1) */\n readonly noseBleeding: number;\n /** Bruise intensity on left cheek (0-1) */\n readonly leftCheekBruise: number;\n /** Bruise intensity on right cheek (0-1) */\n readonly rightCheekBruise: number;\n /** Forehead bruise/cut intensity (0-1) */\n readonly foreheadBruise: number;\n /** Jaw bruise intensity (0-1) */\n readonly jawBruise: number;\n /** Total facial damage accumulation (0-100) */\n readonly totalFacialDamage: number;\n}\n\n/**\n * Expression state with transition timing\n * \n * Manages current expression and smooth transition to next expression.\n * \n * @category Facial Animation\n * @korean 표정상태\n */\nexport interface ExpressionState {\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Expression intensity (0-1, affects degree of expression) */\n readonly intensity: number;\n /** Time to transition to new expression (seconds) */\n readonly transitionTime: number;\n /** Previous expression (for blending) */\n readonly previousExpression?: FacialExpression;\n /** Transition progress (0-1, 0=start, 1=complete) */\n readonly transitionProgress?: number;\n}\n\n/**\n * Eye openness values for expressions\n * \n * @category Facial Animation\n * @korean 눈개방도\n */\nexport const EYE_OPENNESS: Record<FacialExpression, number> = {\n [FacialExpression.NEUTRAL]: 1.0, // Fully open\n [FacialExpression.FOCUSED]: 0.7, // Narrowed\n [FacialExpression.PAINED]: 0.3, // Squinted\n [FacialExpression.EXHAUSTED]: 0.5, // Half-closed\n [FacialExpression.VICTORIOUS]: 0.9, // Nearly fully open\n [FacialExpression.DEFEATED]: 0.0, // Closed\n};\n\n/**\n * Mouth openness values for expressions\n * \n * @category Facial Animation\n * @korean 입개방도\n */\nexport const MOUTH_OPENNESS: Record<FacialExpression, number> = {\n [FacialExpression.NEUTRAL]: 0.0, // Closed\n [FacialExpression.FOCUSED]: 0.0, // Closed, determined\n [FacialExpression.PAINED]: 0.8, // Open, yelling\n [FacialExpression.EXHAUSTED]: 0.4, // Panting\n [FacialExpression.VICTORIOUS]: 0.2, // Slight smile\n [FacialExpression.DEFEATED]: 0.1, // Slack\n};\n\n/**\n * Head movement animation type\n * \n * Defines types of head movements for combat reactions.\n * \n * @category Facial Animation\n * @korean 머리움직임타입\n */\nexport enum HeadMovementType {\n /** Head snaps back when hit */\n RECOIL = \"recoil\",\n /** Slight nod forward during attack */\n NOD = \"nod\",\n /** Head tilts to avoid strike */\n TILT = \"tilt\",\n /** Head turns to track opponent */\n TURN = \"turn\",\n /** Head shakes when stunned */\n SHAKE = \"shake\",\n /** Head drops when defeated */\n DROP = \"drop\",\n}\n\n/**\n * Head movement animation keyframes\n * \n * Sequence of Euler rotations for head movement animations.\n * \n * @category Facial Animation\n * @korean 머리움직임키프레임\n */\nexport interface HeadMovementKeyframes {\n /** Movement type identifier */\n readonly type: HeadMovementType;\n /** Sequence of rotation keyframes */\n readonly rotations: readonly THREE.Euler[];\n /** Duration of each keyframe in seconds */\n readonly frameDuration: number;\n /** Total animation duration in seconds */\n readonly totalDuration: number;\n /** Whether animation loops */\n readonly loop: boolean;\n}\n\n/**\n * Eye tracking state\n * \n * Manages eye direction and pupil position for opponent tracking.\n * \n * @category Facial Animation\n * @korean 눈추적상태\n */\nexport interface EyeTrackingState {\n /** Target position to look at (opponent position) */\n readonly targetPosition: THREE.Vector3;\n /** Current look direction (normalized) */\n readonly lookDirection: THREE.Vector3;\n /** Pupil offset from center (-1 to 1 for each axis) */\n readonly pupilOffset: { x: number; y: number };\n /** Tracking speed (smoothing factor) */\n readonly trackingSpeed: number;\n /** Whether tracking is enabled */\n readonly enabled: boolean;\n}\n\n/**\n * Default facial damage state (no damage)\n * \n * @category Facial Animation\n * @korean 기본얼굴손상상태\n */\nexport const DEFAULT_FACIAL_DAMAGE: FacialDamageState = {\n leftEyeSwelling: 0,\n rightEyeSwelling: 0,\n mouthBleeding: 0,\n noseBleeding: 0,\n leftCheekBruise: 0,\n rightCheekBruise: 0,\n foreheadBruise: 0,\n jawBruise: 0,\n totalFacialDamage: 0,\n};\n\n/**\n * Default expression state (neutral)\n * \n * @category Facial Animation\n * @korean 기본표정상태\n */\nexport const DEFAULT_EXPRESSION_STATE: ExpressionState = {\n expression: FacialExpression.NEUTRAL,\n intensity: 1.0,\n transitionTime: 0.2,\n};\n\n/**\n * Default eye tracking state\n * \n * @category Facial Animation\n * @korean 기본눈추적상태\n */\nexport const DEFAULT_EYE_TRACKING: EyeTrackingState = {\n targetPosition: new THREE.Vector3(0, 1.9, 5), // Looking forward\n lookDirection: new THREE.Vector3(0, 0, 1), // Forward\n pupilOffset: { x: 0, y: 0 },\n trackingSpeed: 0.1,\n enabled: true,\n};\n\n/**\n * Face3D component props\n * \n * @category Facial Animation\n * @korean 얼굴3D속성\n */\nexport interface Face3DProps {\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Facial damage state */\n readonly damage: FacialDamageState;\n /** Opponent position for eye tracking */\n readonly opponentPosition: THREE.Vector3;\n /** Current head rotation (Euler angles) */\n readonly headRotation: THREE.Euler;\n /** Enable eye tracking (default: true) */\n readonly enableEyeTracking?: boolean;\n /** Enable damage visualization (default: true) */\n readonly enableDamageVisuals?: boolean;\n /** Mobile mode (simplified rendering) */\n readonly isMobile?: boolean;\n /** Skin tone color (default: 0xffdbac) */\n readonly skinColor?: number;\n}\n\n/**\n * Eye component props\n * \n * @category Facial Animation\n * @korean 눈3D속성\n */\nexport interface EyeProps {\n /** Eye position relative to head */\n readonly position: [number, number, number];\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Look direction for pupil tracking */\n readonly lookDirection: THREE.Vector3;\n /** Eye swelling amount (0-1) */\n readonly swelling: number;\n /** Whether this is the left or right eye */\n readonly side: \"left\" | \"right\";\n}\n\n/**\n * Mouth component props\n * \n * @category Facial Animation\n * @korean 입3D속성\n */\nexport interface MouthProps {\n /** Mouth position relative to head */\n readonly position: [number, number, number];\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Bleeding intensity (0-1) */\n readonly bleeding: number;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,IAAY,mBAAL,yBAAA,kBAAA;;CAEL,iBAAA,aAAA;;CAEA,iBAAA,aAAA;;CAEA,iBAAA,YAAA;;CAEA,iBAAA,eAAA;;CAEA,iBAAA,gBAAA;;CAEA,iBAAA,cAAA;;AACF,EAAA,CAAA,CAAA;;;;;;;AAQA,IAAa,0BAGT;cAC0B;EAC1B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;cAC4B;EAC1B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;aAC2B;EACzB,QAAQ;EACR,SAAS;EACT,WAAW;CACb;gBAC8B;EAC5B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;iBAC+B;EAC7B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;eAC6B;EAC3B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;AACF;;;;;;;AA2DA,IAAa,eAAiD;cAChC;cACA;aACD;gBACG;iBACC;eACF;AAC/B;;;;;;;AAQA,IAAa,iBAAmD;cAClC;cACA;aACD;gBACG;iBACC;eACF;AAC/B;;;;;;;;;AAUA,IAAY,mBAAL,yBAAA,kBAAA;;CAEL,iBAAA,YAAA;;CAEA,iBAAA,SAAA;;CAEA,iBAAA,UAAA;;CAEA,iBAAA,UAAA;;CAEA,iBAAA,WAAA;;CAEA,iBAAA,UAAA;;AACF,EAAA,CAAA,CAAA;;;;;;;AAkDA,IAAa,wBAA2C;CACtD,iBAAiB;CACjB,kBAAkB;CAClB,eAAe;CACf,cAAc;CACd,iBAAiB;CACjB,kBAAkB;CAClB,gBAAgB;CAChB,WAAW;CACX,mBAAmB;AACrB;;;;;;;AAQA,IAAa,2BAA4C;CACvD,YAAA;CACA,WAAW;CACX,gBAAgB;AAClB;;;;;;;AAQA,IAAa,uBAAyC;CACpD,gBAAgB,IAAI,MAAM,QAAQ,GAAG,KAAK,CAAC;CAC3C,eAAe,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;CACxC,aAAa;EAAE,GAAG;EAAG,GAAG;CAAE;CAC1B,eAAe;CACf,SAAS;AACX"}
|
|
1
|
+
{"version":3,"file":"facial.js","names":[],"sources":["../../src/types/facial.ts"],"sourcesContent":["/**\n * Facial animation types for realistic combat emotion and damage\n * \n * Defines facial expression system with damage tracking, eye tracking,\n * and smooth expression transitions for authentic human-like combat feedback.\n * \n * @module types/facial\n * @category Type Definitions\n * @korean 얼굴애니메이션타입\n */\n\nimport * as THREE from \"three\";\n\n/**\n * Facial expression states for combat\n * \n * Represents emotional and physical states visible on fighter's face.\n * Each expression corresponds to specific combat situations.\n * \n * @category Facial Animation\n * @korean 얼굴표정\n */\nexport enum FacialExpression {\n /** Calm, ready state */\n NEUTRAL = \"neutral\",\n /** Concentrated, ready to attack */\n FOCUSED = \"focused\",\n /** Pain response after hit */\n PAINED = \"pained\",\n /** Low stamina, heavy breathing */\n EXHAUSTED = \"exhausted\",\n /** Brief satisfaction after successful strike */\n VICTORIOUS = \"victorious\",\n /** Knocked out, unconscious */\n DEFEATED = \"defeated\",\n}\n\n/**\n * Bilingual names for facial expressions\n * \n * @category Facial Animation\n * @korean 표정이름\n */\nexport const FACIAL_EXPRESSION_NAMES: Record<\n FacialExpression,\n { korean: string; english: string; romanized: string }\n> = {\n [FacialExpression.NEUTRAL]: {\n korean: \"평온\",\n english: \"Neutral\",\n romanized: \"pyeong-on\",\n },\n [FacialExpression.FOCUSED]: {\n korean: \"집중\",\n english: \"Focused\",\n romanized: \"jipjung\",\n },\n [FacialExpression.PAINED]: {\n korean: \"고통\",\n english: \"Pained\",\n romanized: \"gotong\",\n },\n [FacialExpression.EXHAUSTED]: {\n korean: \"지침\",\n english: \"Exhausted\",\n romanized: \"jichim\",\n },\n [FacialExpression.VICTORIOUS]: {\n korean: \"승리\",\n english: \"Victorious\",\n romanized: \"seungri\",\n },\n [FacialExpression.DEFEATED]: {\n korean: \"패배\",\n english: \"Defeated\",\n romanized: \"paebae\",\n },\n};\n\n/**\n * Facial damage state tracking\n * \n * Tracks accumulated damage to face for visual feedback.\n * Includes bruising, swelling, and bleeding effects.\n * \n * @category Facial Animation\n * @korean 얼굴손상상태\n */\nexport interface FacialDamageState {\n /** Left eye swelling (0-1, 0=none, 1=fully swollen) */\n readonly leftEyeSwelling: number;\n /** Right eye swelling (0-1, 0=none, 1=fully swollen) */\n readonly rightEyeSwelling: number;\n /** Mouth/lip bleeding intensity (0-1) */\n readonly mouthBleeding: number;\n /** Nose bleeding intensity (0-1) */\n readonly noseBleeding: number;\n /** Bruise intensity on left cheek (0-1) */\n readonly leftCheekBruise: number;\n /** Bruise intensity on right cheek (0-1) */\n readonly rightCheekBruise: number;\n /** Forehead bruise/cut intensity (0-1) */\n readonly foreheadBruise: number;\n /** Jaw bruise intensity (0-1) */\n readonly jawBruise: number;\n /** Total facial damage accumulation (0-100) */\n readonly totalFacialDamage: number;\n}\n\n/**\n * Expression state with transition timing\n * \n * Manages current expression and smooth transition to next expression.\n * \n * @category Facial Animation\n * @korean 표정상태\n */\nexport interface ExpressionState {\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Expression intensity (0-1, affects degree of expression) */\n readonly intensity: number;\n /** Time to transition to new expression (seconds) */\n readonly transitionTime: number;\n /** Previous expression (for blending) */\n readonly previousExpression?: FacialExpression;\n /** Transition progress (0-1, 0=start, 1=complete) */\n readonly transitionProgress?: number;\n}\n\n/**\n * Eye openness values for expressions\n * \n * @category Facial Animation\n * @korean 눈개방도\n */\nexport const EYE_OPENNESS: Record<FacialExpression, number> = {\n [FacialExpression.NEUTRAL]: 1.0, // Fully open\n [FacialExpression.FOCUSED]: 0.7, // Narrowed\n [FacialExpression.PAINED]: 0.3, // Squinted\n [FacialExpression.EXHAUSTED]: 0.5, // Half-closed\n [FacialExpression.VICTORIOUS]: 0.9, // Nearly fully open\n [FacialExpression.DEFEATED]: 0.0, // Closed\n};\n\n/**\n * Mouth openness values for expressions\n * \n * @category Facial Animation\n * @korean 입개방도\n */\nexport const MOUTH_OPENNESS: Record<FacialExpression, number> = {\n [FacialExpression.NEUTRAL]: 0.0, // Closed\n [FacialExpression.FOCUSED]: 0.0, // Closed, determined\n [FacialExpression.PAINED]: 0.8, // Open, yelling\n [FacialExpression.EXHAUSTED]: 0.4, // Panting\n [FacialExpression.VICTORIOUS]: 0.2, // Slight smile\n [FacialExpression.DEFEATED]: 0.1, // Slack\n};\n\n/**\n * Head movement animation type\n * \n * Defines types of head movements for combat reactions.\n * \n * @category Facial Animation\n * @korean 머리움직임타입\n */\nexport enum HeadMovementType {\n /** Head snaps back when hit */\n RECOIL = \"recoil\",\n /** Slight nod forward during attack */\n NOD = \"nod\",\n /** Head tilts to avoid strike */\n TILT = \"tilt\",\n /** Head turns to track opponent */\n TURN = \"turn\",\n /** Head shakes when stunned */\n SHAKE = \"shake\",\n /** Head drops when defeated */\n DROP = \"drop\",\n}\n\n/**\n * Head movement animation keyframes\n * \n * Sequence of Euler rotations for head movement animations.\n * \n * @category Facial Animation\n * @korean 머리움직임키프레임\n */\nexport interface HeadMovementKeyframes {\n /** Movement type identifier */\n readonly type: HeadMovementType;\n /** Sequence of rotation keyframes */\n readonly rotations: readonly THREE.Euler[];\n /** Duration of each keyframe in seconds */\n readonly frameDuration: number;\n /** Total animation duration in seconds */\n readonly totalDuration: number;\n /** Whether animation loops */\n readonly loop: boolean;\n}\n\n/**\n * Eye tracking state\n * \n * Manages eye direction and pupil position for opponent tracking.\n * \n * @category Facial Animation\n * @korean 눈추적상태\n */\nexport interface EyeTrackingState {\n /** Target position to look at (opponent position) */\n readonly targetPosition: THREE.Vector3;\n /** Current look direction (normalized) */\n readonly lookDirection: THREE.Vector3;\n /** Pupil offset from center (-1 to 1 for each axis) */\n readonly pupilOffset: { x: number; y: number };\n /** Tracking speed (smoothing factor) */\n readonly trackingSpeed: number;\n /** Whether tracking is enabled */\n readonly enabled: boolean;\n}\n\n/**\n * Default facial damage state (no damage)\n * \n * @category Facial Animation\n * @korean 기본얼굴손상상태\n */\nexport const DEFAULT_FACIAL_DAMAGE: FacialDamageState = {\n leftEyeSwelling: 0,\n rightEyeSwelling: 0,\n mouthBleeding: 0,\n noseBleeding: 0,\n leftCheekBruise: 0,\n rightCheekBruise: 0,\n foreheadBruise: 0,\n jawBruise: 0,\n totalFacialDamage: 0,\n};\n\n/**\n * Default expression state (neutral)\n * \n * @category Facial Animation\n * @korean 기본표정상태\n */\nexport const DEFAULT_EXPRESSION_STATE: ExpressionState = {\n expression: FacialExpression.NEUTRAL,\n intensity: 1.0,\n transitionTime: 0.2,\n};\n\n/**\n * Default eye tracking state\n * \n * @category Facial Animation\n * @korean 기본눈추적상태\n */\nexport const DEFAULT_EYE_TRACKING: EyeTrackingState = {\n targetPosition: new THREE.Vector3(0, 1.9, 5), // Looking forward\n lookDirection: new THREE.Vector3(0, 0, 1), // Forward\n pupilOffset: { x: 0, y: 0 },\n trackingSpeed: 0.1,\n enabled: true,\n};\n\n/**\n * Face3D component props\n * \n * @category Facial Animation\n * @korean 얼굴3D속성\n */\nexport interface Face3DProps {\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Facial damage state */\n readonly damage: FacialDamageState;\n /** Opponent position for eye tracking */\n readonly opponentPosition: THREE.Vector3;\n /** Current head rotation (Euler angles) */\n readonly headRotation: THREE.Euler;\n /** Enable eye tracking (default: true) */\n readonly enableEyeTracking?: boolean;\n /** Enable damage visualization (default: true) */\n readonly enableDamageVisuals?: boolean;\n /** Mobile mode (simplified rendering) */\n readonly isMobile?: boolean;\n /** Skin tone color (default: 0xffdbac) */\n readonly skinColor?: number;\n}\n\n/**\n * Eye component props\n * \n * @category Facial Animation\n * @korean 눈3D속성\n */\nexport interface EyeProps {\n /** Eye position relative to head */\n readonly position: [number, number, number];\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Look direction for pupil tracking */\n readonly lookDirection: THREE.Vector3;\n /** Eye swelling amount (0-1) */\n readonly swelling: number;\n /** Whether this is the left or right eye */\n readonly side: \"left\" | \"right\";\n}\n\n/**\n * Mouth component props\n * \n * @category Facial Animation\n * @korean 입3D속성\n */\nexport interface MouthProps {\n /** Mouth position relative to head */\n readonly position: [number, number, number];\n /** Current facial expression */\n readonly expression: FacialExpression;\n /** Bleeding intensity (0-1) */\n readonly bleeding: number;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,IAAY,mBAAL,yBAAA,kBAAA;;CAEL,iBAAA,aAAA;;CAEA,iBAAA,aAAA;;CAEA,iBAAA,YAAA;;CAEA,iBAAA,eAAA;;CAEA,iBAAA,gBAAA;;CAEA,iBAAA,cAAA;;AACF,EAAA,CAAA,CAAA;;;;;;;AAQA,IAAa,0BAGT;EAC0B,YAAA;EAC1B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;EAC4B,YAAA;EAC1B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;EAC2B,WAAA;EACzB,QAAQ;EACR,SAAS;EACT,WAAW;CACb;EAC8B,cAAA;EAC5B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;EAC+B,eAAA;EAC7B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;EAC6B,aAAA;EAC3B,QAAQ;EACR,SAAS;EACT,WAAW;CACb;AACF;;;;;;;AA2DA,IAAa,eAAiD;EAChC,YAAA;EACA,YAAA;EACD,WAAA;EACG,cAAA;EACC,eAAA;EACF,aAAA;AAC/B;;;;;;;AAQA,IAAa,iBAAmD;EAClC,YAAA;EACA,YAAA;EACD,WAAA;EACG,cAAA;EACC,eAAA;EACF,aAAA;AAC/B;;;;;;;;;AAUA,IAAY,mBAAL,yBAAA,kBAAA;;CAEL,iBAAA,YAAA;;CAEA,iBAAA,SAAA;;CAEA,iBAAA,UAAA;;CAEA,iBAAA,UAAA;;CAEA,iBAAA,WAAA;;CAEA,iBAAA,UAAA;;AACF,EAAA,CAAA,CAAA;;;;;;;AAkDA,IAAa,wBAA2C;CACtD,iBAAiB;CACjB,kBAAkB;CAClB,eAAe;CACf,cAAc;CACd,iBAAiB;CACjB,kBAAkB;CAClB,gBAAgB;CAChB,WAAW;CACX,mBAAmB;AACrB;;;;;;;AAQA,IAAa,2BAA4C;CACvD,YAAA;CACA,WAAW;CACX,gBAAgB;AAClB;;;;;;;AAQA,IAAa,uBAAyC;CACpD,gBAAgB,IAAI,MAAM,QAAQ,GAAG,KAAK,CAAC;CAC3C,eAAe,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;CACxC,aAAa;EAAE,GAAG;EAAG,GAAG;CAAE;CAC1B,eAAe;CACf,SAAS;AACX"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessibility.js","names":[],"sources":["../../src/utils/accessibility.ts"],"sourcesContent":["/**\n * Accessibility utility functions for WCAG 2.1 Level AA compliance\n * Provides keyboard navigation, focus management, color contrast checking,\n * and screen reader support utilities\n *\n * @module utils/accessibility\n * @category Accessibility\n * @korean 접근성 유틸리티\n */\n\nimport React from \"react\";\nimport {\n AriaAttributes,\n ColorContrastConfig,\n createBilingualLabel,\n FocusIndicatorStyle,\n KeyboardActions,\n ScreenReaderAnnouncement,\n WCAGComplianceResult,\n WCAGLevel,\n} from \"../types/AccessibilityTypes\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { EventManager } from \"./EventManager\";\n\n/**\n * Handle keyboard navigation events with WCAG compliance\n * Supports Tab, Enter, Space, Escape, Arrow keys, Home, End\n *\n * @param event - Keyboard event\n * @param actions - Actions to perform for different keys\n *\n * @example\n * ```tsx\n * <div onKeyDown={(e) => handleKeyboardNav(e, {\n * onActivate: () => handleClick(),\n * onCancel: () => handleClose(),\n * onNavigate: (dir) => handleMove(dir),\n * })}>\n * ```\n */\nexport function handleKeyboardNav(\n event: KeyboardEvent,\n actions: KeyboardActions,\n): void {\n const { key, shiftKey } = event;\n\n switch (key) {\n case \"Enter\":\n case \" \":\n // Activate element (button, link, etc.)\n event.preventDefault();\n actions.onActivate?.();\n break;\n\n case \"Escape\":\n // Cancel or close action\n event.preventDefault();\n actions.onCancel?.();\n break;\n\n case \"Tab\":\n // Tab navigation (forward or backward with Shift)\n actions.onTab?.(shiftKey);\n break;\n\n case \"ArrowUp\":\n // Navigate up\n event.preventDefault();\n actions.onNavigate?.(\"up\");\n break;\n\n case \"ArrowDown\":\n // Navigate down\n event.preventDefault();\n actions.onNavigate?.(\"down\");\n break;\n\n case \"ArrowLeft\":\n // Navigate left\n event.preventDefault();\n actions.onNavigate?.(\"left\");\n break;\n\n case \"ArrowRight\":\n // Navigate right\n event.preventDefault();\n actions.onNavigate?.(\"right\");\n break;\n\n case \"Home\":\n // Jump to start\n event.preventDefault();\n actions.onJump?.(\"start\");\n break;\n\n case \"End\":\n // Jump to end\n event.preventDefault();\n actions.onJump?.(\"end\");\n break;\n }\n}\n\n/**\n * Get default focus indicator style (WCAG 2.1 Level AA compliant)\n * Uses 2px solid outline with high contrast cyan color\n * Computed lazily to avoid initialization order dependencies\n */\nfunction getDefaultFocusStyle(): FocusIndicatorStyle {\n const rgb = hexToRgb(KOREAN_COLORS.PRIMARY_CYAN);\n return {\n outlineWidth: 2,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineOffset: 2,\n outlineStyle: \"solid\",\n boxShadow: `0 0 0 2px rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.5)`,\n transitionDuration: 0.2,\n };\n}\n\n/**\n * Get focus indicator CSS style object\n *\n * @param isFocused - Whether element is currently focused\n * @param customStyle - Optional custom focus style overrides\n * @returns CSS style object for focus indicator\n *\n * @example\n * ```tsx\n * const [isFocused, setIsFocused] = useState(false);\n * <button\n * style={getFocusStyle(isFocused)}\n * onFocus={() => setIsFocused(true)}\n * onBlur={() => setIsFocused(false)}\n * >\n * ```\n */\nexport function getFocusStyle(\n isFocused: boolean,\n customStyle?: Partial<FocusIndicatorStyle>,\n): React.CSSProperties {\n const defaultStyle = getDefaultFocusStyle();\n\n if (!isFocused) {\n return {\n // Preserve browser default focus indicator instead of 'none'\n transition: `all ${defaultStyle.transitionDuration}s ease`,\n };\n }\n\n const style = { ...defaultStyle, ...customStyle };\n const rgb = hexToRgb(style.outlineColor ?? KOREAN_COLORS.PRIMARY_CYAN);\n\n return {\n outline: `${style.outlineWidth}px ${style.outlineStyle} rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,\n outlineOffset: `${style.outlineOffset}px`,\n boxShadow:\n style.boxShadow ?? `0 0 0 2px rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.5)`,\n transition: `all ${style.transitionDuration}s ease`,\n };\n}\n\n/**\n * Convert hex color to RGB components\n *\n * @param hex - Hex color code (0xRRGGBB)\n * @returns RGB components\n */\nexport function hexToRgb(hex: number): { r: number; g: number; b: number } {\n return {\n r: (hex >> 16) & 255,\n g: (hex >> 8) & 255,\n b: hex & 255,\n };\n}\n\n/**\n * Calculate contrast ratio between two colors (WCAG 2.1)\n *\n * @param foreground - Foreground color (hex)\n * @param background - Background color (hex)\n * @returns Contrast ratio (1-21)\n *\n * @example\n * ```typescript\n * const ratio = getContrastRatio(KOREAN_COLORS.TEXT_PRIMARY, KOREAN_COLORS.UI_BACKGROUND_DARK);\n * console.log(`Contrast ratio: ${ratio.toFixed(2)}:1`); // Should be >= 4.5:1 for WCAG AA\n * ```\n */\nexport function getContrastRatio(\n foreground: number,\n background: number,\n): number {\n const fg = hexToRgb(foreground);\n const bg = hexToRgb(background);\n\n // Calculate relative luminance (WCAG formula)\n const getLuminance = (r: number, g: number, b: number): number => {\n const [rs, gs, bs] = [r, g, b].map((c) => {\n const sRGB = c / 255;\n return sRGB <= 0.03928\n ? sRGB / 12.92\n : Math.pow((sRGB + 0.055) / 1.055, 2.4);\n });\n return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;\n };\n\n const l1 = getLuminance(fg.r, fg.g, fg.b);\n const l2 = getLuminance(bg.r, bg.g, bg.b);\n\n // Contrast ratio formula\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/**\n * Check if color combination meets WCAG contrast requirements\n *\n * @param config - Color contrast configuration\n * @returns Whether contrast meets WCAG requirements\n *\n * @example\n * ```typescript\n * const meetsWCAG = meetsContrastRequirement({\n * foreground: KOREAN_COLORS.TEXT_PRIMARY,\n * background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n * targetRatio: 4.5,\n * });\n * ```\n */\nexport function meetsContrastRequirement(config: ColorContrastConfig): boolean {\n const ratio = getContrastRatio(config.foreground, config.background);\n return ratio >= config.targetRatio;\n}\n\n/**\n * Get WCAG-compliant foreground color for given background\n * Returns white or black depending on which provides better contrast\n *\n * @param background - Background color (hex)\n * @returns Foreground color (hex) that meets WCAG AA\n */\nexport function getAccessibleForeground(background: number): number {\n const whiteRatio = getContrastRatio(KOREAN_COLORS.TEXT_PRIMARY, background);\n const blackRatio = getContrastRatio(KOREAN_COLORS.BLACK_SOLID, background);\n\n // Return color with better contrast\n return whiteRatio >= blackRatio\n ? KOREAN_COLORS.TEXT_PRIMARY\n : KOREAN_COLORS.BLACK_SOLID;\n}\n\n/**\n * Announce message to screen readers\n * Creates a live region announcement with configurable politeness\n *\n * @param announcement - Screen reader announcement configuration\n *\n * @example\n * ```typescript\n * announceToScreenReader({\n * message: '공격 성공 | Attack successful',\n * politeness: 'polite',\n * delay: 100,\n * });\n * ```\n */\nlet liveRegionInstance: HTMLElement | null = null;\n\nexport function announceToScreenReader(\n announcement: ScreenReaderAnnouncement,\n): void {\n const { message, politeness = \"polite\", delay = 0 } = announcement;\n\n setTimeout(() => {\n // Create or get existing live region\n if (!liveRegionInstance) {\n liveRegionInstance = document.createElement(\"div\");\n liveRegionInstance.id = \"sr-live-region\";\n liveRegionInstance.setAttribute(\"aria-live\", politeness);\n liveRegionInstance.setAttribute(\"aria-atomic\", \"true\");\n liveRegionInstance.style.cssText = `\n position: absolute;\n left: -10000px;\n width: 1px;\n height: 1px;\n overflow: hidden;\n `;\n document.body.appendChild(liveRegionInstance);\n }\n\n // Update politeness level if changed\n if (liveRegionInstance.getAttribute(\"aria-live\") !== politeness) {\n liveRegionInstance.setAttribute(\"aria-live\", politeness);\n }\n\n // Update message\n liveRegionInstance.textContent = message;\n\n // Clear message after 3 seconds\n setTimeout(() => {\n if (liveRegionInstance) {\n liveRegionInstance.textContent = \"\";\n }\n }, 3000);\n }, delay);\n}\n\n/**\n * Clean up screen reader live region (for testing or app unmount)\n */\nexport function cleanupScreenReaderRegion(): void {\n if (liveRegionInstance?.parentNode) {\n liveRegionInstance.parentNode.removeChild(liveRegionInstance);\n liveRegionInstance = null;\n }\n}\n\n/**\n * Trap focus within a container element\n * Prevents focus from leaving the container (e.g., for modals)\n *\n * @param container - Container element to trap focus within\n * @returns Cleanup function to remove focus trap\n *\n * @example\n * ```typescript\n * useEffect(() => {\n * const cleanup = trapFocus(modalRef.current);\n * return cleanup;\n * }, []);\n * ```\n */\nexport function trapFocus(container: HTMLElement | null): () => void {\n if (!container) return () => {};\n\n const focusableElements = getFocusableElements(container);\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Tab\") return;\n\n if (event.shiftKey) {\n // Shift + Tab: Move focus backward\n if (document.activeElement === firstElement) {\n event.preventDefault();\n lastElement?.focus();\n }\n } else {\n // Tab: Move focus forward\n if (document.activeElement === lastElement) {\n event.preventDefault();\n firstElement?.focus();\n }\n }\n };\n\n container.addEventListener(\"keydown\", handleKeyDown);\n\n // Set initial focus\n firstElement?.focus();\n\n // Return cleanup function\n return () => {\n container.removeEventListener(\"keydown\", handleKeyDown);\n };\n}\n\n/**\n * Get all focusable elements within a container\n *\n * @param container - Container element\n * @returns Array of focusable elements\n */\nexport function getFocusableElements(container: HTMLElement): HTMLElement[] {\n const selector = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n '[tabindex]:not([tabindex=\"-1\"])',\n ].join(\", \");\n\n return Array.from(container.querySelectorAll<HTMLElement>(selector));\n}\n\n/**\n * Validate WCAG 2.1 Level AA compliance for a component\n *\n * Note: Color contrast checking requires access to computed styles,\n * which is not available in all testing environments. This validation\n * focuses on structural accessibility attributes.\n *\n * @param element - Element to validate\n * @param config - Validation configuration\n * @returns Compliance validation result\n */\nexport function validateWCAGCompliance(\n element: HTMLElement | null,\n config?: {\n checkKeyboard?: boolean;\n checkFocusVisible?: boolean;\n checkAria?: boolean;\n },\n): WCAGComplianceResult {\n const {\n checkKeyboard = true,\n checkFocusVisible = true,\n checkAria = true,\n } = config ?? {};\n\n const issues: string[] = [];\n let keyboardAccessible = true;\n const colorContrast = true; // Note: Requires computed styles - use getContrastRatio() directly for color validation\n let focusVisible = true;\n let ariaLabels = true;\n\n if (!element) {\n issues.push(\"Element not found\");\n return {\n level: WCAGLevel.A,\n compliant: false,\n criteria: {\n keyboardAccessible: false,\n colorContrast: false,\n focusVisible: false,\n ariaLabels: false,\n semanticHTML: false,\n errorIdentification: false,\n },\n issues,\n };\n }\n\n // Check keyboard accessibility\n if (checkKeyboard) {\n const isInteractive =\n element.tagName === \"BUTTON\" ||\n element.tagName === \"A\" ||\n element.tagName === \"INPUT\" ||\n element.hasAttribute(\"onclick\");\n\n if (isInteractive && element.getAttribute(\"tabindex\") === \"-1\") {\n keyboardAccessible = false;\n issues.push(\"Interactive element not keyboard accessible\");\n }\n }\n\n // Check ARIA labels\n if (checkAria) {\n const hasAriaLabel =\n element.hasAttribute(\"aria-label\") ||\n element.hasAttribute(\"aria-labelledby\");\n\n const hasTextContent = element.textContent?.trim();\n\n if (!hasAriaLabel && !hasTextContent) {\n ariaLabels = false;\n issues.push(\"Missing ARIA label or text content\");\n }\n }\n\n // Check focus indicator\n if (checkFocusVisible) {\n const computedStyle = window.getComputedStyle(element);\n const outline = computedStyle.outline;\n\n if (outline === \"none\" || outline === \"\") {\n focusVisible = false;\n issues.push(\"Missing visible focus indicator\");\n }\n }\n\n const allPass =\n keyboardAccessible && colorContrast && focusVisible && ariaLabels;\n\n return {\n level: allPass ? WCAGLevel.AA : WCAGLevel.A,\n compliant: allPass,\n criteria: {\n keyboardAccessible,\n colorContrast, // Note: Always true - use getContrastRatio() for actual color validation\n focusVisible,\n ariaLabels,\n semanticHTML: true, // Requires manual verification\n errorIdentification: true, // Requires manual verification\n },\n issues,\n };\n}\n\n/**\n * Create comprehensive ARIA attributes for a component\n *\n * @param label - Bilingual label (Korean | English)\n * @param role - ARIA role\n * @param additionalAttrs - Additional ARIA attributes\n * @returns Complete ARIA attributes object\n *\n * @example\n * ```tsx\n * const ariaProps = createAriaAttributes(\n * createBilingualLabel('공격', 'Attack'),\n * 'button',\n * { 'aria-pressed': isPressed }\n * );\n * <button {...ariaProps}>\n * ```\n */\nexport function createAriaAttributes(\n label: string | { korean: string; english: string },\n role?: AriaAttributes[\"role\"],\n additionalAttrs?: Partial<AriaAttributes>,\n): AriaAttributes {\n const bilingualLabel =\n typeof label === \"string\"\n ? label\n : createBilingualLabel(label.korean, label.english).label;\n\n return {\n role,\n \"aria-label\": bilingualLabel,\n ...additionalAttrs,\n };\n}\n\n/**\n * Check if element is currently visible\n * Useful for skip-to-content and focus management\n *\n * @param element - Element to check\n * @returns Whether element is visible\n */\nexport function isElementVisible(element: HTMLElement | null): boolean {\n if (!element) return false;\n\n const style = window.getComputedStyle(element);\n return (\n style.display !== \"none\" &&\n style.visibility !== \"hidden\" &&\n style.opacity !== \"0\" &&\n element.offsetParent !== null\n );\n}\n\n/**\n * Focus first error element in a form\n * Useful for form validation accessibility\n *\n * @param container - Form container\n */\nexport function focusFirstError(container: HTMLElement): void {\n const errorElements = container.querySelectorAll<HTMLElement>(\n '[aria-invalid=\"true\"]',\n );\n\n if (errorElements.length > 0) {\n errorElements[0].focus();\n announceToScreenReader({\n message: \"오류가 발견되었습니다 | Errors found\",\n politeness: \"assertive\",\n });\n }\n}\n\n/**\n * Create skip to content link for keyboard navigation\n * Returns a link that allows users to skip to main content\n *\n * Returns an object with the link element and a cleanup function to properly\n * remove event listeners when the component is no longer needed.\n *\n * **Breaking Change**: This function now returns `{ element, cleanup }` instead of\n * just `HTMLAnchorElement`. Update usage from:\n * ```typescript\n * const link = createSkipLink(id);\n * ```\n * to:\n * ```typescript\n * const { element, cleanup } = createSkipLink(id);\n * ```\n *\n * 컴포넌트가 더 이상 필요하지 않을 때 이벤트 리스너를 적절히 제거하기 위한\n * 링크 요소와 정리 함수가 포함된 객체를 반환합니다.\n *\n * @param targetId - ID of main content element\n * @returns Object with skip link element and cleanup function\n *\n * @example\n * ```typescript\n * const { element, cleanup } = createSkipLink(\"main-content\");\n * document.body.prepend(element);\n * // Later, when component unmounts\n * cleanup();\n * element.remove();\n * ```\n */\nexport function createSkipLink(targetId: string): {\n element: HTMLAnchorElement;\n cleanup: () => void;\n} {\n const skipLink = document.createElement(\"a\");\n skipLink.href = `#${targetId}`;\n skipLink.textContent = \"본문으로 건너뛰기 | Skip to content\";\n skipLink.className = \"skip-link\";\n skipLink.style.cssText = `\n position: absolute;\n top: -40px;\n left: 0;\n background: #00e6e6;\n color: #000;\n padding: 8px;\n text-decoration: none;\n z-index: 10000;\n font-weight: bold;\n `;\n\n // Use EventManager for consistent event handling across the codebase\n const eventManager = new EventManager();\n\n // Event handlers\n const handleFocus = () => {\n skipLink.style.top = \"0\";\n };\n\n const handleBlur = () => {\n skipLink.style.top = \"-40px\";\n };\n\n // Add event listeners via EventManager\n eventManager.add(skipLink, \"focus\", handleFocus);\n eventManager.add(skipLink, \"blur\", handleBlur);\n\n // Cleanup function to remove all event listeners\n const cleanup = () => {\n eventManager.cleanup();\n };\n\n return { element: skipLink, cleanup };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwCA,SAAgB,kBACd,OACA,SACM;CACN,MAAM,EAAE,KAAK,aAAa;CAE1B,QAAQ,KAAR;EACE,KAAK;EACL,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa;GACrB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,WAAW;GACnB;EAEF,KAAK;GAEH,QAAQ,QAAQ,QAAQ;GACxB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,IAAI;GACzB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,MAAM;GAC3B;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,MAAM;GAC3B;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,OAAO;GAC5B;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,SAAS,OAAO;GACxB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,SAAS,KAAK;GACtB;CACJ;AACF;;;;;;AAOA,SAAS,uBAA4C;CACnD,MAAM,MAAM,SAAS,cAAc,YAAY;CAC/C,OAAO;EACL,cAAc;EACd,cAAc,cAAc;EAC5B,eAAe;EACf,cAAc;EACd,WAAW,kBAAkB,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE;EACvD,oBAAoB;CACtB;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cACd,WACA,aACqB;CACrB,MAAM,eAAe,qBAAqB;CAE1C,IAAI,CAAC,WACH,OAAO,EAEL,YAAY,OAAO,aAAa,mBAAmB,QACrD;CAGF,MAAM,QAAQ;EAAE,GAAG;EAAc,GAAG;CAAY;CAChD,MAAM,MAAM,SAAS,MAAM,gBAAgB,cAAc,YAAY;CAErE,OAAO;EACL,SAAS,GAAG,MAAM,aAAa,KAAK,MAAM,aAAa,OAAO,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE;EACxF,eAAe,GAAG,MAAM,cAAc;EACtC,WACE,MAAM,aAAa,kBAAkB,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE;EACjE,YAAY,OAAO,MAAM,mBAAmB;CAC9C;AACF;;;;;;;AAQA,SAAgB,SAAS,KAAkD;CACzE,OAAO;EACL,GAAI,OAAO,KAAM;EACjB,GAAI,OAAO,IAAK;EAChB,GAAG,MAAM;CACX;AACF;;;;;;;;;;;;;;;;AA8FA,IAAI,qBAAyC;AAE7C,SAAgB,uBACd,cACM;CACN,MAAM,EAAE,SAAS,aAAa,UAAU,QAAQ,MAAM;CAEtD,iBAAiB;EAEf,IAAI,CAAC,oBAAoB;GACvB,qBAAqB,SAAS,cAAc,KAAK;GACjD,mBAAmB,KAAK;GACxB,mBAAmB,aAAa,aAAa,UAAU;GACvD,mBAAmB,aAAa,eAAe,MAAM;GACrD,mBAAmB,MAAM,UAAU;;;;;;;GAOnC,SAAS,KAAK,YAAY,kBAAkB;EAC9C;EAGA,IAAI,mBAAmB,aAAa,WAAW,MAAM,YACnD,mBAAmB,aAAa,aAAa,UAAU;EAIzD,mBAAmB,cAAc;EAGjC,iBAAiB;GACf,IAAI,oBACF,mBAAmB,cAAc;EAErC,GAAG,GAAI;CACT,GAAG,KAAK;AACV"}
|
|
1
|
+
{"version":3,"file":"accessibility.js","names":[],"sources":["../../src/utils/accessibility.ts"],"sourcesContent":["/**\n * Accessibility utility functions for WCAG 2.1 Level AA compliance\n * Provides keyboard navigation, focus management, color contrast checking,\n * and screen reader support utilities\n *\n * @module utils/accessibility\n * @category Accessibility\n * @korean 접근성 유틸리티\n */\n\nimport React from \"react\";\nimport {\n AriaAttributes,\n ColorContrastConfig,\n createBilingualLabel,\n FocusIndicatorStyle,\n KeyboardActions,\n ScreenReaderAnnouncement,\n WCAGComplianceResult,\n WCAGLevel,\n} from \"../types/AccessibilityTypes\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { EventManager } from \"./EventManager\";\n\n/**\n * Handle keyboard navigation events with WCAG compliance\n * Supports Tab, Enter, Space, Escape, Arrow keys, Home, End\n *\n * @param event - Keyboard event\n * @param actions - Actions to perform for different keys\n *\n * @example\n * ```tsx\n * <div onKeyDown={(e) => handleKeyboardNav(e, {\n * onActivate: () => handleClick(),\n * onCancel: () => handleClose(),\n * onNavigate: (dir) => handleMove(dir),\n * })}>\n * ```\n */\nexport function handleKeyboardNav(\n event: KeyboardEvent,\n actions: KeyboardActions,\n): void {\n const { key, shiftKey } = event;\n\n switch (key) {\n case \"Enter\":\n case \" \":\n // Activate element (button, link, etc.)\n event.preventDefault();\n actions.onActivate?.();\n break;\n\n case \"Escape\":\n // Cancel or close action\n event.preventDefault();\n actions.onCancel?.();\n break;\n\n case \"Tab\":\n // Tab navigation (forward or backward with Shift)\n actions.onTab?.(shiftKey);\n break;\n\n case \"ArrowUp\":\n // Navigate up\n event.preventDefault();\n actions.onNavigate?.(\"up\");\n break;\n\n case \"ArrowDown\":\n // Navigate down\n event.preventDefault();\n actions.onNavigate?.(\"down\");\n break;\n\n case \"ArrowLeft\":\n // Navigate left\n event.preventDefault();\n actions.onNavigate?.(\"left\");\n break;\n\n case \"ArrowRight\":\n // Navigate right\n event.preventDefault();\n actions.onNavigate?.(\"right\");\n break;\n\n case \"Home\":\n // Jump to start\n event.preventDefault();\n actions.onJump?.(\"start\");\n break;\n\n case \"End\":\n // Jump to end\n event.preventDefault();\n actions.onJump?.(\"end\");\n break;\n }\n}\n\n/**\n * Get default focus indicator style (WCAG 2.1 Level AA compliant)\n * Uses 2px solid outline with high contrast cyan color\n * Computed lazily to avoid initialization order dependencies\n */\nfunction getDefaultFocusStyle(): FocusIndicatorStyle {\n const rgb = hexToRgb(KOREAN_COLORS.PRIMARY_CYAN);\n return {\n outlineWidth: 2,\n outlineColor: KOREAN_COLORS.PRIMARY_CYAN,\n outlineOffset: 2,\n outlineStyle: \"solid\",\n boxShadow: `0 0 0 2px rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.5)`,\n transitionDuration: 0.2,\n };\n}\n\n/**\n * Get focus indicator CSS style object\n *\n * @param isFocused - Whether element is currently focused\n * @param customStyle - Optional custom focus style overrides\n * @returns CSS style object for focus indicator\n *\n * @example\n * ```tsx\n * const [isFocused, setIsFocused] = useState(false);\n * <button\n * style={getFocusStyle(isFocused)}\n * onFocus={() => setIsFocused(true)}\n * onBlur={() => setIsFocused(false)}\n * >\n * ```\n */\nexport function getFocusStyle(\n isFocused: boolean,\n customStyle?: Partial<FocusIndicatorStyle>,\n): React.CSSProperties {\n const defaultStyle = getDefaultFocusStyle();\n\n if (!isFocused) {\n return {\n // Preserve browser default focus indicator instead of 'none'\n transition: `all ${defaultStyle.transitionDuration}s ease`,\n };\n }\n\n const style = { ...defaultStyle, ...customStyle };\n const rgb = hexToRgb(style.outlineColor ?? KOREAN_COLORS.PRIMARY_CYAN);\n\n return {\n outline: `${style.outlineWidth}px ${style.outlineStyle} rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,\n outlineOffset: `${style.outlineOffset}px`,\n boxShadow:\n style.boxShadow ?? `0 0 0 2px rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.5)`,\n transition: `all ${style.transitionDuration}s ease`,\n };\n}\n\n/**\n * Convert hex color to RGB components\n *\n * @param hex - Hex color code (0xRRGGBB)\n * @returns RGB components\n */\nexport function hexToRgb(hex: number): { r: number; g: number; b: number } {\n return {\n r: (hex >> 16) & 255,\n g: (hex >> 8) & 255,\n b: hex & 255,\n };\n}\n\n/**\n * Calculate contrast ratio between two colors (WCAG 2.1)\n *\n * @param foreground - Foreground color (hex)\n * @param background - Background color (hex)\n * @returns Contrast ratio (1-21)\n *\n * @example\n * ```typescript\n * const ratio = getContrastRatio(KOREAN_COLORS.TEXT_PRIMARY, KOREAN_COLORS.UI_BACKGROUND_DARK);\n * console.log(`Contrast ratio: ${ratio.toFixed(2)}:1`); // Should be >= 4.5:1 for WCAG AA\n * ```\n */\nexport function getContrastRatio(\n foreground: number,\n background: number,\n): number {\n const fg = hexToRgb(foreground);\n const bg = hexToRgb(background);\n\n // Calculate relative luminance (WCAG formula)\n const getLuminance = (r: number, g: number, b: number): number => {\n const [rs, gs, bs] = [r, g, b].map((c) => {\n const sRGB = c / 255;\n return sRGB <= 0.03928\n ? sRGB / 12.92\n : Math.pow((sRGB + 0.055) / 1.055, 2.4);\n });\n return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;\n };\n\n const l1 = getLuminance(fg.r, fg.g, fg.b);\n const l2 = getLuminance(bg.r, bg.g, bg.b);\n\n // Contrast ratio formula\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/**\n * Check if color combination meets WCAG contrast requirements\n *\n * @param config - Color contrast configuration\n * @returns Whether contrast meets WCAG requirements\n *\n * @example\n * ```typescript\n * const meetsWCAG = meetsContrastRequirement({\n * foreground: KOREAN_COLORS.TEXT_PRIMARY,\n * background: KOREAN_COLORS.UI_BACKGROUND_DARK,\n * targetRatio: 4.5,\n * });\n * ```\n */\nexport function meetsContrastRequirement(config: ColorContrastConfig): boolean {\n const ratio = getContrastRatio(config.foreground, config.background);\n return ratio >= config.targetRatio;\n}\n\n/**\n * Get WCAG-compliant foreground color for given background\n * Returns white or black depending on which provides better contrast\n *\n * @param background - Background color (hex)\n * @returns Foreground color (hex) that meets WCAG AA\n */\nexport function getAccessibleForeground(background: number): number {\n const whiteRatio = getContrastRatio(KOREAN_COLORS.TEXT_PRIMARY, background);\n const blackRatio = getContrastRatio(KOREAN_COLORS.BLACK_SOLID, background);\n\n // Return color with better contrast\n return whiteRatio >= blackRatio\n ? KOREAN_COLORS.TEXT_PRIMARY\n : KOREAN_COLORS.BLACK_SOLID;\n}\n\n/**\n * Announce message to screen readers\n * Creates a live region announcement with configurable politeness\n *\n * @param announcement - Screen reader announcement configuration\n *\n * @example\n * ```typescript\n * announceToScreenReader({\n * message: '공격 성공 | Attack successful',\n * politeness: 'polite',\n * delay: 100,\n * });\n * ```\n */\nlet liveRegionInstance: HTMLElement | null = null;\n\nexport function announceToScreenReader(\n announcement: ScreenReaderAnnouncement,\n): void {\n const { message, politeness = \"polite\", delay = 0 } = announcement;\n\n setTimeout(() => {\n // Create or get existing live region\n if (!liveRegionInstance) {\n liveRegionInstance = document.createElement(\"div\");\n liveRegionInstance.id = \"sr-live-region\";\n liveRegionInstance.setAttribute(\"aria-live\", politeness);\n liveRegionInstance.setAttribute(\"aria-atomic\", \"true\");\n liveRegionInstance.style.cssText = `\n position: absolute;\n left: -10000px;\n width: 1px;\n height: 1px;\n overflow: hidden;\n `;\n document.body.appendChild(liveRegionInstance);\n }\n\n // Update politeness level if changed\n if (liveRegionInstance.getAttribute(\"aria-live\") !== politeness) {\n liveRegionInstance.setAttribute(\"aria-live\", politeness);\n }\n\n // Update message\n liveRegionInstance.textContent = message;\n\n // Clear message after 3 seconds\n setTimeout(() => {\n if (liveRegionInstance) {\n liveRegionInstance.textContent = \"\";\n }\n }, 3000);\n }, delay);\n}\n\n/**\n * Clean up screen reader live region (for testing or app unmount)\n */\nexport function cleanupScreenReaderRegion(): void {\n if (liveRegionInstance?.parentNode) {\n liveRegionInstance.parentNode.removeChild(liveRegionInstance);\n liveRegionInstance = null;\n }\n}\n\n/**\n * Trap focus within a container element\n * Prevents focus from leaving the container (e.g., for modals)\n *\n * @param container - Container element to trap focus within\n * @returns Cleanup function to remove focus trap\n *\n * @example\n * ```typescript\n * useEffect(() => {\n * const cleanup = trapFocus(modalRef.current);\n * return cleanup;\n * }, []);\n * ```\n */\nexport function trapFocus(container: HTMLElement | null): () => void {\n if (!container) return () => {};\n\n const focusableElements = getFocusableElements(container);\n const firstElement = focusableElements[0];\n const lastElement = focusableElements[focusableElements.length - 1];\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== \"Tab\") return;\n\n if (event.shiftKey) {\n // Shift + Tab: Move focus backward\n if (document.activeElement === firstElement) {\n event.preventDefault();\n lastElement?.focus();\n }\n } else {\n // Tab: Move focus forward\n if (document.activeElement === lastElement) {\n event.preventDefault();\n firstElement?.focus();\n }\n }\n };\n\n container.addEventListener(\"keydown\", handleKeyDown);\n\n // Set initial focus\n firstElement?.focus();\n\n // Return cleanup function\n return () => {\n container.removeEventListener(\"keydown\", handleKeyDown);\n };\n}\n\n/**\n * Get all focusable elements within a container\n *\n * @param container - Container element\n * @returns Array of focusable elements\n */\nexport function getFocusableElements(container: HTMLElement): HTMLElement[] {\n const selector = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n '[tabindex]:not([tabindex=\"-1\"])',\n ].join(\", \");\n\n return Array.from(container.querySelectorAll<HTMLElement>(selector));\n}\n\n/**\n * Validate WCAG 2.1 Level AA compliance for a component\n *\n * Note: Color contrast checking requires access to computed styles,\n * which is not available in all testing environments. This validation\n * focuses on structural accessibility attributes.\n *\n * @param element - Element to validate\n * @param config - Validation configuration\n * @returns Compliance validation result\n */\nexport function validateWCAGCompliance(\n element: HTMLElement | null,\n config?: {\n checkKeyboard?: boolean;\n checkFocusVisible?: boolean;\n checkAria?: boolean;\n },\n): WCAGComplianceResult {\n const {\n checkKeyboard = true,\n checkFocusVisible = true,\n checkAria = true,\n } = config ?? {};\n\n const issues: string[] = [];\n let keyboardAccessible = true;\n const colorContrast = true; // Note: Requires computed styles - use getContrastRatio() directly for color validation\n let focusVisible = true;\n let ariaLabels = true;\n\n if (!element) {\n issues.push(\"Element not found\");\n return {\n level: WCAGLevel.A,\n compliant: false,\n criteria: {\n keyboardAccessible: false,\n colorContrast: false,\n focusVisible: false,\n ariaLabels: false,\n semanticHTML: false,\n errorIdentification: false,\n },\n issues,\n };\n }\n\n // Check keyboard accessibility\n if (checkKeyboard) {\n const isInteractive =\n element.tagName === \"BUTTON\" ||\n element.tagName === \"A\" ||\n element.tagName === \"INPUT\" ||\n element.hasAttribute(\"onclick\");\n\n if (isInteractive && element.getAttribute(\"tabindex\") === \"-1\") {\n keyboardAccessible = false;\n issues.push(\"Interactive element not keyboard accessible\");\n }\n }\n\n // Check ARIA labels\n if (checkAria) {\n const hasAriaLabel =\n element.hasAttribute(\"aria-label\") ||\n element.hasAttribute(\"aria-labelledby\");\n\n const hasTextContent = element.textContent?.trim();\n\n if (!hasAriaLabel && !hasTextContent) {\n ariaLabels = false;\n issues.push(\"Missing ARIA label or text content\");\n }\n }\n\n // Check focus indicator\n if (checkFocusVisible) {\n const computedStyle = window.getComputedStyle(element);\n const outline = computedStyle.outline;\n\n if (outline === \"none\" || outline === \"\") {\n focusVisible = false;\n issues.push(\"Missing visible focus indicator\");\n }\n }\n\n const allPass =\n keyboardAccessible && colorContrast && focusVisible && ariaLabels;\n\n return {\n level: allPass ? WCAGLevel.AA : WCAGLevel.A,\n compliant: allPass,\n criteria: {\n keyboardAccessible,\n colorContrast, // Note: Always true - use getContrastRatio() for actual color validation\n focusVisible,\n ariaLabels,\n semanticHTML: true, // Requires manual verification\n errorIdentification: true, // Requires manual verification\n },\n issues,\n };\n}\n\n/**\n * Create comprehensive ARIA attributes for a component\n *\n * @param label - Bilingual label (Korean | English)\n * @param role - ARIA role\n * @param additionalAttrs - Additional ARIA attributes\n * @returns Complete ARIA attributes object\n *\n * @example\n * ```tsx\n * const ariaProps = createAriaAttributes(\n * createBilingualLabel('공격', 'Attack'),\n * 'button',\n * { 'aria-pressed': isPressed }\n * );\n * <button {...ariaProps}>\n * ```\n */\nexport function createAriaAttributes(\n label: string | { korean: string; english: string },\n role?: AriaAttributes[\"role\"],\n additionalAttrs?: Partial<AriaAttributes>,\n): AriaAttributes {\n const bilingualLabel =\n typeof label === \"string\"\n ? label\n : createBilingualLabel(label.korean, label.english).label;\n\n return {\n role,\n \"aria-label\": bilingualLabel,\n ...additionalAttrs,\n };\n}\n\n/**\n * Check if element is currently visible\n * Useful for skip-to-content and focus management\n *\n * @param element - Element to check\n * @returns Whether element is visible\n */\nexport function isElementVisible(element: HTMLElement | null): boolean {\n if (!element) return false;\n\n const style = window.getComputedStyle(element);\n return (\n style.display !== \"none\" &&\n style.visibility !== \"hidden\" &&\n style.opacity !== \"0\" &&\n element.offsetParent !== null\n );\n}\n\n/**\n * Focus first error element in a form\n * Useful for form validation accessibility\n *\n * @param container - Form container\n */\nexport function focusFirstError(container: HTMLElement): void {\n const errorElements = container.querySelectorAll<HTMLElement>(\n '[aria-invalid=\"true\"]',\n );\n\n if (errorElements.length > 0) {\n errorElements[0].focus();\n announceToScreenReader({\n message: \"오류가 발견되었습니다 | Errors found\",\n politeness: \"assertive\",\n });\n }\n}\n\n/**\n * Create skip to content link for keyboard navigation\n * Returns a link that allows users to skip to main content\n *\n * Returns an object with the link element and a cleanup function to properly\n * remove event listeners when the component is no longer needed.\n *\n * **Breaking Change**: This function now returns `{ element, cleanup }` instead of\n * just `HTMLAnchorElement`. Update usage from:\n * ```typescript\n * const link = createSkipLink(id);\n * ```\n * to:\n * ```typescript\n * const { element, cleanup } = createSkipLink(id);\n * ```\n *\n * 컴포넌트가 더 이상 필요하지 않을 때 이벤트 리스너를 적절히 제거하기 위한\n * 링크 요소와 정리 함수가 포함된 객체를 반환합니다.\n *\n * @param targetId - ID of main content element\n * @returns Object with skip link element and cleanup function\n *\n * @example\n * ```typescript\n * const { element, cleanup } = createSkipLink(\"main-content\");\n * document.body.prepend(element);\n * // Later, when component unmounts\n * cleanup();\n * element.remove();\n * ```\n */\nexport function createSkipLink(targetId: string): {\n element: HTMLAnchorElement;\n cleanup: () => void;\n} {\n const skipLink = document.createElement(\"a\");\n skipLink.href = `#${targetId}`;\n skipLink.textContent = \"본문으로 건너뛰기 | Skip to content\";\n skipLink.className = \"skip-link\";\n skipLink.style.cssText = `\n position: absolute;\n top: -40px;\n left: 0;\n background: #00e6e6;\n color: #000;\n padding: 8px;\n text-decoration: none;\n z-index: 10000;\n font-weight: bold;\n `;\n\n // Use EventManager for consistent event handling across the codebase\n const eventManager = new EventManager();\n\n // Event handlers\n const handleFocus = () => {\n skipLink.style.top = \"0\";\n };\n\n const handleBlur = () => {\n skipLink.style.top = \"-40px\";\n };\n\n // Add event listeners via EventManager\n eventManager.add(skipLink, \"focus\", handleFocus);\n eventManager.add(skipLink, \"blur\", handleBlur);\n\n // Cleanup function to remove all event listeners\n const cleanup = () => {\n eventManager.cleanup();\n };\n\n return { element: skipLink, cleanup };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwCA,SAAgB,kBACd,OACA,SACM;CACN,MAAM,EAAE,KAAK,aAAa;CAE1B,QAAQ,KAAR;EACE,KAAK;EACL,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa;GACrB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,WAAW;GACnB;EAEF,KAAK;GAEH,QAAQ,QAAQ,QAAQ;GACxB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,IAAI;GACzB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,MAAM;GAC3B;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,MAAM;GAC3B;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,aAAa,OAAO;GAC5B;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,SAAS,OAAO;GACxB;EAEF,KAAK;GAEH,MAAM,eAAe;GACrB,QAAQ,SAAS,KAAK;CAE1B;AACF;;;;;;AAOA,SAAS,uBAA4C;CACnD,MAAM,MAAM,SAAS,cAAc,YAAY;CAC/C,OAAO;EACL,cAAc;EACd,cAAc,cAAc;EAC5B,eAAe;EACf,cAAc;EACd,WAAW,kBAAkB,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE;EACvD,oBAAoB;CACtB;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,cACd,WACA,aACqB;CACrB,MAAM,eAAe,qBAAqB;CAE1C,IAAI,CAAC,WACH,OAAO,EAEL,YAAY,OAAO,aAAa,mBAAmB,QACrD;CAGF,MAAM,QAAQ;EAAE,GAAG;EAAc,GAAG;CAAY;CAChD,MAAM,MAAM,SAAS,MAAM,gBAAgB,cAAc,YAAY;CAErE,OAAO;EACL,SAAS,GAAG,MAAM,aAAa,KAAK,MAAM,aAAa,OAAO,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE;EACxF,eAAe,GAAG,MAAM,cAAc;EACtC,WACE,MAAM,aAAa,kBAAkB,IAAI,EAAE,IAAI,IAAI,EAAE,IAAI,IAAI,EAAE;EACjE,YAAY,OAAO,MAAM,mBAAmB;CAC9C;AACF;;;;;;;AAQA,SAAgB,SAAS,KAAkD;CACzE,OAAO;EACL,GAAI,OAAO,KAAM;EACjB,GAAI,OAAO,IAAK;EAChB,GAAG,MAAM;CACX;AACF;;;;;;;;;;;;;;;;AA8FA,IAAI,qBAAyC;AAE7C,SAAgB,uBACd,cACM;CACN,MAAM,EAAE,SAAS,aAAa,UAAU,QAAQ,MAAM;CAEtD,iBAAiB;EAEf,IAAI,CAAC,oBAAoB;GACvB,qBAAqB,SAAS,cAAc,KAAK;GACjD,mBAAmB,KAAK;GACxB,mBAAmB,aAAa,aAAa,UAAU;GACvD,mBAAmB,aAAa,eAAe,MAAM;GACrD,mBAAmB,MAAM,UAAU;;;;;;;GAOnC,SAAS,KAAK,YAAY,kBAAkB;EAC9C;EAGA,IAAI,mBAAmB,aAAa,WAAW,MAAM,YACnD,mBAAmB,aAAa,aAAa,UAAU;EAIzD,mBAAmB,cAAc;EAGjC,iBAAiB;GACf,IAAI,oBACF,mBAAmB,cAAc;EAErC,GAAG,GAAI;CACT,GAAG,KAAK;AACV"}
|
package/lib/utils/effectUtils.js
CHANGED
|
@@ -87,9 +87,7 @@ function applyEffectModifiers(baseValue, effects, statType) {
|
|
|
87
87
|
case "weakened":
|
|
88
88
|
modifier *= .8;
|
|
89
89
|
break;
|
|
90
|
-
case "exhausted":
|
|
91
|
-
if (statType === "speed") modifier *= .7;
|
|
92
|
-
break;
|
|
90
|
+
case "exhausted": if (statType === "speed") modifier *= .7;
|
|
93
91
|
}
|
|
94
92
|
});
|
|
95
93
|
return Math.floor(baseValue * modifier);
|