ResponseProficiencyModel.cs
1 namespace GUNRPG.Core.Combat; 2 3 /// <summary> 4 /// Models response proficiency effects on action commitment costs. 5 /// Response proficiency determines how quickly an operator can transition between actions under pressure. 6 /// 7 /// This completes the operator skill triangle: 8 /// - Reaction Proficiency → how fast actions start (via AccuracyProficiency recognition delays) 9 /// - Accuracy Proficiency → how well actions perform 10 /// - Response Proficiency → how fast actions switch 11 /// 12 /// Design goals: 13 /// - Scale existing timing costs, not introduce new delays 14 /// - Deterministic (no randomness) 15 /// - Applied identically to player and AI operators 16 /// - Visible in timeline and logs 17 /// </summary> 18 public static class ResponseProficiencyModel 19 { 20 /// <summary> 21 /// Maximum penalty multiplier for low proficiency operators. 22 /// At proficiency 0.0, delays are scaled by this factor. 23 /// </summary> 24 public const float MaxDelayPenaltyMultiplier = 1.3f; 25 26 /// <summary> 27 /// Minimum penalty multiplier (bonus) for high proficiency operators. 28 /// At proficiency 1.0, delays are scaled by this factor. 29 /// </summary> 30 public const float MinDelayPenaltyMultiplier = 0.7f; 31 32 /// <summary> 33 /// Proficiency level at which delays are at their base value (1.0x multiplier). 34 /// Operators at this proficiency experience no bonus or penalty. 35 /// </summary> 36 public const float NeutralProficiency = 0.5f; 37 38 /// <summary> 39 /// Minimum absolute delay in milliseconds after scaling. 40 /// Prevents delays from becoming zero or negative. 41 /// </summary> 42 public const float MinEffectiveDelayMs = 10f; 43 44 /// <summary> 45 /// Threshold for considering a multiplier as non-neutral for display purposes. 46 /// Multipliers within this threshold of 1.0 are considered "neutral" and may be hidden in UI. 47 /// </summary> 48 public const float MultiplierDisplayThreshold = 0.01f; 49 50 /// <summary> 51 /// Calculates the effective delay after applying response proficiency scaling. 52 /// 53 /// Formula: effectiveDelayMs = baseDelayMs × lerp(maxPenalty, minPenalty, responseProficiency) 54 /// 55 /// Examples: 56 /// - Low proficiency (0.0) → 1.3× delays (slower transitions) 57 /// - Medium proficiency (0.5) → 1.0× delays (neutral) 58 /// - High proficiency (1.0) → 0.7× delays (faster transitions) 59 /// </summary> 60 /// <param name="baseDelayMs">The base delay in milliseconds before proficiency scaling</param> 61 /// <param name="responseProficiency">Operator's response proficiency (0.0-1.0)</param> 62 /// <returns>The effective delay in milliseconds after proficiency scaling</returns> 63 public static float CalculateEffectiveDelay(float baseDelayMs, float responseProficiency) 64 { 65 responseProficiency = Math.Clamp(responseProficiency, 0f, 1f); 66 67 // Linear interpolation from max penalty to min penalty based on proficiency 68 float multiplier = MaxDelayPenaltyMultiplier + 69 (MinDelayPenaltyMultiplier - MaxDelayPenaltyMultiplier) * responseProficiency; 70 71 float effectiveDelay = baseDelayMs * multiplier; 72 73 // Ensure minimum delay to avoid zero or negative values 74 return Math.Max(effectiveDelay, MinEffectiveDelayMs); 75 } 76 77 /// <summary> 78 /// Calculates the effective delay and returns both the result and the multiplier used. 79 /// Useful for logging and timeline display. 80 /// </summary> 81 /// <param name="baseDelayMs">The base delay in milliseconds before proficiency scaling</param> 82 /// <param name="responseProficiency">Operator's response proficiency (0.0-1.0)</param> 83 /// <returns>A tuple containing (effectiveDelayMs, multiplierUsed)</returns> 84 public static (float effectiveDelayMs, float multiplier) CalculateEffectiveDelayWithMultiplier( 85 float baseDelayMs, 86 float responseProficiency) 87 { 88 responseProficiency = Math.Clamp(responseProficiency, 0f, 1f); 89 90 float multiplier = MaxDelayPenaltyMultiplier + 91 (MinDelayPenaltyMultiplier - MaxDelayPenaltyMultiplier) * responseProficiency; 92 93 // Compute the scaled delay before clamping 94 float scaledDelay = baseDelayMs * multiplier; 95 96 // Apply minimum delay clamp 97 float effectiveDelay = scaledDelay < MinEffectiveDelayMs 98 ? MinEffectiveDelayMs 99 : scaledDelay; 100 101 // For logging/UI, we want baseDelayMs × multiplier ≈ effectiveDelayMs. 102 // When clamping occurs and baseDelayMs is positive, derive an "effective" multiplier 103 // from the clamped delay so that this relationship holds. 104 float effectiveMultiplier = (scaledDelay < MinEffectiveDelayMs && baseDelayMs > 0f) 105 ? effectiveDelay / baseDelayMs 106 : multiplier; 107 108 return (effectiveDelay, effectiveMultiplier); 109 } 110 111 /// <summary> 112 /// Gets the multiplier for a given response proficiency without applying it to a delay. 113 /// </summary> 114 /// <param name="responseProficiency">Operator's response proficiency (0.0-1.0)</param> 115 /// <returns>The delay multiplier (1.3 at 0.0, 1.0 at 0.5, 0.7 at 1.0)</returns> 116 public static float GetDelayMultiplier(float responseProficiency) 117 { 118 responseProficiency = Math.Clamp(responseProficiency, 0f, 1f); 119 120 return MaxDelayPenaltyMultiplier + 121 (MinDelayPenaltyMultiplier - MaxDelayPenaltyMultiplier) * responseProficiency; 122 } 123 124 /// <summary> 125 /// Calculates effective suppression decay rate based on response proficiency. 126 /// Higher proficiency = faster decay (recovery from suppression). 127 /// </summary> 128 /// <param name="baseDecayRate">Base decay rate per second</param> 129 /// <param name="responseProficiency">Operator's response proficiency (0.0-1.0)</param> 130 /// <returns>Effective decay rate per second</returns> 131 public static float CalculateEffectiveSuppressionDecayRate( 132 float baseDecayRate, 133 float responseProficiency) 134 { 135 responseProficiency = Math.Clamp(responseProficiency, 0f, 1f); 136 137 // For decay rate, we want higher proficiency = higher rate (faster recovery) 138 // Invert the multiplier logic: low proficiency = slower decay, high = faster 139 float multiplier = MinDelayPenaltyMultiplier + 140 (MaxDelayPenaltyMultiplier - MinDelayPenaltyMultiplier) * responseProficiency; 141 142 return baseDecayRate * multiplier; 143 } 144 }