SuppressionModel.cs
1 namespace GUNRPG.Core.Combat; 2 3 /// <summary> 4 /// Models suppression mechanics - the psychological pressure from near-misses and incoming fire 5 /// that affects operator performance without dealing damage. Suppression is triggered by 6 /// threatening shots that miss, applies temporary penalties that decay over time, and 7 /// complements flinch (hit-based) without replacing it. 8 /// </summary> 9 public static class SuppressionModel 10 { 11 /// <summary> 12 /// Maximum suppression level (capped to prevent stun-lock). 13 /// </summary> 14 public const float MaxSuppressionLevel = 1.0f; 15 16 /// <summary> 17 /// Minimum suppression level (below this, operator is not considered suppressed). 18 /// </summary> 19 public const float MinSuppressionLevel = 0.0f; 20 21 /// <summary> 22 /// Suppression threshold - level at which an operator is considered "suppressed". 23 /// </summary> 24 public const float SuppressionThreshold = 0.1f; 25 26 /// <summary> 27 /// Base suppression amount applied per near-miss. 28 /// Scaled by weapon class, fire rate, and distance. 29 /// </summary> 30 public const float BaseSuppressionPerMiss = 0.15f; 31 32 /// <summary> 33 /// Angular deviation threshold (degrees) within which a miss still causes suppression. 34 /// Shots that deviate more than this from the target are not threatening enough to suppress. 35 /// </summary> 36 public const float SuppressionAngleThresholdDegrees = 0.5f; 37 38 /// <summary> 39 /// Exponential decay rate per second when not under fire. 40 /// Higher values = faster decay. 41 /// </summary> 42 public const float DecayRatePerSecond = 0.8f; 43 44 /// <summary> 45 /// Decay rate multiplier when under continued fire (slowed decay). 46 /// </summary> 47 public const float DecayRateUnderFireMultiplier = 0.25f; 48 49 /// <summary> 50 /// Time window (ms) to consider "under continued fire" for decay slowdown. 51 /// </summary> 52 public const long ContinuedFireWindowMs = 500; 53 54 // Effect constants - penalties scale linearly with suppression level 55 56 /// <summary> 57 /// Maximum ADS time penalty at full suppression (multiplier). 58 /// At max suppression, ADS time is increased by this factor. 59 /// </summary> 60 public const float MaxADSTimePenaltyMultiplier = 0.5f; 61 62 /// <summary> 63 /// Maximum accuracy proficiency reduction at full suppression. 64 /// Reduces effective accuracy proficiency by this percentage. 65 /// </summary> 66 public const float MaxAccuracyProficiencyReduction = 0.4f; 67 68 /// <summary> 69 /// Maximum recoil control penalty at full suppression. 70 /// Reduces effective recoil control by this percentage. 71 /// </summary> 72 public const float MaxRecoilControlPenalty = 0.3f; 73 74 /// <summary> 75 /// Maximum reaction delay at full suppression (ms). 76 /// </summary> 77 public const float MaxReactionDelayMs = 50f; 78 79 /// <summary> 80 /// Minimum proficiency floor when suppressed (prevents total ineffectiveness). 81 /// </summary> 82 public const float SuppressionProficiencyFloorFactor = 0.4f; 83 84 /// <summary> 85 /// Calculates suppression severity based on weapon characteristics and distance. 86 /// </summary> 87 /// <param name="weaponSuppressionFactor">Weapon-specific suppression factor (LMGs higher than SMGs)</param> 88 /// <param name="weaponFireRateRPM">Weapon fire rate in rounds per minute</param> 89 /// <param name="distanceMeters">Distance between shooter and target</param> 90 /// <param name="angularDeviationDegrees">How close the shot came to hitting (degrees)</param> 91 /// <param name="targetMovementState">Optional movement state of the target to apply modifiers</param> 92 /// <param name="targetDirection">Optional movement direction of the target to apply directional modifiers</param> 93 /// <returns>Suppression severity to apply (0.0 - 1.0)</returns> 94 public static float CalculateSuppressionSeverity( 95 float weaponSuppressionFactor, 96 float weaponFireRateRPM, 97 float distanceMeters, 98 float angularDeviationDegrees, 99 Operators.MovementState? targetMovementState = null, 100 Operators.MovementDirection? targetDirection = null) 101 { 102 // No suppression if shot was too far off 103 if (Math.Abs(angularDeviationDegrees) > SuppressionAngleThresholdDegrees) 104 return 0f; 105 106 // Base suppression from weapon class 107 float baseSuppression = BaseSuppressionPerMiss * weaponSuppressionFactor; 108 109 // Fire rate factor: higher fire rate = more sustained pressure 110 // Normalize around 600 RPM as baseline 111 float fireRateFactor = Math.Clamp(weaponFireRateRPM / 600f, 0.5f, 2.0f); 112 113 // Distance factor: closer = more suppressive 114 // Peak suppression at close range, falls off with distance 115 float distanceFactor = CalculateDistanceFactor(distanceMeters); 116 117 // Angular closeness factor: closer misses are more suppressive 118 float closenessFactor = 1.0f - (Math.Abs(angularDeviationDegrees) / SuppressionAngleThresholdDegrees); 119 120 float severity = baseSuppression * fireRateFactor * distanceFactor * closenessFactor; 121 122 // Apply movement modifier for target 123 if (targetMovementState.HasValue) 124 { 125 float movementMultiplier = MovementModel.GetSuppressionBuildupMultiplier(targetMovementState.Value); 126 severity *= movementMultiplier; 127 } 128 129 // Apply directional modifier for target (advancing = more exposed, retreating = less exposed) 130 if (targetDirection.HasValue) 131 { 132 float directionMultiplier = MovementModel.GetDirectionalSuppressionMultiplier(targetDirection.Value); 133 severity *= directionMultiplier; 134 } 135 136 return Math.Clamp(severity, 0f, MaxSuppressionLevel); 137 } 138 139 /// <summary> 140 /// Calculates distance factor for suppression. 141 /// Closer targets receive more suppression. 142 /// </summary> 143 private static float CalculateDistanceFactor(float distanceMeters) 144 { 145 // Full suppression up to 10m, then linear falloff to 50% at 50m, then 25% beyond 146 if (distanceMeters <= 10f) 147 return 1.0f; 148 if (distanceMeters <= 50f) 149 return 1.0f - (distanceMeters - 10f) / 80f; // 0.5 at 50m 150 return 0.25f; // Minimum floor at long range 151 } 152 153 /// <summary> 154 /// Applies suppression decay over time. 155 /// </summary> 156 /// <param name="currentSuppression">Current suppression level</param> 157 /// <param name="deltaMs">Time elapsed since last update</param> 158 /// <param name="isUnderFire">Whether the operator is currently under continued fire</param> 159 /// <param name="movementState">Optional movement state to apply decay modifiers</param> 160 /// <param name="responseProficiency">Optional response proficiency scaling the decay rate (0.0-1.0): lower values slow recovery, higher values speed it up.</param> 161 /// <returns>New suppression level after decay</returns> 162 public static float ApplyDecay( 163 float currentSuppression, 164 long deltaMs, 165 bool isUnderFire, 166 Operators.MovementState? movementState = null, 167 float? responseProficiency = null) 168 { 169 if (currentSuppression <= MinSuppressionLevel) 170 return MinSuppressionLevel; 171 172 float deltaSeconds = deltaMs / 1000f; 173 float effectiveDecayRate = isUnderFire 174 ? DecayRatePerSecond * DecayRateUnderFireMultiplier 175 : DecayRatePerSecond; 176 177 // Apply movement modifier for decay rate 178 if (movementState.HasValue) 179 { 180 float movementMultiplier = MovementModel.GetSuppressionDecayMultiplier(movementState.Value); 181 effectiveDecayRate *= movementMultiplier; 182 } 183 184 // Apply response proficiency modifier for faster recovery 185 if (responseProficiency.HasValue) 186 { 187 effectiveDecayRate = ResponseProficiencyModel.CalculateEffectiveSuppressionDecayRate( 188 effectiveDecayRate, responseProficiency.Value); 189 } 190 191 // Exponential decay: S(t) = S0 * e^(-rate * t) 192 float decayFactor = (float)Math.Exp(-effectiveDecayRate * deltaSeconds); 193 float newSuppression = currentSuppression * decayFactor; 194 195 // Snap to zero if below threshold 196 if (newSuppression < SuppressionThreshold * 0.1f) 197 return MinSuppressionLevel; 198 199 return Math.Clamp(newSuppression, MinSuppressionLevel, MaxSuppressionLevel); 200 } 201 202 /// <summary> 203 /// Calculates ADS time penalty due to suppression. 204 /// </summary> 205 /// <param name="baseADSTimeMs">Base ADS time in milliseconds</param> 206 /// <param name="suppressionLevel">Current suppression level (0.0 - 1.0)</param> 207 /// <returns>Effective ADS time including penalty</returns> 208 public static float CalculateEffectiveADSTime(float baseADSTimeMs, float suppressionLevel) 209 { 210 suppressionLevel = Math.Clamp(suppressionLevel, MinSuppressionLevel, MaxSuppressionLevel); 211 float penalty = suppressionLevel * MaxADSTimePenaltyMultiplier; 212 return baseADSTimeMs * (1f + penalty); 213 } 214 215 /// <summary> 216 /// Calculates effective accuracy proficiency under suppression. 217 /// </summary> 218 /// <param name="baseAccuracyProficiency">Base accuracy proficiency (0.0 - 1.0)</param> 219 /// <param name="suppressionLevel">Current suppression level (0.0 - 1.0)</param> 220 /// <returns>Effective accuracy proficiency after suppression penalty</returns> 221 public static float CalculateEffectiveAccuracyProficiency( 222 float baseAccuracyProficiency, 223 float suppressionLevel) 224 { 225 baseAccuracyProficiency = Math.Clamp(baseAccuracyProficiency, 0f, 1f); 226 suppressionLevel = Math.Clamp(suppressionLevel, MinSuppressionLevel, MaxSuppressionLevel); 227 228 // Calculate reduction factor based on suppression 229 float reductionFactor = 1f - (suppressionLevel * MaxAccuracyProficiencyReduction); 230 231 // Apply floor to prevent total ineffectiveness 232 float minProficiency = baseAccuracyProficiency * SuppressionProficiencyFloorFactor; 233 float effectiveProficiency = baseAccuracyProficiency * reductionFactor; 234 235 return Math.Max(effectiveProficiency, minProficiency); 236 } 237 238 /// <summary> 239 /// Calculates effective recoil control factor under suppression. 240 /// </summary> 241 /// <param name="baseRecoilControlFactor">Base recoil control factor</param> 242 /// <param name="suppressionLevel">Current suppression level (0.0 - 1.0)</param> 243 /// <returns>Effective recoil control factor after suppression penalty</returns> 244 public static float CalculateEffectiveRecoilControlFactor( 245 float baseRecoilControlFactor, 246 float suppressionLevel) 247 { 248 suppressionLevel = Math.Clamp(suppressionLevel, MinSuppressionLevel, MaxSuppressionLevel); 249 float penalty = suppressionLevel * MaxRecoilControlPenalty; 250 return baseRecoilControlFactor * (1f - penalty); 251 } 252 253 /// <summary> 254 /// Calculates reaction delay due to suppression. 255 /// </summary> 256 /// <param name="suppressionLevel">Current suppression level (0.0 - 1.0)</param> 257 /// <returns>Reaction delay in milliseconds</returns> 258 public static float CalculateReactionDelay(float suppressionLevel) 259 { 260 suppressionLevel = Math.Clamp(suppressionLevel, MinSuppressionLevel, MaxSuppressionLevel); 261 return suppressionLevel * MaxReactionDelayMs; 262 } 263 264 /// <summary> 265 /// Determines if suppression should be applied based on shot deviation. 266 /// </summary> 267 /// <param name="angularDeviationDegrees">Deviation from target in degrees</param> 268 /// <returns>True if the shot was close enough to cause suppression</returns> 269 public static bool ShouldApplySuppression(float angularDeviationDegrees) 270 { 271 return Math.Abs(angularDeviationDegrees) <= SuppressionAngleThresholdDegrees; 272 } 273 274 /// <summary> 275 /// Combines existing suppression with new suppression (stacks up to max). 276 /// </summary> 277 /// <param name="currentSuppression">Current suppression level</param> 278 /// <param name="additionalSuppression">New suppression to add</param> 279 /// <returns>Combined suppression level (capped at max)</returns> 280 public static float CombineSuppression(float currentSuppression, float additionalSuppression) 281 { 282 return Math.Clamp( 283 currentSuppression + additionalSuppression, 284 MinSuppressionLevel, 285 MaxSuppressionLevel); 286 } 287 }