/ GUNRPG.Core / Combat / ResponseProficiencyModel.cs
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  }